pax_global_header00006660000000000000000000000064152643621220014515gustar00rootroot0000000000000052 comment=923cd5748e4bb2f33945eadb77d0da8bf709125b foomuuri-0.34/000077500000000000000000000000001526436212200133105ustar00rootroot00000000000000foomuuri-0.34/.github/000077500000000000000000000000001526436212200146505ustar00rootroot00000000000000foomuuri-0.34/.github/workflows/000077500000000000000000000000001526436212200167055ustar00rootroot00000000000000foomuuri-0.34/.github/workflows/run_tests.yml000066400000000000000000000011351526436212200214560ustar00rootroot00000000000000name: Foomuuri on: push: branches: - main pull_request: branches: - main jobs: run_tests: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.9", "3.12", "3.14"] steps: - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: pip install pylint ruff ty - name: Run test suite run: make test - name: Run lint run: make lint foomuuri-0.34/.gitignore000066400000000000000000000003261526436212200153010ustar00rootroot00000000000000foomuuri-*.tar.gz test/*/*.fw test/*/iplist-cache.json test/*/zone .pre-commit-config.yaml .coverage .ruff_cache htmlcov misc/webdoc/docs misc/webdoc/final misc/webdoc/site misc/webdoc/zensical.toml __pycache__ foomuuri-0.34/.ruff.toml000066400000000000000000000024611526436212200152300ustar00rootroot00000000000000target-version = "py39" lint.select = [ "ALL", ] lint.preview = true lint.ignore = [ "ANN", "complex-structure", "missing-trailing-comma", "incorrect-blank-line-before-class", "multi-line-summary-second-line", "raw-string-in-exception", "f-string-in-exception", "commented-out-code", "future-rewritable-type-annotation", "boolean-type-hint-positional-argument", "boolean-default-value-positional-argument", "boolean-positional-value-in-call", "invalid-function-name", "too-many-return-statements", "too-many-branches", "too-many-arguments", "too-many-statements", "magic-value-comparison", "redefined-loop-name", "pytest-unittest-assertion", "pytest-fixture-param-without-value", "pytest-unittest-raises-assertion", "collection-literal-concatenation", "subprocess-popen-with-shell-equals-true", "subprocess-without-shell-equals-true", "start-process-with-partial-path", "print", "raise-vanilla-args", # --preview "missing-copyright-notice", "docstring-missing-returns", "docstring-missing-yields", "docstring-missing-exception", "reimplemented-operator", "suspicious-subprocess-import", ] lint.flake8-quotes.inline-quotes = "single" format.quote-style = "single" line-length = 79 foomuuri-0.34/CHANGELOG.md000066400000000000000000000434511526436212200151300ustar00rootroot00000000000000# Changelog ## 0.34 (2026-10-16) * BREAKING CHANGE: Iplist filter `|missing-ok` is replaced by `missing_ok=yes` setting. This setting can be defined globally or per iplist, old filter was per hostname/URL. With setting `missing_ok=yes`, a warning is printed instead of an error if: * DNS resolution or URL/file download fails. * Resolution/download succeeds, but the content is empty. With default value `missing_ok=no`, any of these conditions causes an error. * New `overwrite=yes|no` iplist setting controls how resolved IP addresses or URL content are applied to the iplist. With `yes`, old content is removed and replaced with the new addresses. With `no`, old content is kept until it expires, and new addresses are either added or have their expiry timeout updated. The default `overwrite` value depends on the content type as it was in version 0.33: * URL/file content defaults to `overwrite=yes` * DNS resolution defaults to `overwrite=no` * FUTURE BREAKING CHANGE: In next version 0.35, `overwrite`'s default value will change to `overwrite=no` for all content types. * Add support for `==`, `!=`, `<`, `>`, `<=` and `>=` compare operators when matching numeric values. For example `tcp < 1024`. * Add `length` matcher to check IP packet length in bytes. * Add support for `localhost-public { accept }` single line syntax. * Add `permanent_counter` option to `foomuuri` section (default: `no`). When enabled, named counter values are preserved across `foomuuri reload`. They are not preserved across a reboot. * Add `foomuuri counter reset` command to reset named counter values. * `foomuuri iplist list` command can list entries from all iplists or specified ones, optionally filtered to entries containing specified IP addresses. * Add `prometheus-rasdaemon`, `ceph` and `nut` macros to default services. * Fix: Macro expansion didn't handle prefix/suffix correctly if macro refers to another macro which has `something; something`. (#209) * Fix: Support numeric ranges in `uid` and `gid` matchers. (#214) * Fix: Don't add `fib daddr type multicast drop` after rule `multicast accept`. It is unneeded and confuses `nft --optimize`. (#207) * Fix: Add a warning if `zonemap` creates a loop. (#225) * Fix: Handle `szone foo bar` correctly, matching both zones. (#247) * Fix: Catch all interface `*` was not handled correctly when forwarding packets. (#238) ## 0.33 (2026-06-03) * Documentation moved to https://foomuuri.foobar.fi/latest/ * Unify command line syntax: * `foomuuri list` is renamed to `foomuuri ruleset list`. * `foomuuri list macro` is renamed to `foomuuri macro list`. * `foomuuri list counter` is renamed to `foomuuri counter list`. * Add port knocking and automatic IP address banning (fail2ban) support. These happen fully on packet path, native on nftables level. No external programs are needed. * Add support for catch all interface `*` in `zone` section, for example `public *`. It will match all unassigned interfaces. Interfaces assigned to a zone in configuration file or by NetworkManager / D-Bus will use assigned zone. * Add `iplist status [IPLIST]...` command to list number of entries in iplist. * Add `iplist_add`, `iplist_update` and `iplist_delete` matchers to add/update/delete IP address to iplist on packet path. * Add `dynamic=yes` option to `iplist` section line. This option enables dynamic flag, needed for packet path updates. * Add `element_timeout=time` option to `iplist` section line. This option sets default expire timeout for iplist elements. * Add `start=no` option to `iplist` section line. Foomuuri will not add IP addresses to such lists at startup. Entries will be added later by `foomuuri-iplist.timer` service. This can be used with "unsafe" external iplist to make sure Foomuuri start will not fail or block your access for first minutes after reboot. * Change `iplist` section option `-merge` to `merge=no`. * Add `flowtable list-of-interfaces` to `foomuuri` section. It enables Netfilter flowtable infrastructure. It improves network forward performance for high speed interfaces. * Add `matter` macro to default services. * Fix: Interface `lo` is special. Matcher `oifname lo` does not work in prerouting (DNAT) chain. Output it as `fib daddr type local` instead. ## 0.32 (2026-03-11) * BREAKING CHANGE: Prometheus exporter `foomuuri_exporter` is renamed to `prometheus-foomuuri-exporter`. * Add validators to `foomuuri { option value }` settings. * Add deprecation warning to `resolve` section, deprecated in 0.28. * Add `nftrace` statement to enable nftrace ruleset debugging for matching packets. Tracing events can be viewed with `nft monitor trace` command. * Add `url_max_size=bytes` to `iplist` section. It defines maximum content size for downloaded IP address list. Default value is 33554432, 32 MiB. * Optimize duplicate accept rules to single rule. * Add `foomuuri iplist flush` command to delete all added IP addresses from all iplists. * Add `ntske`, `prometheus-keepalived` and `prometheus-ipmi` macros to default services. * Rename macro `gluster-client` to `gluster`. Old macro name is kept as an alias. * Fix: Use `fib daddr type` to match multicast/broadcast packets. It works with WireGuard interface, old `meta pkttype` does not. * Fix: Interface `lo` is special. Nftables matcher `iifname lo` does not work in most chains. Output that as `iif 0` instead. * Fix: Allow unicast traffic in `ospf` macro. ## 0.31 (2026-01-07) * CVE-2025-67603: Add PolicyKit authorization to D-Bus methods. * CVE-2025-67858: Verify `interface` input parameter on D-Bus methods. * Security hardening: * Add `ProtectSystem=full` to all systemd service files. This changes `/etc` to read-only for all Foomuuri processes. Make sure you don't write any state files there in your startup hook or Foomuuri Monitor event hook. * Change umask to 022 when using `--fork` to fork as a background daemon process. * More strict IP address verify for iplist entries. ## 0.30 (2025-12-12) * Add `-merge` option to `iplist` section line to disable IP address auto-merge. It should be disabled in `fail2ban` alike scenarios, where IP addresses are added and deleted by some external program. * Major speedup when handling large iplists. * If all iplist entries have `|missing-ok` filter then whole iplist is considered as `Warning: Iplist is empty`. If any entry doesn't have that filter then empty list is an error. * Pipe commands to nft instead of reading them from `dbus.fw` or `iplist-cache.fw` files. * Use atomic write to `iplist-cache.json` and other files. * Add `foomuuri_exporter` binary to collect statistics and export them to Prometheus. Similar Munin collector is included to Munin contrib repository. * Include `group` states to `foomuuri-monitor` statistics file. * Add `prometheus-*` and `sips` macros to default services. * Make `python-dbus` optional for small systems. It is still highly recommended. Now Foomuuri can be run without any additional Python modules. ## 0.29 (2025-09-26) * Add `snat_prefix` and `dnat_prefix` statements for IPv6-to-IPv6 Network Prefix Translation (NPTv6). * Deprecate unneeded `to` after `snat` and `dnat` statements. It's quietly ignored. * Add `tproxy` matcher for transparent proxying. * Add `mss pmtu` to calculate the MTU in runtime based on what the routing cache has observed via Path MTU Discovery. * Fix `mss` to apply to IPv4 and IPv6. Previously it was only for IPv4. * Add `input` section. It processes incoming packets with destination `localhost`. * Add `try-reload` command to safely test new config. It loads new config, asks confirmation from user to keep it, and reverts back to old config if user didn't reply within 15 seconds. * Foomuuri Monitor supports and prefers fping's `--squiet=s` mode. It works correctly even if the interface is down. fping command option `--interval=ms` is automatically converted to `--squiet=s` if possible. * Add `command_down_interval` and `down_interval` to Foomuuri Monitor. Foomuuri Monitor will run that external command in regular intervals if network connectivity is still down. * Default log prefix can be configured with `foomuuri { log_prefix "..." }`. * Variables `$(szone)`, `$(dzone)` and `$(statement)` can be used in log prefix. * More text to default log prefix can be added with `log + "mytext"`. * Multiple interfaces can be specified to `iifname` and `oifname` matchers. Negative matching also works. * Use human readable output format in `list counter` command. * Add `prometheus`, `prometheus-*` and `alertmanager` macros to default services. * Add `--fork` command line option to fork as a background daemon process. This applies to `monitor` and `dbus` only. * Add `--syslog` command line option to enable syslog logging. * Add bash-completion (requires v2.12 or newer) script. ## 0.28 (2025-04-15) * Merge `iplist` and `resolve` sections to unified `iplist`. Old config will work as is, but updating it to new `iplist` format is recommended: simply rename `resolve {}` to `iplist {}` and check timeout and refresh options. * Add `url_timeout=10d`, `url_refresh=1d`, `dns_timeout=24h` and `dns_refresh=15m` options to `iplist` section to specify expiry timeout and refresh interval for URLs (HTTP or file) and resolved hostnames. * Old `timeout` and `refresh` options are deprecated. They set both `url_XXX` and `dns_XXX` values. * Downloaded `iplist` content can be filtered: * `|shell:/path/to/command` pipe it via external command. * `|json:filter` use external `jq` command to parse it as JSON data. * `|html:XPath` parse it as HTML data. * `|xml:XPath` parse it as XML data. * `|missing-ok` don't print warning if URL download or DNS resolve fails. * Improve `template foo` handling to support matchers and everything else that macros support. * Add `prerouting filter raw` and similar sections to allow specifying chain type and hook priority. * Add `notrack` statement to be used in prerouting section to mark packet to not be added to conntrack. * Add `10 mbytes/second` per byte support to rate limits (`global_rate` etc). * Add `over` support to rate limits to be used with `drop` statement. * Add `dscp` matcher to match packet's DSCP value. * Add `bgp` macro to default services. ## 0.27 (2025-01-28) * BREAKING CHANGES: * Previously `mark_set` was a statement. Now it's a matcher and rule's default statement `accept` is used if not specified. Usually `accept` is what you want to use. To keep previous behavior use `mark_set XX continue`. * `mark_save` and `mark_restore` are removed. They are automatically added when needed. * Multi-ISP example in GitHub wiki is updated to match above changes. * Add `priority_set` to set packet's traffic control class id. * Add `priority_match` to check packet's traffic control class id. * Add `nbd`, `pxe`, `salt`, and `tor` related macros to default services. * Add `--quiet` command line option to suppress warnings. * Print warning in `foomuuri check` if macro is overwritten. * Add more checks for invalid rules, like `ssh saddr` without address. * Add filters to `foomuuri list`: * `foomuuri list macro http https`: specified macros. * `foomuuri list macro 80 443`: macros that include value 80 or 443. * `foomuuri list counter traffic_in traffic_out`: specified counters. * Fix: `foomuuri iplist refresh` actually refreshes it now, no need for `--force`. Option `--soft` checks for next refresh time. ## 0.26 (2024-11-13) * Add `iplist flush name` command to delete all entries in iplist. * Add support for `-macro`, `macro/24`, `[macro]:123` and `macro:123` in macro expansion. * Add support for `icmp echo-request` (and other names) in addition to `icmp 8`. Use name in default `ping` macro instead of number. * Add `--force` command line option to force iplist refresh now. * Write `foomuuri-monitor` states and statistics to a file once a minute. * Add `rtsps` macro to default services. * Fix: Add range support to `iplist list`. * Fix: `iplist add` and `iplist del` didn't work correctly on all cases. ## 0.25 (2024-10-01) * Add `status` command to show if Foomuuri is running, current zone-interface mapping. * Add `set interface eth0 zone public` command to change interface to zone. * Add `set interface eth0 zone -` command to remove interface from all zones. * Add `queue` statement. It forwards packet to userspace, used for example for IPS/IDS. * Rules in `any-public` section will be added to `public-public` too. * Improve syntax error checks for rules. * More relaxed import for external `iplist` lists: handle `;` as comment, allow overlapping IP ranges. * IP address with or without netmask can be used as `resolve` or `iplist` entry. * Add warning if `resolve` hostname doesn't resolve or whole set is empty. * Add `ipsec-nat`, `pop3s`, `gluster-client`, `gluster-management`, `amqp`, `snmptrap` and `activedirectory` macros to default services. * Automatically add final `drop log` rule to zone-zone section if it is missing for improved logging. * Fix: Allow using nft's reserved words like `inet` as interface name or uid/gid. * Fix: Don't traceback if `resolve` or `iplist` refresh takes over 60 second on `foomuuri reload`. ## 0.24 (2024-06-19) * Remove `start-or-good` command line option. Add systemd `foomuuri-boot.service` to implement same functionality safer. * Add `block` command line option to load "block all traffic" ruleset. * Add `continue` statement. Rule `saddr 192.168.1.1 counter log continue` counts and logs traffic from 192.168.1.1 and continues to next rules. * Add `time` matcher to check hour, date and weekday. * Add `mac_saddr` and `mac_daddr` matchers to match MAC address. This works only for incoming traffic. * Add `ct_status` matcher to match conntrack status, for example `dnat` or `snat`. * Add `cgroup` matcher to match cgroup id or cgroupv2 name. * Add `-conntrack` flag to rule. This rule will be outputted before conntrack. This can be used to count all specific traffic, or to accept some traffic without adding it to conntrack (for example high load DNS server). * Add `redis`, `redis-sentinel`, `vnc`, `domain-quic` (DoQ) and `domain-tls` (DoT) macros to default services. * Matcher `szone -public` in `any-localhost` (or `dzone` in `localhost-any`) section can be used to skip adding rule to `public-localhost`. * Allow using any command (`curl` example included) instead of `fping` in network connectivity monitor. * Allow `foomuuri { nft_bin nft --optimize }` to specify options. * Fix: `counter myname` didn't work on `prerouting` section. * Fix: Restart network connectivity monitor `command` if it fails to start or dies. ## 0.23 (2024-03-20) * Rework `multicast` and `broadcast` handling for incoming/outgoing traffic. This simplifies macros and results more optimal ruleset. * Allow outgoing IGMP multicast membership reports, incoming IGMP query. * Change default `log_level` to `level info flags skuid` to include UID/GID for locally generated traffic. * Add `invalid`, `rpfilter` and `smurfs` sections to accept specific traffic that is normally dropped. * Add `ospf` macro to default services. * Add support for negative set matching `saddr -@geoip drop` * Fix: Separate `counter` and `accept counter` rules. * Fix: Better output for `foomuuri check` if not running as root. * Fix: Handle D-Bus change event for `lo` interface better. ## 0.22 (2023-12-12) * Add `protocol` matcher, for example `protocol gre` accepts GRE traffic. * Add support for `localhost-localhost` section and rules. If not defined, it defaults to `accept`. * Rule's log level can be set with `log_level "level crit"`. This overrides global `foomuuri { log_level ... }` setting. * Iplist refresh interval can be configured globally and per-iplist. * Pretty output for `foomuuri iplist list` instead of raw nft output. * Fix handling "[ipv6]", "[ipv6]/mask" and "[ipv6]:port" notations. ## 0.21 (2023-10-06) * Add support for `$(shell command)` in configuration file * Add support for named counters: `https counter web_traffic` * Add support for IPv6 suffix netmask: `::192:168:1:1/-64` * Add support for conntrack count rates: `saddr_rate "ct count 4"` * Add `chain_priority` to `foomuuri` section * Add lot of misconfiguration checks * `foomuuri reload` restarts firewall and refreshes resolve+iplist * `foomuuri list counter` lists all named counters * `foomuuri iplist` subcommands manipulates and lists iplist entries * Harden `dhcp`, `dhcpv6`, `mdns` and `ssdp` macros * Fix icmp to handle matchers correctly (`ping saddr 192.168.1.1 drop`) * Fix caching failed `resolve` section lookups for reboot ## 0.20 (2023-08-15) * Multi-ISP support with internal network connectivity monitor. See wiki's best practices for example configuration. * Add `mark_set`, `mark_restore` and `mark_save` statements * Add `mark_match` matcher * Add `prerouting`, `postrouting`, `output` and `forward` sections for marks * Expand macros and support quotes in `foomuuri` section * Fix running pre/post_stop hooks on `foomuuri stop` * Fix man page section from 1 to 8, other Makefile fixes * Foomuuri is now included to Fedora, EPEL and Debian. Remove local build rules for rpm/deb packages. ## 0.19 (2023-05-19) * Add `iplist` section to import IP address lists. These can be used to import IP country lists, whitelists, blacklists, etc. * Add `hook` section to run external commands when Foomuuri starts/stops * Fix `dhcpv6-server` macro in default.services.conf * Add man page and improve documentation * Add experimental connectivity monitor which will be used for upcoming multi ISP support ## 0.18 (2023-04-18) * Add rule line validator to configuration file parser * Rename zone `fw` to `localhost` and make in configurable * Initial deb packaging * Improve documentation * Lot of internal cleanup ## 0.17 (2023-03-31) * Add `foomuuri list macro` to list all known macros * New default.services.conf entries: mqtt, secure-mqtt, zabbix * Improve documentation * Lot of internal cleanup ## 0.16 (2023-02-27) * First public release foomuuri-0.34/COPYING000066400000000000000000000432541526436212200143530ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. foomuuri-0.34/Makefile000066400000000000000000000075451526436212200147630ustar00rootroot00000000000000# Foomuuri - Multizone bidirectional nftables firewall. .PHONY: all lint test devel clean distclean install sysupdate release tar CURRENT_VERSION ?= $(shell grep ^VERSION src/foomuuri | awk -F\' '{ print $$2 }') # Default target is to run lint and test all: lint test # Run testsuite and check source # # Enable test coverage report: make clean test COVERAGE=y && coverage html lint test: $(MAKE) -C test $@ # Generate firewall ruleset to file, used in development devel: unshare --map-root-user --net src/foomuuri --set=etc_dir=../devel --set=share_dir=etc --set=state_dir=../devel --set=run_dir=../devel check # Delete created files clean distclean: rm -rf src/__pycache__ src/tests/__pycache__ htmlcov .ruff_cache rm -f .coverage foomuuri-*.tar.gz $(MAKE) -C test $@ # Install current source to DESTDIR BINDIR ?= /usr/sbin SYSTEMD_SYSTEM_LOCATION ?= /usr/lib/systemd/system install: mkdir -p $(DESTDIR)/etc/foomuuri/ mkdir -p $(DESTDIR)$(BINDIR)/ cp src/foomuuri $(DESTDIR)$(BINDIR)/ mkdir -p $(DESTDIR)/usr/lib/sysctl.d/ cp etc/50-foomuuri.conf $(DESTDIR)/usr/lib/sysctl.d/50-foomuuri.conf mkdir -p $(DESTDIR)/usr/share/foomuuri/ cp etc/default.services.conf $(DESTDIR)/usr/share/foomuuri/ cp etc/static.nft $(DESTDIR)/usr/share/foomuuri/ cp etc/block.fw $(DESTDIR)/usr/share/foomuuri/ mkdir -p $(DESTDIR)$(SYSTEMD_SYSTEM_LOCATION)/ cp systemd/foomuuri.service $(DESTDIR)$(SYSTEMD_SYSTEM_LOCATION)/ cp systemd/foomuuri-boot.service $(DESTDIR)$(SYSTEMD_SYSTEM_LOCATION)/ cp systemd/foomuuri-dbus.service $(DESTDIR)$(SYSTEMD_SYSTEM_LOCATION)/ cp systemd/foomuuri-iplist.service $(DESTDIR)$(SYSTEMD_SYSTEM_LOCATION)/ cp systemd/foomuuri-iplist.timer $(DESTDIR)$(SYSTEMD_SYSTEM_LOCATION)/ cp systemd/foomuuri-monitor.service $(DESTDIR)$(SYSTEMD_SYSTEM_LOCATION)/ mkdir -p $(DESTDIR)/usr/lib/tmpfiles.d/ cp systemd/foomuuri.tmpfilesd $(DESTDIR)/usr/lib/tmpfiles.d/foomuuri.conf mkdir -p $(DESTDIR)/run/foomuuri/ mkdir -p $(DESTDIR)/var/lib/foomuuri/ mkdir -p $(DESTDIR)/usr/share/dbus-1/system.d/ mkdir -p $(DESTDIR)/usr/share/polkit-1/actions/ cp systemd/fi.foobar.Foomuuri1.conf $(DESTDIR)/usr/share/dbus-1/system.d/ cp systemd/fi.foobar.Foomuuri1.policy $(DESTDIR)/usr/share/polkit-1/actions/ cp firewalld/fi.foobar.Foomuuri-FirewallD.conf $(DESTDIR)/usr/share/dbus-1/system.d/ cp firewalld/dbus-firewalld.conf $(DESTDIR)/usr/share/foomuuri/ mkdir -p $(DESTDIR)/usr/share/man/man8 cp man/foomuuri.8 $(DESTDIR)/usr/share/man/man8/ mkdir -p $(DESTDIR)/usr/share/man/man1 cp man/prometheus-foomuuri-exporter.1 $(DESTDIR)/usr/share/man/man1/ # Install current source to local system's root sysupdate: make install DESTDIR=/ systemctl daemon-reload # Make new release release: test clean @if [ -z "$(VERSION)" ]; then \ echo "Usage: make release VERSION=x.xx"; \ echo; \ echo "Current: $(CURRENT_VERSION)"; \ exit 1; \ fi git diff --exit-code git diff --cached --exit-code grep --quiet VERSION=$(VERSION) misc/webdoc/Makefile || false "Update misc/webdoc/versions.json and Makefile" sed -i -e "s@^\(## ${VERSION} .\)20..-xx-xx.@\1$(shell date +'%Y-%m-%d')\)@" CHANGELOG.md sed -i -e "s@^\(VERSION = '\).*@\1$(VERSION)'@" src/foomuuri sed -i -e "s@^\(footer: .* \).*@\1$(VERSION)@" man/foomuuri.md man/prometheus-foomuuri-exporter.md sed -i -e "s@^\(date: \).*@\1$(shell date +'%b %d, %Y')@" man/foomuuri.md man/prometheus-foomuuri-exporter.md make --directory=man git diff git add CHANGELOG.md src/foomuuri man/foomuuri.md man/foomuuri.8 man/prometheus-foomuuri-exporter.md man/prometheus-foomuuri-exporter.1 git commit --message="v$(VERSION)" git tag "v$(VERSION)" @echo @echo "== TODO ==" @echo "git push && git push --tags" @echo "GitHub release: https://github.com/FoobarOy/foomuuri/releases/new" # Build tarball locally tar: clean tar cavf foomuuri-$(CURRENT_VERSION).tar.gz --transform=s,,foomuuri-$(CURRENT_VERSION)/, --show-transformed .gitignore * foomuuri-0.34/README.md000066400000000000000000000037601526436212200145750ustar00rootroot00000000000000# Foomuuri Foomuuri is a multizone bidirectional nftables firewall. Full documentation is available [here](https://foomuuri.foobar.fi/latest/). Example configuration files are available for a [host firewall](https://foomuuri.foobar.fi/latest/example/host-firewall/) and a [router firewall](https://foomuuri.foobar.fi/latest/example/router-firewall/). See the [installation](https://foomuuri.foobar.fi/latest/install/) page for instructions on installing Foomuuri. Help is available via [GitHub discussions](https://github.com/FoobarOy/foomuuri/discussions) and the `#foomuuri` IRC channel on Libera.Chat. ## Features * Firewall zones * Bidirectional firewalling for incoming, outgoing, and forwarding traffic * Suitable for systems ranging from personal laptops to corporate firewalls * Rich rule language for flexible and complex rules * Predefined list of services for simpler rule writing * Macros and templates supported in the rule language * IPv4 and IPv6 support, with automatic rule splitting per protocol * SNAT, DNAT, and masquerading support * Logging and packet/byte counting * Rate limiting * DNS hostname lookups and IP-list support, with dynamic IP address refreshing * External country and ASN IP lists through iplists (geolocation) * Multiple ISP support, with an internal network connectivity monitor * IPsec matching support * Ability to map specific traffic to separate zones * Automatic IP address banning support * Port knocking support * D-Bus API * Firewalld emulation for NetworkManager zone support * Support for raw nftables rules * Written to use modern nftables features ## Example Configuration Example configuration file to filter incoming traffic only: ``` zone { localhost public eth1 # Interface list may be empty if using NetworkManager. # Glob patterns like * or e* are supported too. } public-localhost { # Allow specified incoming traffic dhcp-client dhcpv6-client ping ssh drop log } localhost-public { # Allow all outgoing traffic accept } ``` foomuuri-0.34/docs/000077500000000000000000000000001526436212200142405ustar00rootroot00000000000000foomuuri-0.34/docs/config/000077500000000000000000000000001526436212200155055ustar00rootroot00000000000000foomuuri-0.34/docs/config/basic.md000066400000000000000000000077271526436212200171250ustar00rootroot00000000000000# Basics ## Configuration Files Foomuuri reads user configuration files from `/etc/foomuuri/*.conf`, in alphabetical order, including all subdirectories. Foomuuri also reads static configuration from `/usr/share/foomuuri/*.conf`, which can be overridden in `/etc/foomuuri`. Configuration can be written to a single file or split across multiple files. The following is a recommendation, not a rule: * Simple configurations (fewer than 200 lines) can go in a single `/etc/foomuuri/foomuuri.conf` file. * Larger configurations should be split across `/etc/foomuuri/foomuuri.conf`, `/etc/foomuuri/localhost.conf`, `/etc/foomuuri/public.conf`, etc.: * `foomuuri.conf` contains everything except the zone-zone sections. * `localhost.conf` contains all `xxx-localhost` (or `localhost-xxx`) sections. * `public.conf` contains all `xxx-public` (or `public-xxx`) sections. * Subdirectories can be used, for example `/etc/foomuuri/zones.d/localhost.conf`. * Very large configurations could be split further, into files such as `localhost-public.conf`, each containing a single zone-zone section. Raw `nftables` rules can be written to `/etc/foomuuri/*.nft` and will be included in the generated ruleset. ## Zone Names The following zone names are recommended, but you may use whatever names you prefer. ### localhost `localhost` is the zone name for the computer running Foomuuri, similar to "localhost" in hostnames. If you use a different name, you must configure it in the [foomuuri { localhost_zone }](section/foomuuri.md) section. ### public `public` is the default external network zone, similar to "the internet". A basic host firewall has only `localhost` and `public` zones. If you use a different name, you should also configure it in the [foomuuri { dbus_zone }](section/foomuuri.md) section. `public` is intended for use in a public location, where you do not trust the other computers on the network not to harm your computer. ### home Similar to `public`, but for use in a home location, where you mostly trust the other computers on the network not to harm your computer. ### work Similar to `public`, but for use in a work location, where you mostly trust the other computers on the network not to harm your computer. ### internal `internal` is your internal network ("intranet") zone, for router firewall configurations. Remote connections from `public` should not be allowed to reach `internal`. ### dmz The demilitarized zone is the publicly accessible part of your internal network. Only selected incoming connections should be accepted from `public` to `dmz`, for example `https`. ### vpn IPsec and similar VPN traffic. ## Miscellaneous Comments in the configuration files are written as `# comment`. A long line can be split across multiple lines by adding `\` at the end of each line. Multiple words can be combined into a single token by enclosing them in quotes. For example, `ssh accept log "accept ssh for testing"` accepts SSH traffic and logs it with the message `accept ssh for testing`. The output of an external command can be used to generate rules. It may return a single line, multiple lines, or part of a line. The syntax is `$(shell command to run with parameters)`. The command runs in a shell, so pipes and `;` work as expected. Be careful to run only trusted commands. `$(shell)` is processed before [macro](section/macro.md) expansion. Example: ``` # This is a comment macro { # Define the local_port_range macro by reading the correct value from /proc. local_port_range $(shell sed s/\\t/-/ < /proc/sys/net/ipv4/ip_local_port_range) } localhost-public { ssh # This is a comment smtp \ daddr 192.0.2.32 # Allow SMTP to a single IP } public-localhost { tcp local_port_range # Allow TCP to ports 32768-60999 (default range) } ``` The command being run cannot contain a `)` character. For such complex commands, it's better to create a shell script and call that instead: ``` macro { mymacro $(shell /etc/foomuuri/mymacro.sh parameters) } ``` foomuuri-0.34/docs/config/rule/000077500000000000000000000000001526436212200164545ustar00rootroot00000000000000foomuuri-0.34/docs/config/rule/index.md000066400000000000000000000005171526436212200201100ustar00rootroot00000000000000# Rule Each line in a configuration section is a single rule. A rule consists of optional matchers and a statement. The order of matchers and the statement within a rule does not matter. This document uses the order "service - source - destination - other-matchers - statement - log", for example: `http saddr 10.1.1.1 drop log`. foomuuri-0.34/docs/config/rule/logging.md000066400000000000000000000064371526436212200204360ustar00rootroot00000000000000# Logging & Counters ## Rule Counters ### counter Adds a byte and packet counter to a rule. All new traffic matching this rule is counted. A counter can be named or anonymous. To name a counter, add a name after `counter`, for example `counter my_counter`. Example: ``` localhost-public { # Add a named counter to count all outgoing traffic. Flag "-conntrack" # is used to count established traffic too. counter outgoing_traffic continue -conntrack # Accept SSH and add an anonymous counter for it ssh counter # Accept HTTP and HTTPS and add a named counter http counter web_traffic https counter web_traffic # Reject SMTP with a named counter smtp reject counter smtp_blocked } ``` Named counter values can be listed with `foomuuri counter list`. Anonymous counters can be listed with `foomuuri ruleset list`. ## Rule Logging & Log Levels ### log Writes a log entry (journal or syslog) when traffic matches this rule. An optional log prefix can be added; the default prefix is `szone-dzone STATEMENT`, for example `localhost-public REJECT`. This results in a journal entry such as `kernel: localhost-public REJECT IN= OUT=eth0 ...`. The following variables are supported in the log prefix: * `$(szone)` * `$(dzone)` * `$(statement)` Additional text can be appended to the default log prefix with `log + " my text"`, resulting in `localhost-public REJECT my text`. Example: ``` public-localhost { # Drop and log SSH with the default prefix "public-localhost DROP" ssh drop log # Drop and log HTTP with a custom prefix, "incoming-http dropped" http drop log "incoming-http dropped" # Drop and log HTTPS with a custom prefix using variables. This results # in the prefix "public => localhost: DROP" https drop log "$(szone) => $(dzone): $(statement)" # Drop and log telnet with the custom prefix "public-localhost DROP:telnet" telnet drop log + ":telnet" # no space, so the result is "DROP:telnet" # Drop and log ftp with the custom prefix "public-localhost DROP ftp-is-disabled" ftp drop log + " ftp-is-disabled" # a space is included here # Use the default prefix "public-localhost DROP" drop log } ``` Foomuuri limits logging to the [log_rate](../section/foomuuri.md) rate. By default, the first three entries per source IP are logged, followed by one additional entry per second. ### log_level Overrides the global `foomuuri { log_level ... }` logging level for this single rule. Possible values are: * `level emerg` * `level alert` * `level crit` * `level err` * `level warn` * `level notice` * `level info` * `level debug` Flags can optionally be appended: * `flags tcp sequence,options` enables logging of TCP sequence and options * `flags ip options` enables IP options * `flags skuid` enables the socket UID * `flags ether` enables the Ethernet link-layer address * `flags all` enables all flags To use the nflog infrastructure instead of syslog, specify `group 0` (or any other number) instead of `level x`. Nflog options can be appended: * `snaplen 256` specifies the length of packet payload to include * `queue-threshold 20` queues packets in the kernel before sending them to userspace Example: ``` public-localhost { # Drop and log incoming http requests at critical level, with all flags http drop log log_level "level crit flags all" ... } ``` foomuuri-0.34/docs/config/rule/matcher.md000066400000000000000000000262161526436212200204300ustar00rootroot00000000000000# Matchers ## Protocols, Addresses & Interfaces ### tcp, udp, icmp, icmpv6 Matches TCP, UDP, ICMP, or ICMPv6 traffic. This matcher is usually followed by a port number; for example, `tcp 443` matches TCP traffic to port 443 (HTTPS). Without a port number, all traffic is matched. See [protocol](matcher.md#protocol) for how to match other protocols. Rather than using `tcp 443` directly, it is recommended to use the predefined `https` macro. All [known macros](https://github.com/FoobarOy/foomuuri/blob/main/etc/default.services.conf) can be listed with `foomuuri macro list`. ### protocol Matches traffic for a specific protocol. For example, `protocol gre` matches all GRE traffic, and `protocol sctp 22` matches SCTP traffic to port 22. For TCP, UDP, ICMP, and ICMPv6, it is recommended to use the [shortcut](matcher.md#tcp-udp-icmp-icmpv6) matchers instead. ### sport, dport Matches the source/destination port, followed by a port number. The `dport` matcher is usually optional: `tcp 443` is equivalent to `tcp dport 443`. Port number can be: * Single number, `443` * Multiple numbers, `80 443`, matching 80 or 443 * Range, `8880-9000` * Range and a number, `80 443 8880-9000` * Negative number, `-443`, matching everything but 443 * Multiple negative numbers, `-80 -443`, matching everything but 80 or 443 ### saddr, daddr Matches the source/destination IPv4 or IPv6 address. An address can be: * Single IPv4 address, `10.0.0.1` * Single IPv6 address, `fd00:f00::1` * Address with a netmask, `10.0.0.0/8` or `fd00:f00::/32` * IPv6 address with a suffix netmask, `::10:0:0:f00/-64`, to match with the netmask `::ffff:ffff:ffff:ffff` * Interval, `10.0.0.10-10.0.0.20` or `fd00:f00::10-fd00:f00::20` * Multiple addresses, `10.0.0.1 10.0.0.10-10.0.0.20 fd00:f00::/32`. Both IPv4 and IPv6 addresses can be listed in the same rule; Foomuuri automatically splits them into the correct traffic chains. * Negative address, `-10.0.0.1` or `-fd00:f00::1`, meaning all other addresses * Negative IPv6 address with a suffix netmask, `-::10:0:0:f00/-64` * Multiple negative addresses, `-10.0.0.1 -fd00:f00::1` * Iplist set name, `@listname` * Negative iplist set name, `-@listname` This matcher is usually combined with the `tcp port` matcher to allow traffic from a specific source IP only, `tcp 443 saddr 10.0.0.1`, or to a specific destination IP. ### mac_saddr, mac_daddr Matches the source/destination MAC address for incoming traffic. This does not work for outgoing traffic. An address can be: * Single MAC address, `01:23:45:67:89:ab` * Multiple MAC addresses, `01:23:45:67:89:ab 01:23:45:67:89:cc` * Negative MAC address, `-01:23:45:67:89:ab`, meaning all other addresses * Multiple negative MAC addresses, `-01:23:45:67:89:ab -01:23:45:67:89:cc` Example: ``` internal-localhost { # Drop spoofed traffic from 10.0.0.3 saddr 10.0.0.3 mac_saddr -12:00:27:00:00:ce drop log "MAC-SPOOF" ... } ``` ### iifname, oifname Matches the incoming/outgoing interface name. This is mostly used in the `snat`, `dnat`, and `zonemap` sections. For example, in `snat`, `saddr 10.0.0.0/8 oifname eth0 masquerade` matches all traffic coming from 10.0.0.0/8 that is going out via eth0, and masquerades it. Multiple interface names can be specified. Negative interface name(s) can also be used, meaning all but the specified name(s). ### multicast, broadcast Matches multicast or broadcast traffic. Foomuuri silently drops all incoming multicast and broadcast traffic unless it is explicitly accepted by a rule. The Linux kernel can match multicast/broadcast only for incoming traffic. Therefore, Foomuuri generates the outgoing nft rule without the multicast/broadcast matcher, even if one was specified in the rule. This makes it easy to use the same rule or macro for all traffic, regardless of whether it is incoming or outgoing. The rule `broadcast udp 11430` allows incoming broadcast messages to UDP port 11430, and all outgoing traffic to UDP port 11430. The rule `multicast` allows all incoming multicast traffic, and nothing for outgoing. It is highly recommended to always specify `daddr` for multicast rules. Example: ``` public-localhost { # Allow all incoming multicast traffic multicast ... } localhost-public { # Allow some outgoing multicast addresses. This rule can also be written # without the "multicast" matcher, since it is omitted for outgoing # traffic anyway. multicast daddr 224.0.0.0/8 239.0.0.0/8 ff00::/8 ... } ``` It is much better to accept only specific multicast/broadcast traffic rather than everything. For example, the `ssdp` macro is defined as: ``` macro { ssdp multicast udp 1900 daddr 239.255.255.250 ff02::c; udp sport 1900 } public-localhost { # Allow incoming ssdp ssdp ... } localhost-public { # Allow outgoing ssdp ssdp ... } ``` This macro allows incoming traffic to specific multicast addresses and UDP port 1900, outgoing traffic to the same addresses and port, and finally unicast traffic from the same port. ### sipsec, dipsec Matches incoming/outgoing IPsec traffic. These are useful when you want to allow traffic to or from IPsec without creating a separate `vpn` zone for it. Example: ``` public-localhost { ssh # Allow SSH with and without IPsec tcp 1234 sipsec # Allow TCP 1234 with IPsec only ... } ``` To split IPsec and non-IPsec traffic into `vpn` and `public` zones, you can use these matchers in the `zonemap` [section](../section/zonemap.md). It is usually unnecessary to create a separate zone for this. The negative matchers `-sipsec` and `-dipsec` can also be used; they match non-IPsec traffic. These are useful in the `snat` and `dnat` [sections](../section/snat.md). ### length Matches the IP packet length in bytes. Length includes the protocol header. Example: ``` public-localhost { # Allows pings up to 91 bytes, since the ICMP header is 8 bytes ping length < 100 ... } ``` ### ipv4, ipv6 A single rule applies to both IPv4 and IPv6 traffic by default. Adding an `ipv4` or `ipv6` matcher to a rule limits it to IPv4 or IPv6 only. ## User & Process Identification ### uid, gid Matches traffic generated by a given uid/gid. This only works for traffic originating from `localhost`. For example, `tcp 2703 uid amavis` in the `localhost-public` section allows outgoing TCP 2703 traffic from the user `amavis`. Multiple uid/gid names or numbers can be specified. Negative value(s) also work, meaning all but the specified value(s). Only the effective gid is matched; matching against supplementary groups does not work. ### cgroup Matches a cgroup ID or cgroupv2 name. The argument can be: * Single number, `1234` * Multiple numbers, `1234 1244` * Range, `4000-5000` * Range and a number, `1234 1244 4000-5000` * Negative number, `-1234`, matching everything but 1234 * Multiple negative numbers, `-1234 -1244`, matching everything but 1234 or 1244 * cgroupv2 name, `user.slice` or `system.slice/sshd.service` **Warning**: A cgroupv2 must exist before Foomuuri starts. Since Foomuuri starts very early during boot, using this feature incorrectly can break your firewall startup. ## Packet Marking & QoS ### mark_match Matches the packet's mark. The argument can be: * Number, `42` or `0x2a` * Number with a mask, `0x100/0xff00`, meaning check whether `mark and 0xff00` equals `0x100` * Negative number, `-42`, meaning any mark other than `42` * Negative number with a mask, `-0x0000/0xff00`, for a not-equal check ### mark_set Sets the packet's mark. The argument can be: * Number, `42` or `0x2a` * Number with a mask, `0x100/0xff00`, meaning set the bits `0xff00` to `0x100`. In other words, this keeps the lower 8 bits as they are and sets the upper 8 bits. This is not a normal matcher, since it matches everything. Usually, some other matcher should be applied first. Example: ``` prerouting { iifname eth0 mark_set 0x100/0xff00 # Mark traffic from eth0 and accept it iifname eth1 mark_set 0x200/0xff00 # Mark traffic from eth1 and accept it mark_set 0x300/0xff00 # Mark all other traffic } ``` ### priority_match Matches the packet's traffic control class ID. The argument can be: * Class ID, `1:ff01` or `1:0xff01` (the `0x` is optional) * Text `none`, for no priority set ### priority_set Sets the packet's traffic control class ID. This is not a normal matcher, since it matches everything. Usually, some other matcher should be applied first. Example: ``` forward { daddr 192.168.0.0/16 priority_match none priority_set 1:ff01 saddr 192.168.0.0/16 priority_match none priority_set 1:ff01 priority_match none priority_set 1:2 } ``` ### dscp Matches the packet's Differentiated Services Code Point (DSCP) value. Example: ``` internal-public { saddr 10.0.0.4 dscp 10 dscp af13 } ``` ## Dynamic IP Lists ### iplist_add Adds the packet's source or destination IP address to an [iplist](../section/iplist.md), using the default `element_timeout`. The timeout is not updated if the IP address is already in the iplist. Two arguments are required: `saddr` or `daddr`, and the iplist `@name`. `iplist_update` is usually the better choice, since it updates the timeout. ### iplist_update Adds or updates the packet's source or destination IP address in an [iplist](../section/iplist.md), using the default `element_timeout`. The timeout is updated if the IP address is already in the iplist. Two arguments are required: `saddr` or `daddr`, and the iplist `@name`. See [port knocking](../../example/advanced.md#port-knocking) and automatic IP address [banning](../../example/advanced.md#automatic-ip-address-banning) for examples. ### iplist_delete Deletes the packet's source or destination IP address from an [iplist](../section/iplist.md). Two arguments are required: `saddr` or `daddr`, and the iplist `@name`. See [port knocking](../../example/advanced.md#port-knocking) for an example. ## Conntrack, Time & Proxying ### ct_status Matches the packet's conntrack status, mostly used with [SNAT or DNAT](../section/dnat.md). Valid arguments are: expected, seen-reply, assured, confirmed, snat, dnat, dying. ### time Matches the current time, date, and day of the week. The argument can be: * Time, `hh:mm` or `hh:mm:ss` * Time interval, `hh:mm-hh:mm` (see the warning below) * Date, `yyyy-mm-dd` * Day of the week: `Monday`, `Tuesday`, `Wednesday`, `Thursday`, `Friday`, `Saturday`, `Sunday` * Comparison operator: `==` (equal, the default), `!=` (not equal), `<`, `>`, `<=`, `>=` * Any combination of the above **Warning**: Some versions of `nft` do not correctly handle a time interval that crosses midnight UTC. If your timezone is +03:00, the interval `23:00-02:00` works, but `02:00-06:00` fails. Example: ``` public-localhost { # Allow until the year 2025 tcp 1234 time "< 2025-01-01" # Kids, go to bed! Reject traffic at night. saddr @kids time "23:00-07:00" reject # An unusual combination: allow traffic on Mondays from 16:00-22:00, # until the year 2025 tcp 5001 time "Monday 16:00-22:00 < 2025-01-01" } ``` ### tproxy Transparent proxy traffic to an IP address:port. Example: ``` prerouting { # Use the lower 8 bits to mark tproxy traffic mark_match -0x00/0xff # Anti-loop protection # All IPv4 and IPv6 TCP traffic tcp tproxy 127.0.0.1:8888 [::1]:8888 mark_set 0x01/0xff } ``` foomuuri-0.34/docs/config/rule/misc.md000066400000000000000000000052341526436212200177350ustar00rootroot00000000000000# Miscellaneous Rules ## TCP & Packet Alterations ### mss Sets the maximum segment size (MSS clamping) for matched traffic. Some connections, such as IPsec or PPPoE, may require this. Example: ``` localhost-vpn { mss 1390 ssh reject log } ``` The special value `mss pmtu` can be used to calculate the value at runtime, based on what the routing cache has observed via Path MTU Discovery (PMTUD). Example: ``` forward { # internal-public mss pmtu } input { # public-localhost mss pmtu } output { # localhost-public mss pmtu } ``` [Matchers](matcher.md) can be used with `mss`, but [statements](statement.md) cannot. ## Zone & Connection Tracking Modifiers ### szone, dzone, new_szone, new_dzone These can be specified in the [zonemap](../section/zonemap.md) section to match the original source or destination zone, and to change it to a new zone. They are used to branch specific traffic out into its own zone, for example to split `vpn` and `public` (non-VPN) traffic. ### conntrack, -conntrack By default, rules are processed after the conntrack check (flag `conntrack`). The flag `-conntrack` can be added to process a rule before conntrack. Conntrack accepts established and related traffic, so a normal rule only sees new traffic. This can be used to count all traffic rather than just new connections, or to accept traffic without adding it to conntrack. For example, a high-load DNS server might accept DNS traffic without conntrack. Example: ``` public-localhost { # Count all incoming traffic counter incoming_traffic continue -conntrack # Count incoming HTTP(S) traffic on a web server tcp dport 80 443 counter web_traffic_in continue -conntrack ... } localhost-public { # Count outgoing HTTP(S) traffic on a web server tcp sport 80 443 counter web_traffic_out continue -conntrack ... } ``` ### helper The Linux kernel provides conntrack helper functionality for some services with multiple ports, such as `ftp` (`tcp 21`). You can enable this functionality by appending `helper kernelname-port` after a matcher, for example `tcp 21 helper ftp-21`. Linux provides the following helpers: `amanda, ftp, h323, irc, netbios_ns, pptp, sane, sip, snmp, tftp`. ## Raw Rules ### nft A raw nftables rule can be written with `nft "raw rule here"`. For example, the `https` rule can be written as: ``` public-localhost { nft "tcp dport 443 accept" nft "udp dport 443 accept" ... } ``` `nft` can also be combined with matchers: ``` # Jump to my-custom-chain for UDP traffic to ports 1000-2000 udp 1000-2000 nft "jump my-custom-chain" ``` See the [nftables website](https://wiki.nftables.org/) for more information about nftables syntax. foomuuri-0.34/docs/config/rule/ratelimit.md000066400000000000000000000112341526436212200207710ustar00rootroot00000000000000# Rate Limit ## Global Rate Limits ### global_rate Defines a global rate limit without a source or destination IP address check. For example, `https global_rate "10/second burst 20"` allows HTTPS traffic at the following rate: * The first 20 new connections are allowed without limit. * Once the burst is used up, up to 10 new connections per second are allowed. * This rate is "global", meaning a single source IP can use all of the available slots, or 20 different sources can each use one slot. * Here, "connection" means a new connection, not the total number of established, active connections (see `ct count` below). There are three types of rates: * New-connection rates (`x/time burst y`), which ignore whether some connections have already been closed: * `"7/second burst 30"` * `"3/second"` (no burst specified, so `burst 5` is assumed) * `"1/minute burst 1"` * `"50/minute burst 200"` * `"30/minute"` * `"100/hour burst 200"` * `"100/hour"` * `"over 8/second burst 10"` * Bandwidth rates (`x bytes/time burst y bytes`): * `"10 mbytes/second burst 12000 kbytes"` * `"10 mbytes/second"` * `"over 10 mbytes/second burst 12000 kbytes"` * `"over 10 mbytes/second"` * Conntrack rates (`ct count x`), counting the total number of established, active connections: * `"ct count 5"` * `"ct count over 6"` A new-connection rate limit can be visualized as a leaking water bucket. `burst 10` specifies the bucket size as 10 units. `3/minute` specifies how fast it leaks - 3 units per minute, or 1 unit every 20 seconds. Every new connection adds one unit of water to the bucket. If it fits, the rule matches (usually meaning the connection is accepted); if the bucket overflows, the rule doesn't match. If burst is not specified, the Linux kernel assumes a value of 5. The minimum value for burst is 1. Some examples: * `5/minute burst 1` allows one new connection (`burst 1`), plus one new connection every 12 seconds (`5/minute`). * `3/minute burst 5` allows up to 5 new connections, then one new connection every 20 seconds once those 5 are used up. A bandwidth rate can be used as a simple traffic limiter, either per service or in total. Example: ``` public-localhost { # Limit all incoming traffic to 30 MiB/s, total across all traffic global_rate "over 30 mbytes/second" drop -conntrack # Limit incoming SSH traffic to 2 MiB/s, per service ssh global_rate "over 2 mbytes/second" drop -conntrack # Allow incoming SSH connections ssh ... } ``` The rate `ct count 5` matches if there are up to 5 established connections, including the current one. So the rule `ssh global_rate "ct count 2"` allows two SSH connections (one existing, plus the current one). The rate `ct count over 6` matches if there are more than 6 established connections. This is usually used with the `drop` statement: `ssh global_rate "ct count over 6" drop`. ## Address-Specific Rate Limits ### saddr_rate, daddr_rate Source/destination IP address-specific rate limiting works similarly to `global_rate`. `saddr_rate` limits the number of connections or amount of bandwidth from a single source IP address, but applies no total (global) limit. `daddr_rate` limits the number of connections or amount of bandwidth to a single destination IP address - usually a host in the `dmz` zone. Both `saddr_rate` and `daddr_rate` can be specified on a single rule. For example, `https saddr_rate "10/second" daddr_rate "1000/second"` specifies: * A single IP can open 10 new connections per second. * A total of 1000 connections per second are allowed across all IPs. ### saddr_rate_mask, daddr_rate_mask Normally, full IPv4 or IPv6 addresses are used when counting `saddr_rate` or `daddr_rate`. This can be changed with a netmask: `ping saddr_rate "5/second burst 20" saddr_rate_mask 24 56` uses `/24` and `/56` instead of the full IP address when counting limits. ### saddr_rate_name, daddr_rate_name To share the same rate limit across two different rules, specify a name for it. For example: ``` http saddr_rate "30/second burst 50" saddr_rate_name http_limit https saddr_rate "30/second burst 50" saddr_rate_name http_limit ``` This counts HTTP and HTTPS traffic together as a single rate, allowing a total of 30 connections per IP per second. Without the shared name, it would allow 30 + 30 connections per IP per second. ### saddr_daddr_rate, saddr_daddr_rate_mask, saddr_daddr_rate_name This is a special form of `saddr_rate` combined with `daddr_rate`, useful when a service has multiple destination IPs (as with DNS round-robin). In this rule, both the source and destination address are used as the count key, instead of just the source or destination alone. foomuuri-0.34/docs/config/rule/statement.md000066400000000000000000000046041526436212200210060ustar00rootroot00000000000000# Statements ## Action Statements ### accept, drop, reject Accepts, drops, or rejects traffic. The default statement for a rule is to accept matched traffic: `tcp 443` is equivalent to `tcp 443 accept`. You should always add an explicit final statement as the last rule in every [zone-zone](../section/zonezone.md) section of your configuration. * For incoming traffic from the internet to localhost/intranet, the recommended statement is `drop log`. * For outgoing traffic from localhost/intranet to the internet, the recommended statement is `reject log`. ### continue Continues to the next rule. This is mostly used for debugging rules. For example, the rule `saddr 10.0.0.4 counter log continue` counts and logs traffic from 10.0.0.4, then continues to the next rule. ### return A special statement that returns from the current nftables chain to the caller chain. Not normally used. ## Address Rewriting (NAT) ### masquerade, snat, dnat, snat_prefix, dnat_prefix These statements are used in the `snat` and `dnat` [sections](../section/snat.md) to rewrite the traffic's source or destination IP address. See the [SNAT](../section/snat.md) and [DNAT](../section/dnat.md) pages for full descriptions and examples. ## Packet Inspection & Flow Control ### notrack Marks a matching packet so that it is not added to conntrack. This must be done early in the `prerouting` section. For example, a high-load DNS server can use this for DNS queries. Example: ``` # Incoming traffic prerouting filter raw { domain notrack # dport tcp sport 53 notrack # sport udp sport 53 notrack } # Locally generated traffic output filter raw { domain notrack tcp sport 53 notrack udp sport 53 notrack } ``` ### queue Forwards a packet to userspace, for example for IPS/IDS inspection. Optional flags and a target can be specified. Example: ``` forward { # Forward all packets to userspace for IPS inspection queue flags fanout,bypass to 3-5 # Forward matching packets only iifname eth0 oifname eth1 queue } ``` ### nftrace Enables nftrace ruleset debugging for matching packets. This is usually done in the `input`, `output`, or `forward` section. Trace events can be viewed with the `nft monitor trace` command. Example: ``` input { # Trace all incoming packets - this generates a lot of trace output! nftrace } forward { # Trace all forwarded ssh packets ssh nftrace } ``` foomuuri-0.34/docs/config/section/000077500000000000000000000000001526436212200171515ustar00rootroot00000000000000foomuuri-0.34/docs/config/section/any.md000066400000000000000000000031701526436212200202630ustar00rootroot00000000000000# zone-any, any-zone, any-any These sections are similar to a zone-zone section, except that they match any destination (zone-any), any source (any-zone), or all traffic (any-any). These [rules](../rule/index.md) are processed first, and processing then continues to the normal zone-zone rules. These rules are not added to [`localhost-localhost`](zonezone.md#localhost-localhost) section. Foomuuri does not add a final `drop log` rule to these sections; it is instead added to the specific zone-zone sections. Example: ``` localhost-any { # Allow ping and SSH from localhost, regardless of destination. ping ssh # The final drop/reject rule is usually added to a specific # localhost-zone section, not to localhost-any. } localhost-public { # Accept all localhost-any rules (ping, SSH), plus HTTPS. https reject log } localhost-internal { # Accept all localhost-any rules (ping, SSH), plus DNS queries. domain reject log } ``` The `szone -public` matcher can be used in a rule to exclude it from `public-localhost`. Example: ``` any-localhost { ssh # Allow SSH from anywhere https szone -public # Allow HTTPS from anywhere except from public } localhost-any { ssh # Allow SSH to anywhere vnc dzone -public # Allow VNC to anywhere except to public } ``` ## Processing Order When traffic goes from `localhost` to `public`, the applicable sections are evaluated in the following order: 1. `any-any` rules 2. `any-public` rules 3. `localhost-any` rules 4. `localhost-public` rules (the final rule is added here even if `localhost-public` is missing from the configuration) foomuuri-0.34/docs/config/section/dnat.md000066400000000000000000000023271526436212200204250ustar00rootroot00000000000000# dnat Destination NAT is used to rewrite traffic using standard [rules](../rule/index.md). Example: ``` dnat { # HTTP traffic to 10.0.0.1 is DNAT'ed to 10.0.0.2 port 8080 daddr 10.0.0.1 http dnat 10.0.0.2:8080 # HTTP traffic to fd00:f00::1 is DNAT'ed to fd00:f00::2 port 8080 daddr fd00:f00::1 http dnat [fd00:f00::2]:8080 # HTTP traffic to 10.0.0.6 is DNAT'ed to 10.0.0.7, port doesn't change daddr 10.0.0.6 http dnat 10.0.0.7 # All SMTP traffic from eth2 is DNAT'ed to 10.0.0.8 or fd00:f00::8, # port doesn't change iifname eth2 smtp dnat 10.0.0.8 fd00:f00::8 # All traffic from eth1 interface is DNAT'ed to 10.0.0.3 iifname eth1 dnat 10.0.0.3 # HTTP traffic to 10.0.0.4 coming from interface eth2 is DNAT'ed to 10.0.0.5 iifname eth2 daddr 10.0.0.4 http dnat 10.0.0.5 # HTTP traffic to localhost coming from interface eth3 is DNAT'ed to 10.0.0.6 iifname eth3 oifname lo http dnat 10.0.0.6 } ``` Remember to accept DNAT'ed traffic in the relevant zone-zone section. This can be done with a specific rule or with the `ct_status` matcher. Example: ``` public-internal { # Specific rule to accept HTTPS https daddr 10.0.0.9 # Generic rule to accept all DNAT'ed traffic ct_status dnat } ``` foomuuri-0.34/docs/config/section/foomuuri.md000066400000000000000000000126711526436212200213470ustar00rootroot00000000000000# foomuuri This section can usually be omitted, since the default values should be fine. This section defines common options for Foomuuri. If you do need to change something, it is usually better to override a single value rather than the whole section. Example: ``` foomuuri { # Change rpfilter's value, keep everything else at the default rpfilter no } ``` ## log_rate `log_rate` (default: `"1/second burst 3"`) is the default logging rate per source IP address. By default, the first three entries per source IP address are logged, followed by one additional entry per second. The rate [specification](../rule/ratelimit.md#global_rate) follows the same format as in a rate limit rule. ## log_input, log_output, log_forward, log_rpfilter, log_invalid, log_smurfs These options define the default logging behavior for specific events: * `log_input` (default: `yes`) * `log_output` (default: `yes`) * `log_forward` (default: `yes`) * `log_rpfilter` (default: `yes`) * `log_invalid` (default: `no`) * `log_smurfs` (default: `no`) The value for each can be: * `yes`, to log it at `log_rate` * `no`, to disable logging for it * `"3/second burst 10"`, to log it at a specific rate ## log_prefix `log_prefix` (default: `"$(szone)-$(dzone) $(statement)"`) defines the default log prefix for [logging](../rule/logging.md#log). Some built-in chains, such as `rpfilter` and `invalid`, always use a static log prefix. ## log_level `log_level` (default: `"level info flags skuid"`) is the syslog level for logging. For possible values, see the rule-specific [version](../rule/logging.md#log_level). ## localhost_zone `localhost_zone` (default: `localhost`) is the name of the zone used for the computer running Foomuuri, similar to "localhost" in hostnames. See the [zone](zone.md) and [zone-zone](zonezone.md) sections for more information. ## dbus_zone `dbus_zone` (default: `public`) is the name of your outgoing internet zone. It is used for D-Bus support. ## rpfilter `rpfilter` (default: `yes`) enables or disables reverse path filtering. The value can be: * `yes`, to enable it on all interfaces * `no`, to disable it * `eth0 eth1 eth2`, to enable it on the specified interfaces * `-eth1 -eth2`, to enable it on all interfaces except `eth1` and `eth2` ## flowtable `flowtable` (default: `no`) enables the Netfilter flowtable infrastructure for the specified interfaces, for example `flowtable eth0 eth1`. It improves forwarding performance on high-speed interfaces. The value `yes`, or negative notation, is not supported here, and at least two interfaces must be specified. The optional `hw_offload=yes` keyword enables hardware offloading (make sure your interface supports `hw-tc-offload`). The optional `counter=yes` keyword enables conntrack accounting support in the flowtable fast path. Note that this is not enabled by default in the Linux kernel; use `sysctl -w net.netfilter.nf_conntrack_acct=1` to enable it. ## counter `counter` (default: `no`) adds an anonymous byte and packet [counter](../rule/logging.md#counter) to all rules. The value can be: * `yes`, to add it to all rules * `no`, to not add it * `localhost-public public-localhost`, to add it only to rules in the `localhost-public` and `public-localhost` sections * `public-any`, to add it to all `public-*` sections * `any-public`, to add it to all `*-public` sections ## permanent_counter If `permanent_counter` (default: `no`) is enabled (value: `yes`), named counter values are preserved across `foomuuri reload`. After a reboot, they are restored to the values saved during the previous `foomuuri start` or `foomuuri reload`. ## set_size `set_size` (default: `65535`) is the size of the [rate limit](../rule/ratelimit.md) set, the [log rate limit](../rule/logging.md) set, and the [dynamic iplist](../rule/matcher.md#iplist_update) set - that is, the maximum number of entries in the kernel set. The default value of 65535 is fine for a normal host; for a corporate firewall, a larger value is recommended, for example 262143 (2^18 - 1). Setting this value too high does no harm. If a set is full, new entries cannot be added; this doesn't matter for log rate limits, but for rate limits and dynamic iplists it means the statement (usually `accept` for a rate limit, `drop` for an iplist) is skipped. See `foomuuri ruleset list` for the contents of your currently active sets. ## recursion_limit `recursion_limit` (default: `10000`) is the internal limit used to avoid macro and template expansion loops. Increase this value if you get a false "Possible macro or template loop" error. ## priority_offset `priority_offset` (default: `5`) is the chain priority offset for rules generated by Foomuuri. Tune this value if you have multiple software adding chains and want to control their processing order. For example, iptables uses offset 0 and firewalld uses 10; the lowest priority is processed first. So, to process Foomuuri's rules first, use `-5`; to process them last, use `20`. ## dbus_firewalld `dbus_firewalld` (default: `no`) enables or disables firewalld D-Bus emulation inside Foomuuri. NetworkManager can instruct firewalld to attach and detach interfaces to zones via a D-Bus call. This option enables Foomuuri to listen for the same firewalld D-Bus calls and act on them. ## nft_bin `nft_bin` (default: `nft`) is the name of the `nft` binary. A full path can be specified if required. ## try_reload_timeout `try_reload_timeout` (default: `15`) is the timeout, in seconds, for the `foomuuri try-reload` command. foomuuri-0.34/docs/config/section/hook.md000066400000000000000000000005331526436212200204340ustar00rootroot00000000000000# hook Foomuuri can run an external command before and after starting and stopping. This section configures those commands. Example: ``` hook { pre_start command-to-run with arguments before loading ruleset post_start echo firewall started pre_stop /etc/foomuuri/pre_stop.sh post_stop /etc/foomuuri/post_stop.sh with arguments } ``` foomuuri-0.34/docs/config/section/index.md000066400000000000000000000002021526436212200205740ustar00rootroot00000000000000# Sections Every Foomuuri configuration must have one [zone](zone.md) section and one or more [zone-zone](zonezone.md) sections. foomuuri-0.34/docs/config/section/invalid.md000066400000000000000000000005031526436212200211170ustar00rootroot00000000000000# invalid, rpfilter, smurfs Packets entering the `invalid`, `rpfilter`, or `smurfs` chains are dropped. These sections can be used to add more rules to them. For example, load-balanced IPVS traffic might enter the `invalid` chain and must be explicitly accepted: ``` invalid { # Accept HTTPS IPVS traffic https } ``` foomuuri-0.34/docs/config/section/iplist.md000066400000000000000000000174271526436212200210120ustar00rootroot00000000000000# iplist Instead of using static IP addresses, Foomuuri can perform periodic DNS hostname lookups and download external IP address lists. These addresses are stored in iplists and cached across reboots and single lookup failures. The first word on each line is the iplist name, which must begin with an `@` character. The words that follow can be any combination of: * IPv4 or IPv6 address, with or without a netmask * DNS hostname * Filename containing IP addresses, with or without a netmask. The filename must be specified as a full path (starting with `/`) or as a relative path prefixed with `./`. * URL for a file containing IP addresses, with or without a netmask Example: ``` iplist { # Resolve known good hostnames @goodhost foobar.fi mydomain.com # Download Finnish IPv4 and IPv6 addresses from https://github.com/ipverse/geo-ip-blocks @fi https://raw.githubusercontent.com/ipverse/geo-ip-blocks/master/country/fi/fi-ipv4.txt @fi + https://raw.githubusercontent.com/ipverse/geo-ip-blocks/master/country/fi/fi-ipv6.txt # Download Finnish Elisa operator IP addresses from https://github.com/ipverse/as-ip-blocks @elisa https://raw.githubusercontent.com/ipverse/as-ip-blocks/master/as/719/ipv4-aggregated.txt @elisa + https://raw.githubusercontent.com/ipverse/as-ip-blocks/master/as/719/ipv6-aggregated.txt # Read a blacklist from text files @blacklist /etc/foomuuri/blacklist*.txt # Read content from a file and add some extra IPs to it @whitelist /etc/foomuuri/whitelist*.txt 10.0.0.0/8 192.0.2.32 # Manipulate this list with the "foomuuri iplist add mylist 10.0.0.1" command. # See the command line help for the "foomuuri iplist add/del/flush" commands. @mylist } public-localhost { # Allow SSH from known good hosts ssh saddr @goodhost # Don't allow blacklisted addresses to reach IMAP imap saddr @blacklist drop log "public-localhost DROP-blacklist" # Allow mylist entries to reach IMAP without rate limiting imap saddr @mylist # Allow Finnish users to reach IMAP at a fast rate of 1 per second imap saddr @fi saddr_rate "1/second burst 10" # Allow everybody to reach IMAP at a slow rate of 1 per minute. # This includes Finnish users who exceed the rate limit above. imap saddr_rate "1/minute burst 1" # ...rest of the rules... } ``` Another example, showing how to create a macro that allows access to Valve Steam: ``` iplist { # Create an iplist from Valve's IP addresses @valve_as https://raw.githubusercontent.com/ipverse/as-ip-blocks/master/as/32590/ipv4-aggregated.txt @valve_as + https://raw.githubusercontent.com/ipverse/as-ip-blocks/master/as/32590/ipv6-aggregated.txt } macro { # Create a macro to allow outgoing traffic to the Valve iplist valve-steam udp 3478 4379-4380 27000-27100 daddr @valve_as; valve-steam + tcp 27015-27050 daddr @valve_as } localhost-public { # Allow outgoing traffic to Valve Steam valve-steam } ``` ## Iplist Filters Downloaded content can be filtered by appending `|filter` after a filename or URL. Multiple filters can be chained. * `|shell:/path/to/command` - pipe the content through an external command * `|json:filter` - parse the content as JSON, using the external `jq` command * `|html:XPath` - parse the content as HTML, using an XPath filter * `|xml:XPath` - parse the content as XML, using an XPath filter Example: ``` iplist { # Download the GitHub IP address list and parse it as JSON, returning # the "actions" list. @github https://api.github.com/meta|json:.actions[] # Download a network scanner's IP address list and parse it from an # HTML page. @netscanner https://internet-measurement.com/|html://div/pre/text() # Read content from file and filter it with external command. # Command must read input from stdin and output result to stdout. # Parameters can be passed to command by "-quoting whole string. @ipfile /etc/foomuuri/ipfile.txt|shell:/etc/foomuuri/my-filter @ipfile2 "/etc/foomuuri/ipfile2.txt|shell:/etc/foomuuri/my-filter2 parameters" } ``` The `json:` filter requires `jq`. The `html:` and `xml:` filters require `python3-lxml`. The `shell:` filter executes an external command; use it only with trusted configuration and input. ## Iplist Settings ### Timeouts & Refresh Options Hostnames are refreshed every 15 minutes and time out after 24 hours. URLs are refreshed once a day and time out after 10 days. These values can be changed globally or per iplist. ``` iplist { # Define global timeouts dns_refresh=15m dns_timeout=24h url_refresh=1d url_timeout=10d # Define a per-iplist timeout @fasturl https://some/url url_refresh=1h30m url_timeout=2d } ``` Refresh operations are scheduled by `foomuuri-iplist.timer`. The timer interval defaults to 15 minutes, so configured refresh times are evaluated at timer runs rather than necessarily at the exact requested second. This interval can be changed with `systemctl edit foomuuri-iplist.timer`. Timeout values determine how long cached addresses remain usable. They can be specified as: * `4w` - weeks * `2d` - days * `3h` - hours * `15m` - minutes * `900s` - seconds * `2d3h15m` or `1w90m` - a combination of the above The optional `element_timeout=time` option sets the default element expiry timeout. This is needed for automatic IP address banning and port knocking. ### Error Handling & Download Limits With the optional `missing_ok=yes` option (default: `no`), a warning is printed instead of an error when: * DNS resolution or the URL/file download fails, or * the resolution/download succeeds but the content is empty. The maximum size of URL content can be set with `url_max_size=bytes`. The default is 33554432 (32 MiB); content exceeding this size is ignored. ### Content Management & Merging The optional `overwrite=yes|no` option controls how resolved IP addresses or URL content are applied to the iplist. With `yes`, old content is removed and replaced with the new addresses. With `no`, old content is kept until it expires, and new addresses are either added or have their expiry timeout updated. The default `overwrite` value depends on the content type: * URL/file content defaults to `overwrite=yes` * DNS resolution defaults to `overwrite=no` The optional `merge=no` option (default: `yes`) disables IP address auto-merging. It is generally recommended to leave auto-merging enabled, except when using an external `fail2ban` integration, where it should be disabled. Shortcut `-merge` is also supported. ### Dynamic Sets & Startup Behavior The optional `dynamic=yes` option (default: `no`) enables the dynamic flag on the generated ruleset. This is required when updating iplist content on the packet path using the [`iplist_update`](../rule/matcher.md#iplist_update) matcher, for example, in automatic IP address [banning](../../example/advanced.md#automatic-ip-address-banning). This option should not be used for normal usage. Foomuuri will not add IP addresses at startup to lists marked with the optional `start=no` option (default: `yes`); entries are added later by the `foomuuri-iplist.timer` service instead. This is useful for "unsafe" external iplists, to ensure that Foomuuri startup does not fail or block access during the first few minutes after a reboot. ## Iplist Command Line Iplists can be manipulated from the command line, for example, by an external `fail2ban` program. The command `foomuuri iplist add IPLIST ipaddress` adds an IP address to `IPLIST`. Other available commands are `foomuuri iplist list`, `foomuuri iplist del`, and `foomuuri iplist flush`. See the command line help for full usage details. Example: ``` iplist { @banned dynamic=yes element_timeout=5m # automatic IP address banning @blacklist merge=no # fail2ban } # Manipulate with the command: # # foomuuri iplist add @blacklist 10.0.0.1 # foomuuri iplist del @blacklist 10.0.0.1 ``` foomuuri-0.34/docs/config/section/macro.md000066400000000000000000000037341526436212200206030ustar00rootroot00000000000000# macro Rather than writing the rule `tcp 443`, it is easier and more readable to write `https`. These alias names are called macros. A macro can be used in any part of a rule, defining it fully or partially. ``` macro { # Define a service as a macro smtp tcp 25 https tcp 443; udp 443 googlemeet udp 3478 19302-19309; https # Define a rate limit as a macro ssh_rate saddr_rate "5/minute burst 5" # A long macro can be split across multiple lines with "+" (append to the # previous line) or "\" (continue on the next line). # Warning: using "+" or "\" does not add ";". You must add it yourself # when needed. good_hosts 10.0.0.1 fd00:f00::1 good_hosts + 10.0.0.2 fd00:f00::2 another 10.0.0.3 \ 10.0.0.4 semicolon http semicolon + ; https } ``` You can use the macros above in other sections: ``` localhost-public { https daddr good_hosts # Allow HTTPS to specific IP addresses tcp 23 daddr good_hosts # Allow TCP 23 to specific IP addresses https reject # Reject all other HTTPS traffic googlemeet # Allow Google Meet to everywhere } public-localhost { ssh ssh_rate # Allow incoming SSH with a rate limit } ``` A macro can include other macros, as `googlemeet` does in the example above. Using `;` in a macro splits it into multiple rule lines. You must use it when a macro combines two different rules, such as `tcp 443` and `udp 443` in the `https` macro, or `udp XXX` and `https` in the `googlemeet` macro. Do not use it when defining a list of items (IP addresses, for example) for a single rule, as in `good_hosts`. All [known macros](https://github.com/FoobarOy/foomuuri/blob/main/etc/default.services.conf) can be listed with `foomuuri macro list`. Macro expansion can be skipped by writing a word in quotes, for example, `"ssh"` is kept as `ssh` rather than being expanded to `tcp 22`. For safety, macro expansion is not performed in the `zone` or `foomuuri` sections. foomuuri-0.34/docs/config/section/snat.md000066400000000000000000000013141526436212200204370ustar00rootroot00000000000000# snat Source NAT is used to rewrite traffic using standard [rules](../rule/index.md). Example: ``` snat { # Masquerade all traffic from 10.0.0.0/8 going to eth0 interface. # The new outgoing IP is eth0's IP address. saddr 10.0.0.0/8 oifname eth0 masquerade # Use outgoing IP 192.0.2.32 for all non-IPsec traffic coming from # 10.0.0.0/8 and going to eth1 interface. saddr 10.0.0.0/8 oifname eth1 -dipsec snat 192.0.2.32 # IPv6-to-IPv6 Network Prefix Translation (NPTv6) saddr fd00:f00:4444::/64 oifname eth2 snat_prefix 2a03:1111:222:8888::/64 } ``` Remember to accept SNAT'ed traffic in the relevant zone-zone section, for example in `internal-public`. See [dnat](dnat.md) for more examples. foomuuri-0.34/docs/config/section/special.md000066400000000000000000000031321526436212200211120ustar00rootroot00000000000000# prerouting, postrouting, forward, input, output These sections are used to specify packet-mangling rules. Mangle rules are processed before the normal zone-zone filtering rules. * `prerouting` applies to all incoming packets * `postrouting` applies to all outgoing packets * `forward` applies to all forwarded packets (for example, `internal-public`) * `input` applies to all packets destined for `localhost` (`public-localhost`) * `output` applies to all locally generated packets (`localhost-public`) These sections are normally used to [set](../rule/matcher.md#mark_set) a packet's mark value or for [MSS clamping](../rule/misc.md#mss). Example: ``` prerouting { # Do nothing if the mark is already set mark_match -0x0000/0xff00 accept # Set the default mark 0x100 on the packet mark_set 0x100/0xff00 # Change the mark to 0x200 if the packet is coming from eth2 iifname eth2 mark_set 0x200/0xff00 # Use mark 0x300 for SSH traffic from eth2 iifname eth2 ssh mark_set 0x300/0xff00 } ``` The chain type and hook priority can also be specified. Example: ``` prerouting filter raw { ... } postrouting nat srcnat + 20 { ... } ``` These sections are special: a `drop` or `reject` rule drops or rejects the packet immediately. The default rule is `accept`, which allows processing to continue to other special sections, the zone-zone section, and then back to other special sections. All of them must accept the packet for it to pass. See the [Netfilter packet flow diagram](https://upload.wikimedia.org/wikipedia/commons/3/37/Netfilter-packet-flow.svg) for the detailed section processing order. foomuuri-0.34/docs/config/section/targetgroup.md000066400000000000000000000002541526436212200220370ustar00rootroot00000000000000# target, group Foomuuri includes a simple network [connectivity monitor](../../tool/monitor.md). These sections are used to [configure](../../tool/monitor.md#target) it. foomuuri-0.34/docs/config/section/template.md000066400000000000000000000014211526436212200213040ustar00rootroot00000000000000# template A template is very similar to a [macro](macro.md) - it's just another way to define a list of rules. Typically, a macro refers to a single service (such as `ssh` or `https`), while a template refers to a list of different services. Example: ``` template outgoing_services { # Define a template called "outgoing_services" dhcp-server domain https ntp ping ssh } localhost-public { # Include the template's content here template outgoing_services # Continue with other rules http reject log } dmz-public { # Use the same template for traffic coming from dmz zone template outgoing_services # Continue with other rules reject log } ``` See the [host firewall example](../../example/host-firewall.md#multi-zone) for a real-world example. foomuuri-0.34/docs/config/section/zone.md000066400000000000000000000035721526436212200204550ustar00rootroot00000000000000# zone This section is required in every configuration. It lists all known zones. ``` zone { localhost public } ``` The example above defines two zones, `localhost` and `public`. Every configuration must include the `localhost` zone, which represents the computer running Foomuuri, similar to "localhost" in hostnames. See [zone names](../basic.md#zone-names) for recommended zone naming. The example above assumes you are using firewalld D-Bus (`dbus_firewalld` config option) emulation, where interfaces are attached to and detached from zones by NetworkManager. This is the recommended approach for laptops and personal servers, and this config option is turned on by default when the `foomuuri-firewalld` package is installed. You can also define a default interface-to-zone mapping by listing interface name(s) after the zone name. This is useful for corporate servers with a static network configuration, and can be used with or without firewalld D-Bus emulation. This mapping is only a default, not static - interfaces can still be moved to other zones via D-Bus calls. ``` zone { localhost # localhost must be left empty public eth0 # eth0 is attached to public dmz eth1 eth2 # eth1 and eth2 are in dmz } ``` It is also possible to use wildcard interface names. If you define `wg*`, make sure NetworkManager doesn't also try to assign `wg0` to a zone directly - doing so would create an "interval overlap" error, since `wg*` and `wg0` would conflict. ``` zone { localhost public eth0 wireguard wg* # Matches wg0, wg1, wgfoo, and so on } ``` There is also a catch-all interface, `*`, which matches all unassigned interfaces. Interfaces already assigned to a zone, whether in the configuration or by NetworkManager, keep that zone. ``` zone { localhost public * # Everything other than eth0 is assigned to public internal eth0 } ``` foomuuri-0.34/docs/config/section/zonemap.md000066400000000000000000000027451526436212200211540ustar00rootroot00000000000000# zonemap By default, Foomuuri maps incoming and outgoing traffic to zones based on the source and destination network interface. These interfaces are assigned to zones dynamically by NetworkManager, or configured in the [zone](zone.md) section. The zonemap section can be used to map traffic to a different zone using standard [rules](../rule/index.md). Example: ``` zonemap { # Map outgoing IPsec traffic destined for zone "public" to use zone # "vpn" instead. dipsec dzone public new_dzone vpn # Same for incoming traffic. sipsec szone public new_szone vpn } localhost-public { # Rules for non-IPsec traffic ipsec # You must allow IPsec traffic here and in public-localhost ...accept some traffic... reject log } localhost-vpn { # Rules for IPsec traffic ...accept some traffic... reject log } ``` The example above, splitting traffic into IPsec and non-IPsec zones, is the most common use case. You can use any matcher - for example, `daddr` or `saddr` to map specific IP addresses to their own zones, or `uid` or `gid` to map outgoing traffic from a specific local user to its own zone: ``` zonemap { # Map IP address 10.2.3.0/24 from internal to dmz saddr 10.2.3.0/24 szone internal new_szone dmz daddr 10.2.3.0/24 dzone internal new_dzone dmz # Map outgoing traffic from user myservice to myzone uid myservice szone localhost new_szone myzone # Map all outgoing IPsec traffic to xxx-vpn, regardless of the original # dzone dipsec new_dzone vpn } ``` foomuuri-0.34/docs/config/section/zonezone.md000066400000000000000000000044661526436212200213540ustar00rootroot00000000000000# zone-zone A FromZone-ToZone section defines [rules](../rule/index.md) for traffic coming from FromZone and going to ToZone. Typically, you first accept certain traffic and then [reject or drop](../rule/statement.md#accept-drop-reject) everything else as a final rule. Rules within a zone-zone section are (mostly, see [below](zonezone.md#processing-order)) processed in the order listed. Example: ``` public-localhost { # Allow some incoming traffic dhcp-client ping ssh # Drop everything else drop log } localhost-public { # Allow some outgoing traffic dhcp-server domain https ping ssh # Reject everything else reject log } ``` If not specified, Foomuuri automatically appends a final `drop log` rule (or `reject log` for `localhost-something` sections) to each zone-zone section. It is always better to add this final rule explicitly in your configuration. This final rule is also added to any unconfigured zone-zone pairs. If you have many zones, you will end up with many zone-zone pairs. See [configuration files](../basic.md#configuration-files) for recommendations on splitting them across multiple configuration files. ## localhost-localhost The `localhost-localhost` zone-zone section (i.e., loopback traffic, `127.0.0.1` and `::1`) is a special case: its final rule is `accept`. Usually there is no need to add a `localhost-localhost` section explicitly. A typical use case for `localhost-localhost` is to deny specific traffic and then accept everything else: ``` localhost-localhost { # Don't allow user "untrusted" to connect to local services uid untrusted drop log # Don't allow local HTTP traffic http reject log # Accept everything else accept } ``` Note that loopback traffic from your public IP to your public IP belongs to `localhost-localhost`, not `public-public`. ## Processing Order Rules within a zone-zone section are automatically sorted and processed in the following block order: 1. [zonemap](zonemap.md) rules 2. [MSS clamping](../rule/misc.md#mss) options 3. Rules with [`-conntrack`](../rule/misc.md#conntrack-conntrack) flag 4. [ICMP](../rule/matcher.md#tcp-udp-icmp-icmpv6) rules 5. Previously accepted established and related traffic, accepted by conntrack 6. Incoming [multicast and broadcast](../rule/matcher.md#multicast-broadcast) rules 7. Everything else foomuuri-0.34/docs/example/000077500000000000000000000000001526436212200156735ustar00rootroot00000000000000foomuuri-0.34/docs/example/advanced.md000066400000000000000000000053301526436212200177630ustar00rootroot00000000000000# Advanced Filtering ## Port Knocking Port knocking is a technique in which attempting to connect to port A enables access to port B from that same source IP address. It is typically used to hide the SSH service. Example configuration: ``` zone { localhost public } iplist { # List of IP addresses that have performed the initial knock. # Entries are valid for 30 seconds. @knock dynamic=yes element_timeout=30s } public-localhost { # Allow SSH if the source IP address is in the @knock iplist. ssh saddr @knock # Add the source IP address to the @knock iplist on a UDP packet to port 5042. udp 5042 iplist_update saddr @knock drop log + ":knock" # Remove the IP address from the @knock iplist on any packet to any other # port. This prevents SSH access if a port scan is received. saddr @knock iplist_delete saddr @knock continue log + ":unknock" # ...other normal rules... drop log } ``` ## Automatic IP Address Banning Foomuuri supports automatic IP address banning without any external programs, so `fail2ban` is usually unnecessary. Banning happens entirely on the packet path, natively at the nftables level. Key differences compared to the `fail2ban` program: * Banning is based on connection count/rate alone, not on protocol success/failure. This means that too many successful SSH connections could still trigger a ban, so it is important to use a high rate limit, a list of known good hosts, or both. * A Foomuuri restart or a reboot clears the ban list. * No external programs or complex log file parsing are required. Example configuration: ``` zone { localhost public } iplist { # List of known good hosts; never ban these. @good 192.168.0.0/24 foobar.fi # List of banned IP addresses. They are banned for 5 minutes. @banned dynamic=yes element_timeout=5m } public-localhost { # Drop all new traffic if the source IP address is in the @banned iplist. # Update/reset the ban expiry timeout. Add this as the first rule. saddr @banned iplist_update saddr @banned drop log + ":banned" # Allow SSH from known good hosts, and from others up to a rate limit. # If a source exceeds 5 connections per minute, add it to the @banned iplist. ssh saddr @good ssh saddr_rate "5/minute burst 5" ssh iplist_update saddr @banned drop log + ":ban-ssh" # ...other normal rules, with or without similar rate-limit banning... # Instead of the normal final "drop log" rule: # - Make sure @good addresses are never banned. # - Allow up to 2/minute dropped connections without banning. # - If the rate is exceeded, assume it's a port scan and ban the source. saddr @good drop log saddr_rate "2/minute burst 10" drop log iplist_update saddr @banned drop log + ":ban-portscan" } ``` foomuuri-0.34/docs/example/host-firewall.md000066400000000000000000000073051526436212200210020ustar00rootroot00000000000000# Host Firewall The following examples apply to: * Your personal laptop * Your personal workstation * A corporate server behind a [router firewall](router-firewall.md) * A corporate server in the cloud * Any other system with only one network connection ## Incoming Only This is the simplest possible firewall. All outgoing traffic is accepted, and a small set of listed incoming services is accepted. ``` mermaid flowchart LR public@{shape: cloud} --> localhost@{shape: stadium} ``` ``` zone { localhost public * # All network interfaces belong to zone "public" } public-localhost { # Allow specified incoming traffic dhcp-client dhcpv6-client ping ssh drop log } localhost-public { # Allow all outgoing traffic accept } ``` The example above is a complete `/etc/foomuuri/foomuuri.conf` configuration file - nothing else needs to be added. It allows incoming (`public-localhost`) traffic for: * DHCP reply packets, to obtain a lease from an external DHCP server (IPv4 and IPv6) * Ping packets (no ping-flood protection) * SSH * Everything else is dropped and logged All outgoing (`localhost-public`) traffic is accepted. This is usually safe, but a more specific, bidirectional firewall is safer. ## Bidirectional This example accepts a specific list of incoming services and a specific list of outgoing services. ``` mermaid flowchart LR public@{shape: cloud} <--> localhost@{shape: stadium} ``` ``` zone { localhost public * } public-localhost { dhcp-client dhcpv6-client ping saddr_rate "5/second burst 20" ssh saddr_rate "5/minute burst 5" drop log } localhost-public { dhcp-server dhcpv6-server domain http https imap ntp ping smtp ssh reject log } ``` This complete `/etc/foomuuri/foomuuri.conf` configuration file allows the following incoming traffic: * DHCP reply packets * Ping packets, except ping floods * SSH, up to 5 connections per minute per source IP * Everything else is dropped and logged The following outgoing traffic is allowed: * DHCP request packets, to obtain a lease * DNS queries * HTTP and HTTPS * IMAP * NTP * Ping packets * SMTP * SSH * Everything else is rejected and logged ## Multi-zone This example is similar to the bidirectional example above, except that there are two outgoing zones: * `public` is the default, untrusted connection. No network interface is listed for it; use NetworkManager to assign a network interface to the `public` zone when connecting to an untrusted Wi-Fi network, such as one in a cafe. * `home` is the trusted connection. Likewise, use NetworkManager to select the `home` zone when you are in a safe location, such as your home or work office. This example also demonstrates how to use a `template` to avoid listing the same basic services in both `localhost-public` and `localhost-home`. ``` mermaid flowchart LR subgraph WAN direction TB public@{shape: cloud} home@{shape: cloud} end public <--> localhost@{shape: stadium} home <--> localhost ``` ``` zone { localhost public home } public-localhost { # Incoming traffic in a cafe dhcp-client dhcpv6-client ping saddr_rate "5/second burst 20" ssh saddr_rate "5/minute burst 5" drop log } home-localhost { # Incoming traffic in a safe location dhcp-client dhcpv6-client lsdp mdns ping ssdp ssh drop log } template outgoing_services { # Common outgoing traffic dhcp-server dhcpv6-server domain http https imap ntp ping smtp ssh } localhost-public { # Outgoing traffic in a cafe template outgoing_services reject log } localhost-home { # Outgoing traffic in a safe location template outgoing_services googlemeet ipp mdns ssdp reject log } ``` foomuuri-0.34/docs/example/multiple-isp.md000066400000000000000000000275171526436212200206550ustar00rootroot00000000000000# Multiple ISP Foomuuri supports multiple ISPs (also known as multi-ISP, multi-WAN, or multiple simultaneous uplink connections), in either active-active (load balancing) or active-passive (failover) configurations. ## Configuration with Static ISP Routes This example requires static ISP routes and works with both NetworkManager and systemd-networkd. Both active-active and active-passive configurations are supported; the two are very similar, differing by only one line. This example configuration assumes: * ISP #1 is on network interface `enp1s0`, with local IP 172.23.70.36/24 and gateway 172.23.70.254. Its traffic is marked with value 0x100 and uses route table 1001. * ISP #2 is on network interface `enp2s0`, with local IP 172.23.12.31/24 and gateway 172.23.12.254. Its traffic is marked with value 0x200 and uses route table 1002. * The `internal` zone's network interface is `enp8s0`, with network 10.0.0.0/8. Outgoing traffic to the `public` zone is masqueraded. ### Firewall Configuration (foomuuri.conf) Example `foomuuri.conf` file: ``` # Basic configuration: zone { # Define zones with interfaces localhost public enp1s0 enp2s0 internal enp8s0 } foomuuri { # Reverse path filtering must be disabled for public interfaces rpfilter -enp1s0 -enp2s0 } snat { # Masquerade outgoing traffic from internal to public. Both ISPs must be # masqueraded separately. saddr 10.0.0.0/8 oifname enp1s0 masquerade saddr 10.0.0.0/8 oifname enp2s0 masquerade } # The multi-ISP logic is here, using marks to select which ISP to use. # Rule order matters: specific rules should come first, generic ones last. prerouting { # Accept if a mark is already set (non-zero). The existing mark is used. mark_match -0x0000/0xff00 # == Incoming traffic == # Mark traffic from enp1s0 as 0x100 (ISP1) and from enp2s0 as 0x200 (ISP2). # This is needed to correctly route reply packets. iifname enp1s0 mark_set 0x100/0xff00 iifname enp2s0 mark_set 0x200/0xff00 # == Outgoing traffic == # Specific rules should be added first. For example, uncomment the next # line to route all SSH traffic from internal to public via ISP2. #iifname enp8s0 ssh mark_set 0x200/0xff00 # Similarly, some source IPs can always be routed via ISP1. #saddr 10.0.1.0/24 mark_set 0x100/0xff00 # For an active-active configuration, use the following line. It uses a # random number generator to mark traffic with 0x100 or 0x200, routing # 60% (0-5) of outgoing traffic to ISP1 and 40% (6-9) to ISP2. nft "meta mark set numgen random mod 10 map { 0-5: 0x100, 6-9: 0x200 } ct mark set meta mark accept" # For an active-passive configuration, uncomment the next line and # comment out the nft line above. It simply assigns mark 0x100 (ISP1) to # all traffic and uses ISP2 only as a fallback. #mark_set 0x100/0xff00 } # foomuuri-monitor config: target isp1 { # Monitor ISP1 connectivity by pinging 8.8.4.4. Ideally this would be # ISP1's router IP address instead. command fping --iface enp1s0 8.8.4.4 command_up /etc/foomuuri/multi-isp up 1 command_down /etc/foomuuri/multi-isp down 1 } target isp2 { # Monitor ISP2 connectivity by pinging its router at 172.25.31.149. command fping --iface enp2s0 172.25.31.149 command_up /etc/foomuuri/multi-isp up 2 command_down /etc/foomuuri/multi-isp down 2 } # Normal zone-zone rules, copied from the router firewall example configuration: public-localhost { ping saddr_rate "5/second burst 20" ssh saddr_rate "5/minute burst 5" drop log } internal-localhost { dhcp-server dhcpv6-server domain domain-s ntp ping ssh reject log } template outgoing_services { # Shared list of services for localhost-public and internal-public. domain domain-s http https ntp ping smtp ssh } localhost-public { template outgoing_services reject log } internal-public { template outgoing_services googlemeet imap reject log } public-internal { drop log } localhost-internal { dhcp-client dhcpv6-client ping ssh reject log } ``` ### Multi-ISP Route Script (/etc/foomuuri/multi-isp) Example `/etc/foomuuri/multi-isp` script. Remember to save it as executable: `chmod 750 /etc/foomuuri/multi-isp`. ``` bash #!/bin/sh # Path to "ip" command IP=/usr/sbin/ip case "${1}" in "start") # Started, run by foomuuri-multi-isp.service echo "Foomuuri multi-ISP start" # Create new route tables for both ISPs having default route ${IP} route add table 1001 default dev enp1s0 via 172.23.70.254 ${IP} route add table 1002 default dev enp2s0 via 172.23.12.254 # Delete default routes from main table ${IP} route del table main default dev enp1s0 via 172.23.70.254 ${IP} route del table main default dev enp2s0 via 172.23.12.254 # Start rules with main-table lookup ${IP} rule add prio 1000 from all table main # Packets with specific source IP must go to correct ISP ${IP} rule add prio 1001 from 172.23.70.36 table 1001 ${IP} rule add prio 1002 from 172.23.12.31 table 1002 # Other ISP-specific rules will be added by monitor's up-event. ;; "stop") # Stopped, run by foomuuri-multi-isp.service echo "Foomuuri multi-ISP stop" # Add default route back to main table ${IP} route add table main default dev enp1s0 via 172.23.70.254 metric 100 ${IP} route add table main default dev enp2s0 via 172.23.12.254 metric 101 # Delete added rules ${IP} rule del prio 1000 ${IP} rule del prio 1001 ${IP} rule del prio 1002 ${IP} rule del prio 1101 2> /dev/null ${IP} rule del prio 1102 2> /dev/null ${IP} rule del prio 1201 2> /dev/null ${IP} rule del prio 1202 2> /dev/null # Delete added route tables ${IP} route flush table 1001 ${IP} route flush table 1002 ;; "up") # ISP is up, add rules to route traffic to it echo "Foomuuri multi-ISP isp${2} up" # Use packet mark to select this ISP ${IP} rule add prio 110${2} from all fwmark 0x${2}00/0xff00 table 100${2} # Fallback to this ISP if other is down ${IP} rule add prio 120${2} from all table 100${2} ;; "down") # ISP is down, delete its rules echo "Foomuuri multi-ISP isp${2} down" ${IP} rule del prio 110${2} ${IP} rule del prio 120${2} ;; *) echo "syntax error" exit 1 esac ``` ### Systemd Service Setup Example `/etc/systemd/system/foomuuri-multi-isp.service` file. Remember to enable it with `systemctl enable foomuuri-multi-isp.service`. ``` systemd [Unit] Description=Multizone bidirectional nftables firewall - Multi-ISP Documentation=https://foomuuri.foobar.fi/latest/ After=network-online.target Requires=network-online.target PartOf=foomuuri.service [Service] Type=oneshot RemainAfterExit=yes ExecStart=/etc/foomuuri/multi-isp start ExecStop=/etc/foomuuri/multi-isp stop [Install] WantedBy=multi-user.target ``` ## Configuration with Routes Dynamically Allocated by ISPs This example configuration assumes: * ISP #1 is on network interface `enp1s0`, with routes and IP addresses allocated via DHCP/IPv6 RA. Its traffic is marked with value 0x100. * ISP #2 is on network interface `enp2s0`, with routes and IP addresses allocated via DHCP/IPv6 RA. Its traffic is marked with value 0x200. * The `internal` zone's network interface is `enp8s0`, with IP 10.0.0.1/24. Outgoing traffic to the `public` zone is masqueraded. * At least the public interfaces and their routing tables are managed by `systemd-networkd`. * This example demonstrates a purely failover setup from ISP #1 to ISP #2. For load balancing, add a second target monitor for `enp2s0` in `foomuuri.conf`, and adjust the `switch.sh` script to identify and replace the fwmark randomizer as documented in the static routes example above. ### Networkd Routing Tables Configuration Set up the routing tables by creating the following two files. `/etc/systemd/networkd.conf.d/table-primary.conf`: ``` [Network] RouteTable=primary:100 ``` `/etc/systemd/networkd.conf.d/table-secondary.conf`: ``` [Network] RouteTable=secondary:200 ``` Configure `enp1s0` as follows, in `/etc/systemd/network/enp1s0.network`: ``` [Match] Name=enp1s0 [Network] DHCP=yes IPv6AcceptRA=yes [DHCPv4] RouteTable=primary [IPv6AcceptRA] RouteTable=primary [RoutingPolicyRule] FirewallMark=0x100/0xff00 Family=both Table=primary Priority=40100 [RoutingPolicyRule] Family=both Table=primary Priority=41000 ``` Configure `enp2s0` as follows, in `/etc/systemd/network/enp2s0.network`: ``` [Match] Name=enp2s0 [Network] DHCP=yes IPv6AcceptRA=yes [DHCPv4] RouteTable=secondary [IPv6AcceptRA] RouteTable=secondary [RoutingPolicyRule] FirewallMark=0x200/0xff00 Family=both Table=secondary Priority=40200 [RoutingPolicyRule] Family=both Table=secondary Priority=42000 ``` ### Verification & Dynamic Firewall Rules After restarting `systemd-networkd`, the `ip route show table main` command should show the following output: `10.0.0.0/24 dev enp8s0 proto kernel scope link src 10.0.0.1` As you can see, no default route is defined in the main routing table, since `systemd-networkd` added the default routes only to the separate primary and secondary tables defined above. You can confirm this by checking the output of `ip route show table 100` and `ip route show table 200`, where you should see the routes added via DHCP (use `ip -6 route show table 100` for IPv6 router advertisements). `ip rule` should output something like this: ``` 0: from all lookup local 32766: from all lookup main 32767: from all lookup default 40100: from all fwmark 0x100/0xff00 lookup 100 proto static 40200: from all fwmark 0x200/0xff00 lookup 200 proto static 41000: from all lookup 100 proto static 42000: from all lookup 200 proto static ``` The catch-all rules `41000` and `42000` are needed so that `localhost` knows where to look up the default routes. Now all that remains is to configure Foomuuri to use the `fwmark` rules. Example `foomuuri.conf` file: ``` zone { localhost internal enp8s0 public enp1s0 enp2s0 } foomuuri { rpfilter -enp1s0 -enp2s0 } snat { oifname enp1s0 masquerade oifname enp2s0 masquerade } prerouting { mark_match -0x0000/0xff00 iifname enp1s0 mark_set 0x100/0xff00 iifname enp2s0 mark_set 0x200/0xff00 } target main { command fping --iface enp1s0 8.8.8.8 command_up /etc/foomuuri/switch.sh up command_down /etc/foomuuri/switch.sh down } zone-zone rules... ``` ### Automatic Switch Script (/etc/foomuuri/switch.sh) If the `fping` monitor detects that the main interface `enp1s0` has no uplink, the following `switch.sh` script (remember to `chmod 750` it) sets the default fwmark to 0x200, routing traffic via the backup interface `enp2s0` by directly manipulating the nft prerouting chain. ``` bash #!/bin/bash case "$1" in up) echo "main is up" FWMARK="0x00000100" FWMASK="0xffff01ff" ;; down) echo "main is down" FWMARK="0x00000200" FWMASK="0xffff02ff" ;; *) echo "syntax error" exit 1 esac HANDLE="$( nft --handle list chain inet foomuuri filter_prerouting_mangle | grep --only-matching --perl-regexp '^\s+meta mark set meta mark.*handle\s\K([0-9]+)$' )" if [[ $HANDLE ]] then echo "catchall fwmark rule with handle $HANDLE found" nft replace rule inet foomuuri filter_prerouting_mangle handle "$HANDLE" \ meta mark set meta mark \& "$FWMASK" \| "$FWMARK" \ ct mark set meta mark accept echo "replaced handle $HANDLE with new fwmark ${FWMARK}/${FWMASK}" else echo "no existing catchall fwmark rule found" nft add rule inet foomuuri filter_prerouting_mangle \ meta mark set meta mark \& "$FWMASK" \| "$FWMARK" \ ct mark set meta mark accept echo "added catchall fwmark ${FWMARK}/${FWMASK}" fi ``` foomuuri-0.34/docs/example/router-firewall.md000066400000000000000000000142011526436212200213360ustar00rootroot00000000000000# Router Firewall ## localhost - public - internal ### Overview & Network Diagram This example is for a small corporate firewall: * A single firewall computer that runs: * DHCP server * DNS resolver * Email and all other services run in the cloud * Multiple laptops and workstations on the internal network * Bidirectional firewalling ``` mermaid flowchart LR public@{shape: cloud} <--> localhost@{shape: stadium} subgraph Local Network localhost <--> internal end ``` ### Firewall Configuration ``` zone { localhost public eth0 internal eth1 } snat { # Masquerade traffic from internal to public. saddr 10.0.0.0/8 oifname eth0 masquerade } public-localhost { # Allow only ping and SSH to localhost. ping saddr_rate "5/second burst 20" ssh saddr_rate "5/minute burst 5" # If localhost obtains its public IP via DHCP, add "dhcp-client" here. drop log } internal-localhost { # localhost runs the DHCP (IPv4 and IPv6) server and DNS resolver for the # internal network, plus basic SSH and similar rules. dhcp-server dhcpv6-server domain domain-s ntp ping ssh reject log } template outgoing_services { # Shared list of services for localhost-public and internal-public. domain domain-s http https ntp ping smtp ssh } localhost-public { # Basic services from localhost to the internet: DNS queries, HTTPS, SSH, # etc. Also allow SMTP so localhost can send email. template outgoing_services # If localhost obtains its public IP via DHCP, add "dhcp-server" here and # "dhcp-client" to public-localhost. reject log } internal-public { # Laptops and workstations on the internal network can access web and # email services on the internet. This ruleset is similar to # localhost-public in the host firewall example, and is the most # important zone-zone section to configure. template outgoing_services googlemeet imap reject log } public-internal { # No traffic is allowed from the internet to the internal network. drop log } localhost-internal { # DHCP server reply packets dhcp-client dhcpv6-client # Very limited access from localhost to the internal network. ping ssh reject log } ``` ### Enable Packet Forwarding For `public-internal` and other forwarding to work, you must enable IP packet forwarding in the Linux kernel. Do this by creating `/etc/sysctl.d/50-ip.forwarding.conf` with the following lines: ``` # Enable IP packet forwarding net.ipv4.conf.all.forwarding = 1 net.ipv6.conf.all.forwarding = 1 ``` ## localhost - public - dmz - internal ### Overview & Network Diagram This example is for a larger corporate firewall: * A single firewall computer that runs: * DHCP server * DNS resolver * A single dmz computer that runs: * Web server * Email server * Multiple laptops and workstations on the internal network * Bidirectional firewalling See also the note about enabling IP packet forwarding above. ``` mermaid flowchart LR public@{shape: cloud} <--> localhost@{shape: stadium} subgraph Local Network localhost <--> internal localhost <--> dmz end ``` ### Firewall Configuration ``` zone { localhost public eth0 internal eth1 dmz eth2 } snat { # Masquerade traffic from internal to public. saddr 10.0.0.0/8 oifname eth0 masquerade } dnat { # DNAT incoming SMTP and HTTPS traffic from public to the dmz server. This # section is only needed if the dmz server doesn't have a public IP address. iifname eth0 smtp http https dnat 10.1.0.2 # public -> dmz iifname eth1 daddr 192.0.2.32 smtp http https dnat 10.1.0.2 # internal -> dmz } macro { # Define rate limits as macros, since the same limits are used in # public-localhost and public-dmz. http_rate saddr_rate "100/second burst 400" saddr_rate_name http_limit mail_rate saddr_rate "1/second burst 10" ping_rate saddr_rate "5/second burst 20" ssh_rate saddr_rate "5/minute burst 5" } template localhost_services { # Shared list of services running on localhost. It runs the DHCP server # and DNS resolver for the internal and dmz networks, plus basic SSH and # similar rules. dhcp-server dhcpv6-server domain domain-s ntp ping ssh } public-localhost { # Allow only ping and SSH from the internet to localhost. ping ping_rate ssh ssh_rate drop log } internal-localhost { # Servers on the internal network can access localhost's basic services. template localhost_services reject log } dmz-localhost { # Servers on dmz can access localhost's basic services, similar to # internal-localhost. template localhost_services reject log } template public_services { # Shared list of services that can be reached on the internet. domain domain-s http https ntp ping ssh } localhost-public { # Basic services from localhost to the internet: DNS queries, HTTPS, # SSH, etc. template public_services reject log } internal-public { # Basic services from internal to the internet: DNS queries, HTTPS, # SSH, etc. template public_services googlemeet reject log } dmz-public { # Basic services from dmz to the internet, plus SMTP for email transfer. template public_services smtp reject log } public-internal { # No traffic is allowed from the internet to the internal network. drop log } localhost-internal { # DHCP server reply packets dhcp-client dhcpv6-client # Very limited access from localhost to the internal network. ping ssh reject log } dmz-internal { # Servers on dmz don't need any access to the internal network. reject log } template dmz_services { # Shared list of services that run on dmz. http https ping smtp ssh } localhost-dmz { # DHCP server reply packets dhcp-client dhcpv6-client # localhost can access dmz server services. template dmz_services reject log } public-dmz { # Allow traffic from the internet to the dmz server, with rate limits. http http_rate https http_rate smtp mail_rate ping ping_rate ssh ssh_rate drop log } internal-dmz { # Laptops and workstations on the internal network can access dmz server # services, plus IMAP for reading email. template dmz_services imap reject log } ``` foomuuri-0.34/docs/faq.md000066400000000000000000000061141526436212200153330ustar00rootroot00000000000000# FAQ ## GNOME NetworkManager supports dynamically [mapping](config/section/zone.md) connections to firewall zones, but the GNOME Control Center graphical connection editor does not. There are two alternative ways to edit a connection's zone: * Use the graphical `nm-connection-editor` (recommended) * Use the command line `nmcli connection modify connection.zone ` This is only needed when changing a connection's firewall zone. All other edits can still be made through GNOME Control Center. ## DHCP To obtain an IP address via DHCP, you must allow both the outgoing `dhcp-server` request and the incoming `dhcp-client` reply. For example: ``` localhost-public { # Allow localhost's DHCP client to send a request to a DHCP server running # on public zone (discover/request a lease). dhcp-server dhcpv6-server } public-localhost { # Allow the reply packet from public's DHCP server to localhost's client # (offer an IP address). dhcp-client dhcpv6-client } ``` Similar rules are required if you run a DHCP server on `localhost` that serves leases to clients in your `internal` zone: ``` internal-localhost { # Allow incoming DHCP discover/request from the internal zone's client to # the DHCP server running on localhost. dhcp-server dhcpv6-server } localhost-internal { # Allow localhost's DHCP server to send an offer reply to the internal # zone's client. dhcp-client dhcpv6-client } ``` ## Proxy ARP See this [discussion](https://github.com/FoobarOy/foomuuri/discussions/2) for instructions on configuring proxy ARP with Foomuuri using [hooks](config/section/hook.md). ## Hairpin NAT / NAT Loopback See this [issue](https://github.com/FoobarOy/foomuuri/issues/8) for instructions on configuring hairpin NAT with Foomuuri using [SNAT](config/section/snat.md) and [DNAT](config/section/dnat.md). In most cases, split DNS is preferable to hairpin NAT. With split DNS, a locally served zone resolves to local IP addresses while a publicly served zone resolves to public IP addresses. ## fail2ban Foomuuri supports automatic IP address [banning](example/advanced.md#automatic-ip-address-banning) without any external programs, so `fail2ban` is usually unnecessary. Banning happens entirely on the packet path, natively at the nftables level. Alternatively, see this [issue](https://github.com/FoobarOy/foomuuri/issues/9) for instructions on integrating Foomuuri with `fail2ban`. ## Custom nftables Chains See this [discussion](https://github.com/FoobarOy/foomuuri/discussions/31) for instructions on defining custom nftables chains and jumping to them. ## QEMU/libvirt and vnet Interfaces in a Bridge See this [discussion](https://github.com/FoobarOy/foomuuri/discussions/15) for instructions on silently dropping `OUTPUT REJECT IN= OUT=vnetXX` log entries, which appear after a bridge interface is added or removed. ## Preventing Private Networks from Routing to the Public Internet See this [issue](https://github.com/FoobarOy/foomuuri/issues/24) for instructions on blocking routing of 10.0.0.0/8 and similar private networks to the public internet. foomuuri-0.34/docs/index.md000066400000000000000000000044331526436212200156750ustar00rootroot00000000000000--- hide: - navigation --- # Foomuuri Foomuuri is a multizone bidirectional nftables firewall. Example configuration files are available for a [host firewall](example/host-firewall.md) and a [router firewall](example/router-firewall.md). See the [installation](install.md) page for instructions on installing Foomuuri. Help is available via [GitHub discussions](https://github.com/FoobarOy/foomuuri/discussions) and the `#foomuuri` IRC channel on Libera.Chat. ## Features * Firewall [zones](config/section/zone.md) * [Bidirectional](example/host-firewall.md#bidirectional) firewalling for incoming, outgoing, and forwarding traffic * Suitable for systems ranging from personal [laptops](example/host-firewall.md) to [corporate](example/router-firewall.md) firewalls * Rich rule language for flexible and complex [rules](config/rule/index.md) * Predefined list of [services](https://github.com/FoobarOy/foomuuri/blob/main/etc/default.services.conf) for simpler rule writing * [Macros](config/section/macro.md) and [templates](config/section/template.md) supported in the rule language * IPv4 and IPv6 support, with automatic rule [splitting](config/rule/matcher.md#saddr-daddr) per protocol * [SNAT](config/section/snat.md), [DNAT](config/section/dnat.md), and masquerading support * [Logging](config/rule/logging.md) and packet/byte counting * [Rate limiting](config/rule/ratelimit.md) * DNS hostname [lookups](config/section/iplist.md) and IP-list support, with dynamic IP address refreshing * External country and ASN IP lists through [iplists](config/section/iplist.md) (geolocation) * [Multiple ISP](example/multiple-isp.md) support, with an internal network [connectivity monitor](tool/monitor.md) * [IPsec](config/rule/matcher.md#sipsec-dipsec) matching support * Ability to [map](config/section/zonemap.md) specific traffic to separate zones * Automatic IP address [banning](example/advanced.md#automatic-ip-address-banning) support * [Port knocking](example/advanced.md#port-knocking) support * D-Bus API * Firewalld emulation for NetworkManager zone support * Support for raw nftables [rules](config/rule/misc.md#nft) * Written to use modern nftables features ## Changelog Recent changes are documented in the [changelog](https://github.com/FoobarOy/foomuuri/blob/main/CHANGELOG.md). foomuuri-0.34/docs/install.md000066400000000000000000000060541526436212200162350ustar00rootroot00000000000000# Installation The recommended way to install Foomuuri is through your Linux distribution's package manager, using the instructions for your platform below. For a sample configuration, see the [host firewall example](example/host-firewall.md), which documents a typical `/etc/foomuuri/foomuuri.conf` file. ## Fedora, RHEL, and CentOS Stream Foomuuri is included in Fedora and EPEL. ```sh dnf install foomuuri foomuuri-firewalld ``` Continue with [post-installation configuration](#post-installation-configuration). ## Debian and Ubuntu Foomuuri is included in Debian sid, forky, and trixie (13), as well as bookworm-backports (12), and in Ubuntu 23.10 (Mantic) and 24.04 (Noble). ```sh apt install foomuuri foomuuri-firewalld ``` Continue with [post-installation configuration](#post-installation-configuration). ## Arch Linux Foomuuri is available in the Arch User Repository (AUR). ```sh # Build and install the package git clone https://aur.archlinux.org/foomuuri.git cd foomuuri makepkg pacman -U foomuuri-*-x86_64.pkg.tar.zst ``` Continue with [post-installation configuration](#post-installation-configuration). ## Building from Source Source tarballs are available on the [releases page](https://github.com/FoobarOy/foomuuri/releases). **Requirements** - `nftables` version 1.0.0 or higher, built with JSON support - `python` version 3.9 or higher **Recommended** - `python3-dbus` and `python3-gobject` (`python3-gi` on some distributions), for D-Bus support **Optional** - `python3-systemd`, for systemd notify support - `python3-urllib3`, for iplist URL download support - `python3-lxml`, for iplist HTML/XML filtering - `jq`, for iplist JSON filtering - `fping`, for network connectivity monitor ```sh # Extract the source tar xf foomuuri-0.??.tar.gz cd foomuuri-0.?? # Install to the root filesystem make install DESTDIR=/ systemctl daemon-reload sysctl --system ``` Continue with [post-installation configuration](#post-installation-configuration). ## Post-Installation Configuration These steps apply regardless of how Foomuuri was installed. 1. Configure and validate: ```sh $EDITOR /etc/foomuuri/foomuuri.conf foomuuri check ``` For a sample configuration, see the [host firewall example](example/host-firewall.md). 2. Disable and stop any existing firewall service, for example: ```sh systemctl disable --now ferm.service systemctl disable --now firehol.service systemctl disable --now firewalld.service systemctl disable --now netfilter-persistent.service systemctl disable --now nftables.service systemctl disable --now ufw.service systemctl disable shorewall-init.service systemctl disable shorewall.service systemctl disable shorewall6.service nft flush ruleset ``` 3. Start Foomuuri: ```sh systemctl start foomuuri.service ``` 4. Verify firewall logging in the system journal: ```sh journalctl --follow --dmesg ``` 5. Enable Foomuuri at boot, once you have confirmed it is working correctly: ```sh systemctl enable foomuuri.service ``` foomuuri-0.34/docs/tool/000077500000000000000000000000001526436212200152155ustar00rootroot00000000000000foomuuri-0.34/docs/tool/monitor.md000066400000000000000000000114721526436212200172330ustar00rootroot00000000000000# Foomuuri Monitor Foomuuri includes a simple network connectivity monitor. It monitors your internet connection by pinging an external server, and can run a command whenever the network link goes up or down. An example [command](https://github.com/FoobarOy/foomuuri/blob/main/misc/monitor.event) that sends an email notification to root is included in the doc directory. Another example for [multiple ISP](../example/multiple-isp.md) configurations and commands, is also available. ## target ### Target Commands The minimal configuration is: ``` target google { command fping 8.8.4.4 } ``` This creates a monitor called `google` that runs the `fping` command, pinging IP address 8.8.4.4 every second. Foomuuri parses the output and logs up and down events. Multiple targets can be defined. A more realistic example: ``` target my-isp-router { command fping --interval=2000 172.25.31.149 command_up /etc/foomuuri/monitor.event command_down /etc/foomuuri/monitor.event } ``` This pings IP address 172.25.31.149 every two seconds and runs the `monitor.event` command whenever the link goes up or down. That command sends an email notification to root. See `man fping` or the [fping website](https://www.fping.org/) for a description of `fping`'s parameters. Foomuuri supports both `interval` and `squiet` modes; it is recommended to use whole seconds in `--interval`. It is recommended to use an IP address rather than a hostname as the `fping` target. Hostname lookups will fail if the network is down when `fping` starts. Foomuuri handles this case, but it causes a 30-second delay and a possible `fping` restart loop. `curl` and other programs can also be used instead of `fping`. See the example [shell script](https://github.com/FoobarOy/foomuuri/blob/main/misc/monitor-example-command.sh) for how to use them. ### Up/Down State Thresholds "Up" and "down" states are defined with the following parameters: ``` target my-isp-router { history_size 100 # how many results are saved history_up 80 # count of UPs => n => UP history_down 30 # count of DOWNs >= n => DOWN consecutive_up 20 # last n were UP => UP consecutive_down 10 # last n were DOWN => DOWN ... } ``` A target is considered up if 80 of the last 100 pings succeeded (failures in between are allowed) or if the last 20 pings succeeded (no failures allowed). A target is considered down if 30 of the last 100 pings failed, or if the last 10 pings failed. ### Periodic Down Interval Execution The optional `command_down_interval` setting can be specified. Foomuuri will run it every `down_interval` seconds (default: 600, i.e., every 10 minutes). Example: ``` target my-isp-router { # Connectivity is still down. Ask NetworkManager to re-initialize # the eth0 connection. command_down_interval nmcli connection up eth0 # Run it every 15 minutes. down_interval 900 ... } ``` ### Environment Variables The up/down command receives status information via environment variables: * `FOOMUURI_CHANGE_TYPE`: type, `target` or `group` * `FOOMUURI_CHANGE_NAME`: name of the target or group changing state * `FOOMUURI_CHANGE_STATE`: state, `up` or `down` * `FOOMUURI_CHANGE_LOG`: additional logging information * `FOOMUURI_CHANGE_HISTORY`: a list of `!` (error) or `.` (ok) characters indicating the status of recent checks * `FOOMUURI_ALL_TARGET`: list of all configured targets * `FOOMUURI_ALL_GROUP`: list of all configured groups * `FOOMUURI_TARGET_xxx`: state (`up` or `down`) for target `xxx` (the target name is sanitized to remove non-alphanumeric characters) * `FOOMUURI_GROUP_xxx`: state (`up` or `down`) for group `xxx` (the group name is sanitized to remove non-alphanumeric characters) Only a single command can be specified. If you need to run multiple commands, use a shell wrapper script to run them. ### Statistics Monitor statistics are written to a file periodically. This interval can be changed with `statistics_interval 60`. The default value is 60 seconds (once a minute). The number of statistics kept can be changed with `statistics_size 300`. The default value is 300, keeping the last 300 results. ## group Multiple monitor [targets](monitor.md#target) can be grouped into a single monitor. Example: ``` group my-isp-group { target my-isp-router google command_up /etc/foomuuri/monitor.event command_down /etc/foomuuri/monitor.event } ``` This creates a monitor called `my-isp-group` that includes two targets. A group is considered up if any of its targets is up, and down only if all of its targets are down. It is generally safer to run up/down commands in a `group` section with multiple targets than in a single `target` section. The optional `command_down_interval` and `down_interval` settings can also be defined here; see [above](monitor.md#target) for details. foomuuri-0.34/docs/zensical.toml000066400000000000000000000063611526436212200167530ustar00rootroot00000000000000[project] nav = [ {"Home" = "index.md"}, {"Installation" = "install.md"}, {"Configuration" = [ {"Basics" = "config/basic.md"}, {"Sections" = [ "config/section/index.md", {"foomuuri" = "config/section/foomuuri.md"}, {"zone" = "config/section/zone.md"}, {"zone-zone" = "config/section/zonezone.md"}, {"zone-any, any-zone, any-any" = "config/section/any.md"}, {"macro" = "config/section/macro.md"}, {"iplist" = "config/section/iplist.md"}, {"snat" = "config/section/snat.md"}, {"dnat" = "config/section/dnat.md"}, {"template" = "config/section/template.md"}, {"hook" = "config/section/hook.md"}, {"zonemap" = "config/section/zonemap.md"}, {"prerouting, postrouting, forward, input, output" = "config/section/special.md"}, {"invalid, rpfilter, smurfs" = "config/section/invalid.md"}, {"target, group" = "config/section/targetgroup.md"}, ]}, {"Rule" = [ "config/rule/index.md", {"Matchers" = "config/rule/matcher.md"}, {"Statements" = "config/rule/statement.md"}, {"Logging" = "config/rule/logging.md"}, {"Rate Limit" = "config/rule/ratelimit.md"}, {"Miscellaneous" = "config/rule/misc.md"}, ]}, ]}, {"Examples" = [ {"Host Firewall" = "example/host-firewall.md"}, {"Router Firewall" = "example/router-firewall.md"}, {"Advanced Filtering" = "example/advanced.md"}, {"Multiple ISP" = "example/multiple-isp.md"}, ]}, {"Tools" = [ {"Connectivity Monitor" = "tool/monitor.md"}, ]}, {"FAQ" = "faq.md"}, ] site_name = "Foomuuri" site_url = "https://foomuuri.foobar.fi/latest" repo_url = "https://github.com/FoobarOy/foomuuri" copyright = "Copyright 2023-2026, Kim B. Heino, Foobar Oy and contributors." [project.extra.version] provider = "mike" alias = true [project.theme] custom_dir = "overrides" features = [ "content.code.copy", "content.code.select", "navigation.footer", "navigation.indexes", "navigation.path", "navigation.tabs", "navigation.top", "navigation.tracking", "search.highlight", "toc.integrate", ] [project.markdown_extensions.toc] toc_depth = 2 # Palette toggle for automatic mode [[project.theme.palette]] media = "(prefers-color-scheme)" toggle.icon = "lucide/sun-moon" toggle.name = "Switch to light mode" # Palette toggle for light mode [[project.theme.palette]] media = "(prefers-color-scheme: light)" scheme = "default" toggle.icon = "lucide/sun" toggle.name = "Switch to dark mode" # Palette toggle for dark mode [[project.theme.palette]] media = "(prefers-color-scheme: dark)" scheme = "slate" toggle.icon = "lucide/moon" toggle.name = "Switch to system preference" [project.markdown_extensions.pymdownx.highlight] anchor_linenums = true line_spans = "__span" pygments_lang_class = true [project.markdown_extensions.pymdownx.inlinehilite] [project.markdown_extensions.pymdownx.snippets] [project.markdown_extensions.pymdownx.superfences] custom_fences = [ { name = "mermaid", class = "mermaid", format = "pymdownx.superfences.fence_code_format" } ] foomuuri-0.34/etc/000077500000000000000000000000001526436212200140635ustar00rootroot00000000000000foomuuri-0.34/etc/50-foomuuri.conf000066400000000000000000000016331526436212200170240ustar00rootroot00000000000000# Warning: Do not edit this file as Foomuuri update will overwrite it. # # foomuuri: not-conf # Use secure ARP settings net.ipv4.conf.default.arp_announce = 2 net.ipv4.conf.all.arp_announce = 2 net.ipv4.conf.default.arp_ignore = 1 net.ipv4.conf.all.arp_ignore = 1 net.ipv4.conf.default.arp_filter = 1 net.ipv4.conf.all.arp_filter = 1 # Don't accept or send redirects net.ipv6.conf.default.accept_redirects = 0 net.ipv6.conf.all.accept_redirects = 0 net.ipv4.conf.default.accept_redirects = 0 net.ipv4.conf.all.accept_redirects = 0 net.ipv4.conf.default.send_redirects = 0 net.ipv4.conf.all.send_redirects = 0 # Set printk logging level so that firewall logging doesn't show up in console kernel.printk = 4 ## To enable IP packet forwarding copy these two lines uncommented to ## /etc/sysctl.d/50-ip.forwarding.conf file. Do not edit this file. # # net.ipv4.conf.all.forwarding = 1 # net.ipv6.conf.all.forwarding = 1 foomuuri-0.34/etc/block.fw000066400000000000000000000011411526436212200155100ustar00rootroot00000000000000# "Block all traffic" ruleset that can be used in foomuuri-boot.service # instead of "good.fw". This file is also used by "foomuuri block" command. # # Use command "systemctl edit --full foomuuri-boot.service" to switch to this # file instead of "good.fw". # # Warning: Do not edit this file as Foomuuri update will overwrite it. table inet foomuuri delete table inet foomuuri table inet foomuuri { chain input { type filter hook input priority filter drop } chain output { type filter hook output priority filter drop } chain forward { type filter hook forward priority filter drop } } foomuuri-0.34/etc/default.services.conf000066400000000000000000000130341526436212200202010ustar00rootroot00000000000000# Known services as macros. # # Macro name should match service name in /etc/services file, macro # definitation should be minimal set of ports to open. Minimal means that # client and server should have separate macros, web-administration should # have it's own macro, etc. # # Warning: Do not edit this file as Foomuuri update will overwrite it. macro { activedirectory domain; kerberos; ntp; kpasswd; ldap; ldaps; udp 389; tcp 135 3268 3269 49152-65535 adb tcp 5555 afp tcp 548 # afpovertcp airport udp 192 # osu-nms alertmanager tcp 9093 amqp tcp 5672 android tcp 5228-5230 4070 4460; udp 5228-5230 2002; https apple tcp 2197 5223; https bgp tcp 179 ceph tcp 3300 6789 6800-7568 cockpit tcp 9090 dhcp-client udp 68 ipv4; broadcast udp 68 # bootpc, from server to client dhcp-server udp 67 ipv4; broadcast udp 67 # bootps, from client to server dhcpv6-client udp sport 547 dport 546 daddr fe80::/10 dhcpv6-server multicast udp sport 546 dport 547 daddr ff02::1:2 discord tcp 2053 2083 2087 2096; udp 3478-3482 19294-19344 50000-65535; https domain tcp 53; udp 53 domain-quic udp 853 domain-s domain-quic; domain-tls domain-tls tcp 853 facetime udp 3478-3497 16384-16387 16393-16402; apple finger tcp 79 fooham tcp 9997; udp 9997 freeipa domain; http; https; kerberos; kpasswd; ldap; ldaps ftp tcp 21 helper ftp-21 ftps tcp 990 galera tcp 4444 4567-4568 git tcp 9418 gluster tcp 24007 49152-60999 gluster-client gluster warning "macro gluster-client is obsoleted, use gluster instead" gluster-management tcp 24008 googlemeet udp 3478 19302-19309; https gotomeeting tcp 3478; udp 3478; https hkp tcp 11371 http tcp 80 http-alt tcp 8000 8008 8080 8443 http2 tcp 443 https tcp 443; udp 443 imap tcp 143 imaps tcp 993 ipp tcp 631 ipsec udp 500 4500; protocol "esp" ipsec-nat udp sport 4500; ipsec irc tcp 6667 helper irc-6667 ircs-u tcp 6697 jetdirect tcp 9100 kerberos tcp 88; udp 88 kpasswd tcp 464; udp 464 ldap tcp 389 ldaps tcp 636 lsdp broadcast udp 11430 matter udp 5540 mdns multicast udp 5353 daddr 224.0.0.251 ff02::fb; multicast protocol "igmp" daddr 224.0.0.251; udp sport 5353 meetecho tcp 1935 8000 8181; https microsoftteams udp 3478-3481; https minecraft tcp 25565 mongodb tcp 27017 mqtt tcp 1883 ms-sql-m udp 1434 ms-sql-s tcp 1433 mysql tcp 3306 nbd tcp 10809 nfs tcp 2049 nfsv3 tcp 2049 111 20048 ntp udp 123 ntske tcp 4460 nut tcp 3493 ospf multicast protocol "ospf" daddr 224.0.0.5 224.0.0.6 ff02::5 ff02::6; multicast protocol "igmp" daddr 224.0.0.5 224.0.0.6; protocol "ospf" ping icmp echo-request; icmpv6 echo-request pop3s tcp 995 postgresql tcp 5432 prometheus tcp 9090 prometheus-blackbox tcp 9115 prometheus-chrony tcp 9123 prometheus-foobar tcp 11042 prometheus-foomuuri tcp 11041 prometheus-gluster tcp 9713 prometheus-hcloud tcp 9501 prometheus-ipmi tcp 9290 prometheus-keepalived tcp 9165 prometheus-knot tcp 9433 prometheus-mysqld tcp 9104 prometheus-node tcp 9100 prometheus-nut tcp 9199 prometheus-php-fpm tcp 9253 prometheus-postfix tcp 9907 prometheus-postgresql tcp 9187 prometheus-rasdaemon tcp 10029 prometheus-redis tcp 9121 prometheus-smartctl tcp 9633 prometheus-ssl tcp 9219 prometheus-systemd tcp 9558 prometheus-unbound tcp 9167 prometheus-windows tcp 9182 pxe udp 4011 razor tcp 2703 rdp tcp 3389; udp 3389 redis tcp 6379 redis-sentinel tcp 26379 rfb vnc rsync tcp 873 rtsps tcp 322 salt tcp 4505 4506 secure-mqtt tcp 8883 sieve tcp 4190 sip udp 5060 helper sip-5060 sips tcp 5061 smb tcp 139 445 # cifs smtp tcp 25 snmp udp 161 helper snmp-161 snmptrap udp 162 ssdp multicast udp 1900 daddr 239.255.255.250 ff02::c; multicast protocol "igmp" daddr 239.255.255.250; udp sport 1900 ssh tcp 22 submission tcp 587 submissions tcp 465 svn tcp 3690 syslog tcp 514; udp 514 syslog-tls tcp 6514; udp 6514 telnet tcp 23 telnets tcp 992 tftp udp 69 helper tftp-69 tor tcp 9001 tor-browser-bundle tcp 9150 tor-control tcp 9051 tor-directory tcp 9030 tor-socks tcp 9050 traceroute udp 33434-33524 vnc tcp 5900 vrrp-multicast multicast protocol "vrrp" daddr 224.0.0.18 ff02::12; multicast protocol "igmp" daddr 224.0.0.18 whois tcp 43 4321 wireguard udp 51820 ws-discovery multicast udp 3702 daddr 239.255.255.250 ff02::c; multicast protocol "igmp" daddr 239.255.255.250; udp sport 3702; tcp 5357 xmpp-client tcp 5222 zabbix-agent tcp 10050 zabbix-trapper tcp 10051 zoom tcp 8801-8802; udp 3478-3479 8801-8810; https } foomuuri-0.34/etc/static.nft000066400000000000000000000034331526436212200160660ustar00rootroot00000000000000 chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } foomuuri-0.34/firewalld/000077500000000000000000000000001526436212200152615ustar00rootroot00000000000000foomuuri-0.34/firewalld/dbus-firewalld.conf000066400000000000000000000000421526436212200210300ustar00rootroot00000000000000foomuuri { dbus_firewalld yes } foomuuri-0.34/firewalld/fi.foobar.Foomuuri-FirewallD.conf000066400000000000000000000022711526436212200234520ustar00rootroot00000000000000 foomuuri-0.34/man/000077500000000000000000000000001526436212200140635ustar00rootroot00000000000000foomuuri-0.34/man/Makefile000066400000000000000000000002371526436212200155250ustar00rootroot00000000000000all: foomuuri.8 prometheus-foomuuri-exporter.1 %.1: %.md pandoc --standalone --to=man $^ --output $@ %.8: %.md pandoc --standalone --to=man $^ --output $@ foomuuri-0.34/man/foomuuri.8000066400000000000000000000066021526436212200160250ustar00rootroot00000000000000.\" Automatically generated by Pandoc 3.7.0.2 .\" .TH "FOOMUURI" "8" "Oct 16, 2026" "Foomuuri 0.34" "User Manual" .SH NAME foomuuri \- multizone bidirectional nftables firewall .SH SYNOPSIS \f[B]foomuuri\f[R] [\f[I]OPTIONS\f[R]] \f[I]COMMAND\f[R] .SH DESCRIPTION \f[B]Foomuuri\f[R] is a firewall generator for nftables based on the concept of zones. It is suitable for all systems from personal machines to corporate firewalls, and supports advanced features such as a rich rule language, IPv4/IPv6 rule splitting, dynamic DNS lookups, a D\-Bus API and firewalld emulation for NetworkManager\(cqs zone support. .SH OPTIONS .TP \f[B]\-\-help\f[R] Print help and exit. .TP \f[B]\-\-version\f[R] Print version information and exit. .TP \f[B]\-\-verbose\f[R] Verbose output. .TP \f[B]\-\-quiet\f[R] Be quiet. .TP \f[B]\-\-force\f[R] Force some operations, don\(cqt check anything. .TP \f[B]\-\-soft\f[R] Don\(cqt force operations, check more. .TP \f[B]\-\-fork\f[R] Fork as a background daemon process. .TP \f[B]\-\-syslog\f[R] Enable syslog logging. .TP \f[B]\-\-set=OPTION=VALUE\f[R] Set foomuuri{} config OPTION to VALUE. .SH COMMANDS .TP \f[B]start\f[R] Load configuration files, generate new ruleset and load it to kernel. .TP \f[B]stop\f[R] Remove ruleset from kernel. .TP \f[B]reload\f[R] Same as \f[B]start\f[R], followed by \f[B]iplist refresh\f[R]. .TP \f[B]try\-reload\f[R] Same as \f[B]reload\f[R], ask confirmation to keep new config. Revert back to old config if no reply. .TP \f[B]status\f[R] Show current status: running, zone\-interface mapping. .TP \f[B]check\f[R] Load configuration files and verify syntax. .TP \f[B]block\f[R] Load \(lqblock all traffic\(rq ruleset to kernel. .TP \f[B]ruleset list [ZONE\-ZONE]\&...\f[R] List active ruleset currently loaded to kernel. Include whole ruleset or only specified \f[B]ZONE\-ZONE\f[R]. .TP \f[B]macro list [NAME | VALUE]\&...\f[R] List all macros or macros with specified NAME(s) or VALUE(s). .TP \f[B]counter list [COUNTER]\&...\f[R] List all or specified named COUNTER(s). .TP \f[B]counter reset [COUNTER]\&...\f[R] Reset all or specified named COUNTER(s) values. .TP \f[B]iplist status [IPLIST]\&...\f[R] List number of entries of all or specified IPLIST(s). .TP \f[B]iplist list [IPLIST]\&... [IPADDRESS]\&...\f[R] List entries of all or specified IPLIST(s) containing IPADDRESS(es). .TP \f[B]iplist add IPLIST [TIMEOUT] IPADDRESS [IPADDRESS]\&...\f[R] Add or refresh IPADDRESS(es) to IPLIST. TIMEOUT format is the same as in iplist{} section, for example \(lq4h\(rq. .TP \f[B]iplist del IPLIST IPADDRESS [IPADDRESS]\&...\f[R] Delete IPADDRESS(es) from IPLIST. .TP \f[B]iplist flush [IPLIST]\&...\f[R] Delete all added IP addresses from all or specified IPLIST(s). .TP \f[B]iplist refresh [IPLIST]\&...\f[R] Refresh all or specified IPLIST(s) now. With the \f[B]\-\-soft\f[R] option, the refresh interval is checked before fetching new values from the URL or DNS. .TP \f[B]set interface INTERFACE zone {ZONE | \-}\f[R] Change INTERFACE to ZONE, or remove from all zones. .SH FILES \f[B]Foomuuri\f[R] reads configuration files from \f[I]/etc/foomuuri/*.conf\f[R]. See \c .UR https://foomuuri.foobar.fi/latest/example/host-firewall/ .UE \c \ for example configuration. .SH AUTHORS Kim B. Heino, b\(atbbbs.net, Foobar Oy .SH BUG REPORTS Submit bug reports \c .UR https://github.com/FoobarOy/foomuuri/issues .UE \c .SH SEE ALSO Full documentation \c .UR https://foomuuri.foobar.fi/latest/ .UE \c foomuuri-0.34/man/foomuuri.md000066400000000000000000000060151526436212200162540ustar00rootroot00000000000000--- title: FOOMUURI section: 8 header: User Manual footer: Foomuuri 0.34 date: Oct 16, 2026 --- # NAME foomuuri - multizone bidirectional nftables firewall # SYNOPSIS **foomuuri** [*OPTIONS*] *COMMAND* # DESCRIPTION **Foomuuri** is a firewall generator for nftables based on the concept of zones. It is suitable for all systems from personal machines to corporate firewalls, and supports advanced features such as a rich rule language, IPv4/IPv6 rule splitting, dynamic DNS lookups, a D-Bus API and firewalld emulation for NetworkManager's zone support. # OPTIONS **\--help** : Print help and exit. **\--version** : Print version information and exit. **\--verbose** : Verbose output. **\--quiet** : Be quiet. **\--force** : Force some operations, don't check anything. **\--soft** : Don't force operations, check more. **\--fork** : Fork as a background daemon process. **\--syslog** : Enable syslog logging. **\--set=OPTION=VALUE** : Set foomuuri{} config OPTION to VALUE. # COMMANDS **start** : Load configuration files, generate new ruleset and load it to kernel. **stop** : Remove ruleset from kernel. **reload** : Same as **start**, followed by **iplist refresh**. **try-reload** : Same as **reload**, ask confirmation to keep new config. Revert back to old config if no reply. **status** : Show current status: running, zone-interface mapping. **check** : Load configuration files and verify syntax. **block** : Load "block all traffic" ruleset to kernel. **ruleset list [ZONE-ZONE]...** : List active ruleset currently loaded to kernel. Include whole ruleset or only specified **ZONE-ZONE**. **macro list [NAME | VALUE]...** : List all macros or macros with specified NAME(s) or VALUE(s). **counter list [COUNTER]...** : List all or specified named COUNTER(s). **counter reset [COUNTER]...** : Reset all or specified named COUNTER(s) values. **iplist status [IPLIST]...** : List number of entries of all or specified IPLIST(s). **iplist list [IPLIST]... [IPADDRESS]...** : List entries of all or specified IPLIST(s) containing IPADDRESS(es). **iplist add IPLIST [TIMEOUT] IPADDRESS [IPADDRESS]...** : Add or refresh IPADDRESS(es) to IPLIST. TIMEOUT format is the same as in iplist{} section, for example "4h". **iplist del IPLIST IPADDRESS [IPADDRESS]...** : Delete IPADDRESS(es) from IPLIST. **iplist flush [IPLIST]...** : Delete all added IP addresses from all or specified IPLIST(s). **iplist refresh [IPLIST]...** : Refresh all or specified IPLIST(s) now. With the **-\-soft** option, the refresh interval is checked before fetching new values from the URL or DNS. **set interface INTERFACE zone {ZONE | -}** : Change INTERFACE to ZONE, or remove from all zones. # FILES **Foomuuri** reads configuration files from */etc/foomuuri/\*.conf*. See for example configuration. # AUTHORS Kim B. Heino, b@bbbs.net, Foobar Oy # BUG REPORTS Submit bug reports # SEE ALSO Full documentation foomuuri-0.34/man/prometheus-foomuuri-exporter.1000066400000000000000000000030761526436212200220570ustar00rootroot00000000000000.\" Automatically generated by Pandoc 3.7.0.2 .\" .TH "PROMETHEUS\-FOOMUURI\-EXPORTER" "1" "Oct 16, 2026" "prometheus\-foomuuri\-exporter 0.34" "User Manual" .SH NAME prometheus\-foomuuri\-exporter \- Prometheus exporter for Foomuuri metrics .SH SYNOPSIS \f[B]prometheus\-foomuuri\-exporter\f[R] [\f[I]OPTIONS\f[R]] .SH DESCRIPTION \f[B]prometheus\-foomuuri\-exporter\f[R] reads Foomuuri metrics and exports them to Prometheus. .SH OPTIONS .TP \f[B]\-\-help\f[R] Print help and exit. .TP \f[B]\-\-address ADDRESS\f[R] Listen address. (default: ::) .TP \f[B]\-\-port PORT\f[R] Listen port number. (default: 11041) .TP \f[B]\-\-tls\-certificate FILENAME\f[R] TLS certificate file name. .TP \f[B]\-\-tls\-key FILENAME\f[R] TLS key file name. .TP \f[B]\-\-no\-monitor\-statistics\f[R] Do not export Foomuuri Monitor statistics. .TP \f[B]\-\-no\-ruleset\-statistics\f[R] Do not export ruleset statistics. .TP \f[B]\-\-statistics\-file FILENAME\f[R] Foomuuri Monitor statistics file name. .TP \f[B]\-\-set\-include REGEXP\f[R] Set names to be included to ruleset size statistics. .TP \f[B]\-\-set\-exclude REGEXP\f[R] Set names to be excluded from ruleset size statistics. .TP \f[B]\-\-counter\-include REGEXP\f[R] Counter names to be included to ruleset traffic statistics. .TP \f[B]\-\-counter\-exclude REGEXP\f[R] Counter names to be excluded from ruleset traffic statistics. .SH AUTHORS Kim B. Heino, b\(atbbbs.net, Foobar Oy .SH BUG REPORTS Submit bug reports \c .UR https://github.com/FoobarOy/foomuuri/issues .UE \c .SH SEE ALSO Full documentation \c .UR https://foomuuri.foobar.fi/latest/ .UE \c foomuuri-0.34/man/prometheus-foomuuri-exporter.md000066400000000000000000000026141526436212200223140ustar00rootroot00000000000000--- title: PROMETHEUS-FOOMUURI-EXPORTER section: 1 header: User Manual footer: prometheus-foomuuri-exporter 0.34 date: Oct 16, 2026 --- # NAME prometheus-foomuuri-exporter - Prometheus exporter for Foomuuri metrics # SYNOPSIS **prometheus-foomuuri-exporter** [*OPTIONS*] # DESCRIPTION **prometheus-foomuuri-exporter** reads Foomuuri metrics and exports them to Prometheus. # OPTIONS **\--help** : Print help and exit. **\--address ADDRESS** : Listen address. (default: ::) **\--port PORT** : Listen port number. (default: 11041) **\--tls-certificate FILENAME** : TLS certificate file name. **\--tls-key FILENAME** : TLS key file name. **\--no-monitor-statistics** : Do not export Foomuuri Monitor statistics. **\--no-ruleset-statistics** : Do not export ruleset statistics. **\--statistics-file FILENAME** : Foomuuri Monitor statistics file name. **\--set-include REGEXP** : Set names to be included to ruleset size statistics. **\--set-exclude REGEXP** : Set names to be excluded from ruleset size statistics. **\--counter-include REGEXP** : Counter names to be included to ruleset traffic statistics. **\--counter-exclude REGEXP** : Counter names to be excluded from ruleset traffic statistics. # AUTHORS Kim B. Heino, b@bbbs.net, Foobar Oy # BUG REPORTS Submit bug reports # SEE ALSO Full documentation foomuuri-0.34/misc/000077500000000000000000000000001526436212200142435ustar00rootroot00000000000000foomuuri-0.34/misc/foomuuri-bash-completion000066400000000000000000000052451526436212200211230ustar00rootroot00000000000000# Foomuuri completion -*- shell-script -*- _comp_cmd_foomuuri() { local cur prev words cword was_split comp_args _comp_initialize -s -- "$@" || return local option_count=0 for word in "${words[@]}"; do if [[ "${word}" == --* ]]; then option_count=$((option_count+1)) fi done case $((cword-option_count)) in 1) _comp_compgen -- -W "help start stop reload try-reload status check block ruleset macro counter iplist set" ;; 2) case ${prev} in ruleset | macro | counter) _comp_compgen -- -W "list" ;; iplist) _comp_compgen -- -W "status list add del flush refresh" ;; set) _comp_compgen -- -W "interface" ;; esac ;; 3) case ${prev} in list) case ${words[cword-2]} in ruleset) zones=$(foomuuri --quiet --quiet status | sed '/^ /!d; s/^ *//; s/ .*//') if [[ ${cur} =~ "-" ]]; then for szone in ${zones}; do for dzone in ${zones}; do _comp_compgen -a -- -W "${szone}-${dzone}" done done else for zone in ${zones}; do _comp_compgen -a -- -W "${zone}-" done compopt -o nospace fi ;; macro) macros=$(foomuuri --quiet --quiet macro list | sed '/^ /!d; s/^ *//; s/ .*//') _comp_compgen -- -W "${macros}" ;; counter) counters=$(foomuuri --quiet --quiet counter list | sed '1,2d; s/ .*//') _comp_compgen -- -W "${counters}" ;; iplist) iplists=$(foomuuri --quiet --quiet iplist status | sed '/^@/!d; s/ .*//') _comp_compgen -- -W "${iplists}" ;; esac ;; status | add | del | flush | refresh) # iplist iplists=$(foomuuri --quiet --quiet iplist status | sed '/^@/!d; s/ .*//') _comp_compgen -- -W "${iplists}" ;; interface) # set interfaces=$(ip link show | sed '/^[0-9]\+: /!d; s/[0-9]\+: //; /lo:/d; s/:.*//') _comp_compgen -- -W "${interfaces}" ;; esac ;; 4) interfaces=$(ip link show | sed '/^[0-9]\+: /!d; s/[0-9]\+: //; /lo:/d; s/:.*//') for intf in ${interfaces}; do if [ "${prev}" = "${intf}" ]; then # set interface intf _comp_compgen -- -W "zone" fi done ;; 5) case ${prev} in zone) # set interface intf zone zones=$(foomuuri --quiet --quiet status | sed '/^ /!d; s/^ *//; s/ .*//') _comp_compgen -- -W "${zones} -" return # "-" is not an option ;; esac ;; esac [[ ${was_split} ]] && return if [[ ${cur} == -* ]]; then _comp_compgen_help [[ ${COMPREPLY-} == *= ]] && compopt -o nospace fi } && complete -F _comp_cmd_foomuuri foomuuri # ex: filetype=sh foomuuri-0.34/misc/monitor-example-command.sh000077500000000000000000000007701526436212200213420ustar00rootroot00000000000000#!/bin/sh # This is an example shell script how to use curl instead of fping to monitor # network connectivity. # # target foobar { # command /etc/foomuuri/monitor-example-command.sh # command_up /etc/foomuuri/monitor.event # command_down /etc/foomuuri/monitor.event # } while true; do # Echoed text must be "OK" or "ERROR", everything else is ignored [ "$(curl --silent http://foobar.fi/test/connectivity)" = "OK" ] && echo OK || echo ERROR # Small wait and repeat sleep 5 done foomuuri-0.34/misc/monitor.event000077500000000000000000000015641526436212200170060ustar00rootroot00000000000000#!/bin/sh # Example command_up / command_down script for foomuuri-monitor. # This script sends an email to root. # Ignore startup change event [ "${FOOMUURI_CHANGE_LOG}" = "startup change" ] && exit 0 # Notify root by email ( # Changed state echo "State change event:" echo " ${FOOMUURI_CHANGE_TYPE} ${FOOMUURI_CHANGE_NAME} ${FOOMUURI_CHANGE_STATE}" echo " ${FOOMUURI_CHANGE_LOG}" echo # All states echo "All states:" for name in ${FOOMUURI_ALL_TARGET}; do state_ref=FOOMUURI_TARGET_${name} state=$(eval "echo \"\$${state_ref}\"") echo " target ${name} ${state}" done for name in ${FOOMUURI_ALL_GROUP}; do state_ref=FOOMUURI_GROUP_${name} state=$(eval "echo \"\$${state_ref}\"") echo " group ${name} ${state}" done ) | mail -s "[foomuuri-monitor] ${FOOMUURI_CHANGE_TYPE} ${FOOMUURI_CHANGE_NAME} ${FOOMUURI_CHANGE_STATE}" root foomuuri-0.34/misc/webdoc/000077500000000000000000000000001526436212200155065ustar00rootroot00000000000000foomuuri-0.34/misc/webdoc/Makefile000066400000000000000000000027321526436212200171520ustar00rootroot00000000000000# Generate web documentation for https://foomuuri.foobar.fi/ .PHONY: all version worktree clean distclean all: distclean mkdir final $(MAKE) version VERSION=0.27 HASH=cdfd86778a61d1ed81eaad50dbebc54bd7e9452e $(MAKE) version VERSION=0.28 HASH=38c5d4a1097f30cc2addd093b66c4a82e0e95763 $(MAKE) version VERSION=0.29 HASH=81c54c327f79baffdb7246d0bc637df821645d31 $(MAKE) version VERSION=0.30 HASH=8a6de6c5e61f62d84cbe9b24cfb742f9c9a65616 $(MAKE) version VERSION=0.31 HASH=8a6de6c5e61f62d84cbe9b24cfb742f9c9a65616 $(MAKE) version VERSION=0.32 HASH=9f850bfa3d55fca9972ff4556fece079e6c10d44 $(MAKE) version VERSION=0.33 HASH=v0.33 $(MAKE) version VERSION=0.34 HASH=v0.34 $(MAKE) version VERSION=development HASH=HEAD cp versions.json robots.txt final/ ln -s $(shell jq -r '.[] | select(.version == "latest") | .title | ltrimstr("v")' versions.json) final/latest ln -s latest/sitemap.xml final/ $(MAKE) clean version: clean (cd ../.. && git archive $(HASH) docs/) | tar --extract [ -f docs/zensical.toml ] || cat docs/nav.toml common.toml > docs/zensical.toml mv docs/zensical.toml . zensical build --strict sed -i -e "/<.loc>$$/a \\ $$(git show -s --format=%cI $(HASH))" site/sitemap.xml find site -exec touch --date="$$(git show -s --format=%ci $(HASH))" {} \; mv site final/$(VERSION) worktree: clean cp -a ../../docs docs mv docs/zensical.toml . zensical build --strict clean: rm -rf .cache zensical.toml docs site distclean: clean rm -rf final foomuuri-0.34/misc/webdoc/common.toml000066400000000000000000000025611526436212200176770ustar00rootroot00000000000000# This file is needed for build documentation for versions 0.27 - 0.32. # [project] (in docs/nav.toml) site_name = "Foomuuri" site_url = "https://foomuuri.foobar.fi/latest" repo_url = "https://github.com/FoobarOy/foomuuri" copyright = "Copyright 2023-2026, Kim B. Heino, Foobar Oy and contributors." [project.extra.version] provider = "mike" alias = true [project.theme] custom_dir = "overrides" features = [ "content.code.copy", "content.code.select", "navigation.expand", "navigation.path", "navigation.top", "navigation.tracking", "search.highlight", ] # Palette toggle for automatic mode [[project.theme.palette]] media = "(prefers-color-scheme)" toggle.icon = "lucide/sun-moon" toggle.name = "Switch to light mode" # Palette toggle for light mode [[project.theme.palette]] media = "(prefers-color-scheme: light)" scheme = "default" toggle.icon = "lucide/sun" toggle.name = "Switch to dark mode" # Palette toggle for dark mode [[project.theme.palette]] media = "(prefers-color-scheme: dark)" scheme = "slate" toggle.icon = "lucide/moon" toggle.name = "Switch to system preference" [project.markdown_extensions.pymdownx.highlight] anchor_linenums = true line_spans = "__span" pygments_lang_class = true [project.markdown_extensions.pymdownx.inlinehilite] [project.markdown_extensions.pymdownx.snippets] [project.markdown_extensions.pymdownx.superfences] foomuuri-0.34/misc/webdoc/overrides/000077500000000000000000000000001526436212200175105ustar00rootroot00000000000000foomuuri-0.34/misc/webdoc/overrides/main.html000066400000000000000000000003041526436212200213170ustar00rootroot00000000000000{% extends "base.html" %} {% block outdated %} You're not viewing the latest version. Click here to go to latest. {% endblock %} foomuuri-0.34/misc/webdoc/robots.txt000066400000000000000000000001531526436212200175560ustar00rootroot00000000000000User-agent: * Allow: / Disallow: /0 Disallow: /development Sitemap: https://foomuuri.foobar.fi/sitemap.xml foomuuri-0.34/misc/webdoc/versions.json000066400000000000000000000010661526436212200202540ustar00rootroot00000000000000[ { "version": "development", "title": "development", "aliases": [] }, { "version": "latest", "title": "v0.34", "aliases": ["latest"] }, { "version": "0.33", "title": "v0.33", "aliases": [] }, { "version": "0.32", "title": "v0.32", "aliases": [] }, { "version": "0.31", "title": "v0.31", "aliases": [] }, { "version": "0.30", "title": "v0.30", "aliases": [] }, { "version": "0.29", "title": "v0.29", "aliases": [] }, { "version": "0.28", "title": "v0.28", "aliases": [] }, { "version": "0.27", "title": "v0.27", "aliases": [] } ] foomuuri-0.34/prometheus/000077500000000000000000000000001526436212200155035ustar00rootroot00000000000000foomuuri-0.34/prometheus/Makefile000066400000000000000000000017501526436212200171460ustar00rootroot00000000000000# Foomuuri - Multizone bidirectional nftables firewall. .PHONY: all test clean distclean install sysupdate # Tests are run as part of top level makefile. Nothing to do here. all test: # Nothing to be cleaned clean distclean: # Install current source to DESTDIR EXPORTER_DIR ?= /usr/bin EXPORTER_NAME ?= prometheus-foomuuri-exporter SYSTEMD_SYSTEM_LOCATION ?= /usr/lib/systemd/system SETTINGS_LOCATION ?= /etc/default install: mkdir -p $(DESTDIR)$(EXPORTER_DIR)/ cp prometheus-foomuuri-exporter $(DESTDIR)$(EXPORTER_DIR)/$(EXPORTER_NAME) mkdir -p $(DESTDIR)$(SYSTEMD_SYSTEM_LOCATION)/ cat prometheus-foomuuri-exporter.service | sed s/prometheus-foomuuri-exporter/$(EXPORTER_NAME)/g > $(DESTDIR)$(SYSTEMD_SYSTEM_LOCATION)/$(EXPORTER_NAME).service mkdir -p $(DESTDIR)$(SETTINGS_LOCATION)/ cp prometheus-foomuuri-exporter.default $(DESTDIR)$(SETTINGS_LOCATION)/$(EXPORTER_NAME) # Install current source to local system's root sysupdate: make install DESTDIR=/ systemctl daemon-reload foomuuri-0.34/prometheus/prometheus-foomuuri-exporter000077500000000000000000000157611526436212200233470ustar00rootroot00000000000000#!/usr/bin/python3 # pylint: disable=invalid-name # pylint: enable=invalid-name """Prometheus exporter for Foomuuri metrics.""" import argparse import json import pathlib import re import subprocess import time from collections import Counter # pylint: disable=import-error from prometheus_client import REGISTRY, start_http_server from prometheus_client.core import CounterMetricFamily, GaugeMetricFamily from prometheus_client.registry import Collector class MonitorCollector(Collector): """Collect Foomuuri Monitor metrics.""" # pylint: disable=too-few-public-methods def __init__(self, args): """Initialize class.""" super().__init__() self._stat_file = pathlib.Path(args.statistics_file) def collect(self): """Return Foomuuri Monitor metrics.""" try: stats = json.loads(self._stat_file.read_text(encoding='utf-8')) except (OSError, ValueError): return # Target/group state g = GaugeMetricFamily( 'foomuuri_monitor_up', 'Target connectivity status.', labels=['type', 'name'], ) for name, value in stats.items(): g.add_metric([value['type'], name], value['state']) yield g # Target packet loss g = GaugeMetricFamily( 'foomuuri_monitor_packet_loss_ratio', 'Average ping packet loss.', labels=['type', 'name'], ) for name, value in stats.items(): if not value['time']: continue loss = sum(1 for item in value['time'] if item is None) g.add_metric([value['type'], name], loss / len(value['time'])) yield g # Target packet ping latency g = GaugeMetricFamily( 'foomuuri_monitor_ping_seconds', 'Average network round trip time.', labels=['type', 'name'], ) for name, value in stats.items(): times = [item / 1000 for item in value['time'] if item is not None] if not times: continue g.add_metric([value['type'], name], sum(times) / len(times)) yield g class RulesetCollector(Collector): """Collect ruleset metrics.""" # pylint: disable=too-few-public-methods def __init__(self, args): """Initialize class.""" super().__init__() self._set_include = re.compile(args.set_include) self._set_exclude = re.compile(args.set_exclude) self._counter_include = re.compile(args.counter_include) self._counter_exclude = re.compile(args.counter_exclude) def collect(self): """Return ruleset metrics.""" try: nftdata = json.loads( subprocess.run( ['nft', '--json', 'list', 'table', 'inet', 'foomuuri'], stdout=subprocess.PIPE, check=False, encoding='utf-8', ).stdout ) except (OSError, ValueError): return # Set size - merge IPv4 and IPv6 to single value sets = Counter() for item in nftdata.get('nftables', {}): if 'set' in item: name = item['set']['name'][:-2] # Without "_4" if self._set_include.search( name ) and not self._set_exclude.search(name): sets.update({name: len(item['set'].get('elem', []))}) g = GaugeMetricFamily( 'foomuuri_set_elements', 'Number of elements in set.', labels=['name'], ) for name, value in sets.items(): g.add_metric([name], value) yield g # Named counters counters = {} for item in nftdata.get('nftables', {}): if 'counter' in item: name = item['counter']['name'] if self._counter_include.search( name ) and not self._counter_exclude.search(name): counters[name] = { 'bytes': item['counter']['bytes'], 'packets': item['counter']['packets'], } g = CounterMetricFamily( 'foomuuri_counter_bytes_total', 'Counter bytes value.', labels=['name'], ) for name, value in counters.items(): g.add_metric([name], value['bytes']) yield g g = CounterMetricFamily( 'foomuuri_counter_packets_total', 'Counter packets value.', labels=['name'], ) for name, value in counters.items(): g.add_metric([name], value['packets']) yield g def main(): """Parse arguments and run.""" # Command line parser parser = argparse.ArgumentParser( description='Export Foomuuri statistics to Prometheus.' ) parser.add_argument( '--address', default='::', help='listen address (default: ::)' ) parser.add_argument( '--port', type=int, default=11041, help='listen port number (default: 11041)', ) parser.add_argument( '--tls-certificate', metavar='FILENAME', help='TLS certificate file name', ) parser.add_argument( '--tls-key', metavar='FILENAME', help='TLS key file name' ) parser.add_argument( '--no-monitor-statistics', action='store_true', help='do not export Foomuuri Monitor statistics', ) parser.add_argument( '--no-ruleset-statistics', action='store_true', help='do not export ruleset statistics', ) parser.add_argument( '--statistics-file', metavar='FILENAME', default='/var/lib/foomuuri/monitor.statistics', help='Foomuuri Monitor statistics file name', ) parser.add_argument( '--set-include', metavar='REGEXP', default='.', # default all help='set names to be included to ruleset size statistics', ) parser.add_argument( '--set-exclude', metavar='REGEXP', default='$^', # default nothing help='set names to be excluded from ruleset size statistics', ) parser.add_argument( '--counter-include', metavar='REGEXP', default='.', help='counter names to be included to ruleset traffic statistics', ) parser.add_argument( '--counter-exclude', metavar='REGEXP', default='$^', help='counter names to be excluded from ruleset traffic statistics', ) args = parser.parse_args() # Register collectors if not args.no_monitor_statistics: REGISTRY.register(MonitorCollector(args)) if not args.no_ruleset_statistics: REGISTRY.register(RulesetCollector(args)) # Run exporter start_http_server( port=args.port, addr=args.address, certfile=args.tls_certificate, keyfile=args.tls_key, ) while True: time.sleep(1) if __name__ == '__main__': main() foomuuri-0.34/prometheus/prometheus-foomuuri-exporter.default000066400000000000000000000000111526436212200247450ustar00rootroot00000000000000OPTIONS= foomuuri-0.34/prometheus/prometheus-foomuuri-exporter.service000066400000000000000000000005041526436212200247700ustar00rootroot00000000000000[Unit] Description=Prometheus exporter for Foomuuri metrics After=foomuuri-monitor.service After=network-online.target [Service] Environment=OPTIONS= EnvironmentFile=/etc/default/prometheus-foomuuri-exporter User=root Restart=on-failure ExecStart=prometheus-foomuuri-exporter $OPTIONS [Install] WantedBy=multi-user.target foomuuri-0.34/src/000077500000000000000000000000001526436212200140775ustar00rootroot00000000000000foomuuri-0.34/src/__init__.py000066400000000000000000000000001526436212200161760ustar00rootroot00000000000000foomuuri-0.34/src/foomuuri000077500000000000000000006703021526436212200157020ustar00rootroot00000000000000#!/usr/bin/python3 # pylint: disable=too-many-lines """Foomuuri - Multizone bidirectional nftables firewall. Copyright 2023-2026, Kim B. Heino, Foobar Oy and contributors. License: GPL-2.0-or-later """ import collections import concurrent.futures import contextlib import dataclasses import datetime as dt import io import ipaddress import itertools import json import os import pathlib import re import select import shlex import signal import socket import subprocess import sys import syslog import time import typing import unicodedata # dbus is optional but highly recommended for dynamic zone/interface changes try: import dbus import dbus.mainloop.glib import dbus.service HAVE_DBUS = True except ImportError: HAVE_DBUS = False # lxml is optional, used by iplist xml and html filters try: import lxml.etree HAVE_LXML = True except ImportError: HAVE_LXML = False # systemd notify support is optional try: import systemd.daemon HAVE_SYSTEMD = True except ImportError: HAVE_SYSTEMD = False VERSION = '0.34' class Validators: """Validators collection.""" @staticmethod def str_zone_name(value: str) -> bool: """Assert value is a valid zone name.""" return all( [value.isascii(), re.fullmatch(r'[a-zA-Z][a-zA-Z_0-9./]*', value)] ) @staticmethod def str_iplist_name(value: str) -> bool: """Assert value is a valid iplist name, including @.""" return all( [ value.isascii(), re.fullmatch(r'@[a-zA-Z_][-a-zA-Z_0-9./]*', value), ] ) @staticmethod def str_interface_name(value: str) -> bool: r"""Assert value is a valid network interface name. Linux accepts also \\ and " characters. Reject them here as they would break JSONs without quoting. """ if any(letter <= ' ' for letter in value): return False # Reject whitespace/control chars as Linux does if '/' in value: return False # Linux rejects this if '\\' in value or '"' in value: return False # Linux accepts these return 1 <= len(value) <= 15 # IFNAMSIZ=16 including NUL @staticmethod def has_elements(value: typing.Union[set, list]) -> bool: """Assert value is non-empty list or set.""" return len(value) >= 1 @staticmethod def int_positive_or_zero(value: int) -> bool: """Assert value is not negative.""" return value >= 0 @staticmethod def int_positive(value: int) -> bool: """Assert value is positive.""" return value > 0 @staticmethod def str_yes_no(value: str) -> bool: """Assert value is either 'yes' or 'no'.""" return value in {'yes', 'no'} @staticmethod def str_comparison_operator(value: str) -> bool: """Assert value is comparison operator.""" return value in {'==', '!=', '<', '>', '<=', '>='} class Converters: # pylint: disable=too-few-public-methods """Type converters collection.""" @staticmethod def canonical_iplist_name(name: str) -> str: """Return iplist name with leading @.""" return name if name.startswith('@') else f'@{name}' @staticmethod def str_yes_no_to_bool(value: str) -> bool: """Convert str 'yes'/'no' to bool True/False, or raise ValueError.""" if value == 'yes': return True if value == 'no': return False raise ValueError @dataclasses.dataclass class TypedConfig: """Storage class with simple attribute type enforcement and validation.""" def __setattr__(self, name: str, value: object) -> None: """Validate and set attribute value.""" field = self._get_field(name) expected_type = self._isinstance_target(field.type) if not isinstance(value, expected_type): raise TypeError( f'{name}: expected value type {field.type}, got {type(value)}' ) validator = field.metadata.get('validate') if validator and not validator(value): raise ValueError(f'{name}: value failed validation') super().__setattr__(name, value) @staticmethod def _isinstance_target(expected_type: typing.Any) -> typing.Any: """Return isinstance() target of a type hint. Convert parameterized hints to their origin and typing.Union hints to tuples of targets. """ type_origin = typing.get_origin(expected_type) if type_origin is typing.Union: return tuple( TypedConfig._isinstance_target(arg) for arg in typing.get_args(expected_type) ) return type_origin or expected_type def get(self, name: str, default: typing.Any = None) -> typing.Any: """Get attribute value.""" return getattr(self, name, default) def __getitem__(self, name: str) -> typing.Any: """Get attribute value via index operator.""" return getattr(self, name) def __setitem__(self, name: str, value: object) -> None: """Set attribute value via index operator.""" self.__setattr__(name, value) def __iter__(self) -> typing.Iterator[str]: """Iterate over dataclass field names.""" # pylint: disable=no-member return iter(self.__dataclass_fields__) def __contains__(self, name: str) -> bool: """Check dataclass field name membership.""" # pylint: disable=no-member return name in self.__dataclass_fields__ def _convert_from_str( self, field: dataclasses.Field, value: str ) -> typing.Any: """Convert string using field type and metadata.""" if not isinstance(value, str): raise TypeError( f'{field.name}: expected value type {str}, got {type(value)}' ) expected_type = self._isinstance_target(field.type) # "Built-in" value converters. Executed when # convert_str is not defined in field metadata. implicit_converters = { int: int, set: lambda v: set(v.split()), list: str.split, pathlib.PosixPath: pathlib.PosixPath, } if converter := ( field.metadata.get('convert_str') or implicit_converters.get(expected_type) ): return converter(value) if isinstance(value, expected_type): return value raise TypeError( f'{field.name}: value conversion to {field.type} not implemented' ) def set_from_str(self, name: str, value: str) -> None: """Set attribute value converted from string.""" field = self._get_field(name) new_value = self._convert_from_str(field, value) setattr(self, name, new_value) def append_from_str(self, name: str, value: str) -> None: """Append attribute value converted from string.""" field = self._get_field(name) new_value = self._convert_from_str(field, value) expected_type = self._isinstance_target(field.type) # "Built-in" implicit appenders. implicit_appenders = { str: lambda value, append: f'{value} {append}', set: lambda value, append: value | append, list: lambda value, append: value + append, } if (appender := implicit_appenders.get(expected_type)) and isinstance( new_value, expected_type ): new_value = appender(self[name], new_value) else: raise TypeError( f'{name}: appending of {field.type} not implemented' ) setattr(self, name, new_value) def _get_field(self, name: str) -> dataclasses.Field: """Get dataclass field for attribute name.""" # pylint: disable=no-member if (field := self.__dataclass_fields__.get(name)) is None: raise AttributeError(f'{name}: unknown attribute') return field @dataclasses.dataclass class Config(TypedConfig): """Parsed foomuuri{} section variables from config files.""" # pylint: disable=too-many-instance-attributes # Basic settings log_rate: str = '1/second burst 3' log_input: str = 'yes' log_output: str = 'yes' log_forward: str = 'yes' log_rpfilter: str = 'yes' log_invalid: str = 'no' log_smurfs: str = 'no' log_prefix: str = '$(szone)-$(dzone) $(statement)' log_level: str = 'level info flags skuid' localhost_zone: str = dataclasses.field( default='localhost', metadata={'validate': Validators.str_zone_name} ) dbus_zone: str = dataclasses.field( default='public', metadata={'validate': Validators.str_zone_name} ) rpfilter: set[str] = dataclasses.field( default_factory=lambda: {'yes'}, metadata={'validate': Validators.has_elements}, ) flowtable: set[str] = dataclasses.field(default_factory=set) counter: set[str] = dataclasses.field( default_factory=lambda: {'no'}, metadata={'validate': Validators.has_elements}, ) permanent_counter: bool = dataclasses.field( default=False, metadata={'convert_str': Converters.str_yes_no_to_bool} ) set_size: int = dataclasses.field( default=65535, metadata={'validate': Validators.int_positive} ) recursion_limit: int = dataclasses.field( default=10000, metadata={'validate': Validators.int_positive} ) priority_offset: int = 5 dbus_firewalld: str = dataclasses.field( default='no', metadata={'validate': Validators.str_yes_no} ) nft_bin: list[str] = dataclasses.field( default_factory=lambda: ['nft'], metadata={ 'convert_str': shlex.split, 'validate': Validators.has_elements, }, ) try_reload_timeout: int = dataclasses.field( default=15, metadata={'validate': Validators.int_positive} ) # Directories and files. Files are relative to state_dir. etc_dir: pathlib.PosixPath = dataclasses.field( default_factory=lambda: pathlib.PosixPath('/etc/foomuuri') ) share_dir: pathlib.PosixPath = dataclasses.field( default_factory=lambda: pathlib.PosixPath('/usr/share/foomuuri') ) state_dir: pathlib.PosixPath = dataclasses.field( default_factory=lambda: pathlib.PosixPath('/var/lib/foomuuri') ) run_dir: pathlib.PosixPath = dataclasses.field( default_factory=lambda: pathlib.PosixPath('/run/foomuuri') ) good_file: str = 'good.fw' next_file: str = 'next.fw' resolve_file: str = 'resolve.fw' # old iplist/resolve iplist_file: str = 'iplist.fw' # old iplist/resolve iplist_manual_file: str = 'iplist-manual.fw' # old iplist/resolve iplist_cache_file: str = 'iplist-cache.json' zone_file: str = 'zone' monitor_statistics_file: str = 'monitor.statistics' # Variable in foomuuri{}, or command line fork: int = 0 # --fork verbose: int = 0 # --verbose syslog: int = 0 # --syslog @dataclasses.dataclass class Internal(TypedConfig): """Internal variables and command line options.""" # pylint: disable=too-many-instance-attributes # Command line command: str = '' # Command to run: "start" etc parameters: list = dataclasses.field( default_factory=list ) # Parameters for command force: int = 0 # --force config_override: dict[str, typing.Union[str, int]] = dataclasses.field( default_factory=dict ) # Other internally used variables # Lograte names and limits: name -> (log statement, rule statement) logrates: dict[str, tuple[str, str]] = dataclasses.field( default_factory=dict ) # List of helpers: (helper, protocol, ports) helpers: list[tuple[str, str, str]] = dataclasses.field( default_factory=list ) permanent_counter_values: dict[str, tuple[int, int]] = dataclasses.field( default_factory=dict ) # Added without default value to prevent uninitialized use keep_going: int = dataclasses.field(init=False) post_start: list[str] = dataclasses.field(init=False) post_stop: list[str] = dataclasses.field(init=False) pre_start: list[str] = dataclasses.field(init=False) pre_stop: list[str] = dataclasses.field(init=False) priority_offset: str = dataclasses.field(init=False) catch_all_interface_zone: str = dataclasses.field(init=False) root_power: bool = dataclasses.field(init=False) # Running as "root"? zonemap_loop_check: dict[tuple[str, str, str, str], 'Rule'] = ( dataclasses.field(init=False) ) # (szone, dzone, new_szone, new_dzone) -> Rule # Url loader # Constants url_chunk_size: int = 1_048_576 url_get_timeout: dict[str, float] = dataclasses.field( default_factory=lambda: {'connect': 3.05, 'read': 3.05} ) # Timeouts (seconds) url_get_retries: dict = dataclasses.field( default_factory=lambda: { 'connect': 3, 'redirect': 3, 'read': 3, 'raise_on_redirect': False, } ) # Retries count and redirect behaviour @dataclasses.dataclass class IPListOptions(TypedConfig): """iplist{} options container.""" # pylint: disable=too-many-instance-attributes dns_timeout: int = dataclasses.field( default=86400, metadata={ 'convert_str': lambda d: parse_duration(d, 0), 'validate': Validators.int_positive, }, ) # 24h dns_refresh: int = dataclasses.field( default=900, metadata={ 'convert_str': lambda d: parse_duration(d, 0), 'validate': Validators.int_positive, }, ) # 15m url_timeout: int = dataclasses.field( default=864000, metadata={ 'convert_str': lambda d: parse_duration(d, 0), 'validate': Validators.int_positive, }, ) # 10d url_refresh: int = dataclasses.field( default=86400, metadata={ 'convert_str': lambda d: parse_duration(d, 0), 'validate': Validators.int_positive, }, ) # 1d element_timeout: int = dataclasses.field( default=0, metadata={ 'convert_str': lambda d: parse_duration(d, -1), 'validate': Validators.int_positive_or_zero, }, ) # disabled url_max_size: int = dataclasses.field( default=33_554_432, metadata={'validate': Validators.int_positive} ) # 32MiB start: bool = dataclasses.field( default=True, metadata={'convert_str': Converters.str_yes_no_to_bool} ) # load on start merge: bool = dataclasses.field( default=True, metadata={'convert_str': Converters.str_yes_no_to_bool} ) # merge dynamic: bool = dataclasses.field( default=False, metadata={'convert_str': Converters.str_yes_no_to_bool} ) # dynamic flag, implies merge=no and no interval overwrite: typing.Optional[bool] = dataclasses.field( default=None, metadata={'convert_str': Converters.str_yes_no_to_bool} ) # overwrite or append source entries, None uses default value missing_ok: typing.Optional[bool] = dataclasses.field( default=None, metadata={'convert_str': Converters.str_yes_no_to_bool} ) # allow empty/failed URL download or DNS resolve def parse_option(self, option: str) -> bool: """Parse and set iplist option value from option. Option is either a a string in option=value format, or specific word. """ shortcuts = { '-start': ('start', False), '-merge': ('merge', False), } if shortcut := shortcuts.get(option): name, value = shortcut self[name] = value return True if len(assignment := option.split('=', maxsplit=1)) == 2: name, value = assignment # Only try setting if name is a valid identifier if name.isidentifier(): self.set_from_str(name, value) return True return False @dataclasses.dataclass(eq=False) class IPList: """Container of iplist options and sources.""" options: IPListOptions = dataclasses.field(default_factory=IPListOptions) sources: list[str] = dataclasses.field(default_factory=list) @dataclasses.dataclass(eq=False) class IPLists: """Mapping of iplist name to IPList.""" _entries: dict[str, IPList] = dataclasses.field(default_factory=dict) def __getitem__(self, name: str) -> IPList: """Get iplist value via index operator.""" return self._entries[name] def __setitem__(self, name: str, value: IPList) -> None: """Set iplist value via index operator.""" self._entries[name] = value def __iter__(self) -> typing.Iterator[str]: """Iterate over iplist names.""" return iter(self._entries) def items(self) -> typing.ItemsView[str, IPList]: """Return iterable of iplist mapping.""" return self._entries.items() def values(self) -> typing.ValuesView[IPList]: """Return iterable of iplist mapping values.""" return self._entries.values() def filter_names_by(self, option: str, required_value: bool) -> set[str]: """Return set of iplist names with required option value.""" return { name for name, value in self._entries.items() if value.options[option] == required_value } class IPListSourceOptions: # pylint: disable=too-few-public-methods """Container of iplist runtime source options.""" timeout: int = sys.maxsize refresh: int = sys.maxsize max_size: int = sys.maxsize overwrite: typing.Optional[bool] = None missing_ok: typing.Optional[bool] = None def update_if_less(self, name: str, new_value: int) -> None: """Update attribute value if new value is less than current.""" setattr(self, name, min(new_value, getattr(self, name))) def set_option( self, name: str, new_value: typing.Optional[bool], source: str, ) -> None: """Set attribute value. Print warning if conflicting value is given. Print a warning if value changes: - True -> False - False -> True - True or False -> None (unset, default value) """ old_value = getattr(self, name) if old_value is not None and old_value != new_value: warning( f'Conflicting iplist source option "{name}" for "{source}"' ) if new_value is not None: # Don't change from True/False to None setattr(self, name, new_value) @dataclasses.dataclass class Rule(TypedConfig): # pylint: disable=too-many-instance-attributes """A single rule parsed from config.""" # Basic rules statement: str = 'accept' cast: str = 'unicast' protocol: typing.Optional[str] = None saddr: typing.Optional[str] = None daddr: typing.Optional[str] = None sport: typing.Optional[str] = None dport: typing.Optional[str] = None iifname: typing.Optional[str] = None oifname: typing.Optional[str] = None mac_saddr: typing.Optional[str] = None mac_daddr: typing.Optional[str] = None # Is this IPv4/6 specific rule? ipv4: bool = False ipv6: bool = False # Rate limits global_rate: typing.Optional[str] = None saddr_rate: typing.Optional[str] = None saddr_rate_mask: typing.Optional[str] = None saddr_rate_name: typing.Optional[str] = None daddr_rate: typing.Optional[str] = None daddr_rate_mask: typing.Optional[str] = None daddr_rate_name: typing.Optional[str] = None saddr_daddr_rate: typing.Optional[str] = None saddr_daddr_rate_mask: typing.Optional[str] = None saddr_daddr_rate_name: typing.Optional[str] = None # User limits uid: typing.Optional[str] = None gid: typing.Optional[str] = None # Zonemap specific rules szone: typing.Optional[str] = None dzone: typing.Optional[str] = None new_szone: typing.Optional[str] = None new_dzone: typing.Optional[str] = None # Misc rules nat_to: typing.Optional[str] = None # snat/dnat to queue: typing.Optional[str] = None # optional queue flags counter: typing.Optional[str] = None helper: typing.Optional[str] = None sipsec: typing.Optional[str] = None dipsec: typing.Optional[str] = None log: typing.Optional[str] = None log_level: typing.Optional[str] = None nft: typing.Optional[str] = None mss: typing.Optional[str] = None template: typing.Optional[str] = None tproxy: typing.Optional[str] = None mark_set: typing.Optional[str] = None mark_match: typing.Optional[str] = None priority_set: typing.Optional[str] = None priority_match: typing.Optional[str] = None iplist_add: typing.Optional[str] = None iplist_update: typing.Optional[str] = None iplist_delete: typing.Optional[str] = None length: typing.Optional[str] = None dscp: typing.Optional[str] = None cgroup: typing.Optional[str] = None ct_status: typing.Optional[str] = None time: typing.Optional[str] = None after_conntrack: bool = True warning: typing.Optional[str] = None # Print warning to user # Internal housekeeping plain: bool = True # Plain "log" or "counter" without anything else fileline: str = dataclasses.field(default='') # For error messages line: list[str] = dataclasses.field(default_factory=list) # Original line only_one: dict[str, str] = dataclasses.field(default_factory=dict) RuleList = list[Rule] ZonePairKey = tuple[str, str] SpecialChainKey = tuple[str, typing.Optional[str], typing.Optional[str]] ZonePairRules = dict[ZonePairKey, RuleList] SpecialChainRules = dict[SpecialChainKey, RuleList] Macros = dict[str, list[str]] # macro name -> expansion values Zones = dict[str, list[str]] # zone name -> interface list mapping CONFIG = Config() INTERNAL = Internal() OUT = [] # Generated nftables ruleset / commands def fail(error: str, fatal: bool = True) -> int: """Exit with error message.""" if CONFIG.verbose >= -1: # Double-quiet will suppress output print(f'Error: {error}', flush=True) if CONFIG.syslog: syslog.syslog(syslog.LOG_ERR, f'Error: {error}') if fatal: sys.exit(1) return 1 def warning(text: str) -> None: """Print warning message.""" if CONFIG.verbose >= 0: print(f'Warning: {text}', flush=True) if CONFIG.syslog: syslog.syslog(syslog.LOG_WARNING, f'Warning: {text}') def verbose(line: str, level: int = 1) -> None: """Print line if --verbose was given in command line.""" if CONFIG.verbose >= level: print(line, flush=True) if CONFIG.syslog: syslog.syslog(syslog.LOG_NOTICE, line) def int_to_human(number: int) -> str: """Format int in human readable way.""" return f'{number:,}' def print_table( data: list[tuple], align: str = '<', header: bool = False, sep: str = ' ', ) -> None: """Simplistic formatted table printer.""" if not data or (header and len(data) == 1): return if not set(align).issubset('^<>'): raise ValueError('Supported alignment: left <, right >, center ^') columns = len(data[0]) width = [ max(len(str(row[column])) for row in data) for column in range(columns) ] if len(align) == 1: align *= columns elif len(align) != columns: raise ValueError('Align value does not match column count') for r, row in enumerate(data): print( sep.join( [ f'{column:{align[c]}{width[c]}}' for c, column in enumerate(row) ] ).rstrip(), ) if header and not r: print(sep.join(['-' * w for w in width])) def out(line: str) -> None: """Add single line to ruleset.""" OUT.append(line) def notify(message: str) -> None: """Send systemd notify message.""" if HAVE_SYSTEMD: try: systemd.daemon.notify(message) except OSError as error: warning(f'Failed to send systemd notify "{message}": {error}') def state_file(key: str) -> pathlib.PosixPath: """Return CONFIG.state_dir / CONFIG.key as a combined PosixPath. All xxx_file variables are relative to state_dir. Values "../filename" and "/full/path/filename" works too. """ return CONFIG.state_dir / CONFIG[key] def join_args(args: list) -> str: """Return joined string of stringified args.""" if isinstance(args, list): return shlex.join(map(str, args)) raise TypeError(f'args type must be a {list}, not a {type(args)}') def syslog_open_or_close() -> None: """Initialize or stop syslogging.""" if CONFIG.syslog: syslog.openlog('foomuuri', syslog.LOG_PID, syslog.LOG_DAEMON) else: syslog.closelog() def run_program_rc( args: list, *, env: typing.Optional[dict[str, str]] = None, stdin: typing.Optional[str] = None, print_output: bool = True, quiet: bool = False, ) -> int: """Run external program and return its errorcode. Print its output.""" if not args: return 0 verbose(join_args(args)) try: proc = subprocess.run( args, check=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf-8', env=env, input=stdin, timeout=60, ) except (OSError, subprocess.TimeoutExpired) as error: fail(f'Failed to run command "{join_args(args)}": {error}', False) return 1 output = proc.stdout.rstrip() if ( output and (proc.returncode or print_output or CONFIG.verbose > 0) and not quiet ): verbose(output, 0) return proc.returncode def run_program_pipe( args: list[str], text_input: typing.Optional[str], check_error: bool = True, ) -> typing.Optional[str]: """Run external program with stdin, return its output.""" if not args: return '' try: proc = subprocess.run( args, check=False, input=text_input, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf-8', timeout=60, ) except (OSError, subprocess.TimeoutExpired) as error: fail(f'Failed to run command "{join_args(args)}": {error}') if proc.returncode and check_error: warning( f'Failed to run command "{join_args(args)}": ' f'return code {proc.returncode}' ) return None return proc.stdout def run_program_shell(args: str, fileline: str) -> str: """Run external program as shell command and return its output. Command failure is fatal. Parameter args must be a string, not list. """ if not args: return '' try: proc = subprocess.run( args, check=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf-8', shell=True, timeout=60, ) except (OSError, subprocess.TimeoutExpired) as error: fail(f'{fileline}Failed to run command "{args}": {error}') if proc.returncode: fail( f'{fileline}Failed to run command "{args}": ' f'return code {proc.returncode}' ) return proc.stdout.rstrip() def nft_command(cmd: str, **kwargs: typing.Any) -> int: """Run "nft cmd", wrapper to run_program_rc().""" return run_program_rc(CONFIG.nft_bin + [cmd], **kwargs) def nft_json(cmd: str) -> typing.Optional[dict]: """Run "nft --json cmd", return its output as json, ignore errors.""" if not cmd: return {} args = CONFIG.nft_bin + ['--json', cmd] verbose(join_args(args), 2) try: proc = subprocess.run( args, check=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf-8', timeout=60, ) except (OSError, subprocess.TimeoutExpired): return None if proc.returncode: if CONFIG.verbose >= 1 or proc.stdout.startswith('JSON support'): warning( f'Failed to run command "{join_args(args)}": ' f'{proc.stdout.strip()}' ) return None try: verbose(proc.stdout, 2) return json.loads(proc.stdout) except json.decoder.JSONDecodeError: return None def daemonize() -> None: """Fork as a background daemon. Silently ignore errors.""" # Enabled in config? if not CONFIG.fork: return CONFIG.fork = 0 # Fork only once! # Fork #1 try: pid = os.fork() if pid > 0: sys.exit(0) except OSError: return # Run in a new session. Don't do "os.chdir('/')" so that relative paths # work in development. os.umask(0o022) os.setsid() # Fork #2 try: pid = os.fork() if pid > 0: sys.exit(0) except OSError: return # Redirect stdin/out/err for stream in (sys.stdin, sys.stdout, sys.stderr): devnull = os.open(os.devnull, os.O_RDWR) os.dup2(devnull, stream.fileno()) def shell_expansion(content: str, fileline: str) -> str: """Expand $(shell command) in configuration file. This is the first expansion done. Failure in command is fatal. """ while '$(shell ' in content: prefix, postfix = content.split('$(shell ', 1) if ')' not in postfix: postfix = postfix.splitlines()[0] fail(f'{fileline}"$(shell" without ")" in command: {postfix}') shell, postfix = postfix.split(')', 1) content = f'{prefix}{run_program_shell(shell, fileline)}{postfix}' return content def find_config_files( basedir: pathlib.PosixPath, mask: str ) -> list[pathlib.PosixPath]: """Find all config files in basedir, ignoring hidden and backups.""" files = sorted(basedir.rglob(mask)) return [item for item in files if not item.name.startswith(('.', '#'))] def _validate_section_format( current_section_id: typing.Optional[str], tokens: list[str], fileline: str, ) -> str: """Config file section validation helper. Used by read_config() internally. """ if current_section_id: fail( f'{fileline}New "{" ".join(tokens)}" while section ' f'"{current_section_id}" is still open' ) if tokens.count('{') > 1: fail(f'{fileline}One opening brace allowed: {" ".join(tokens)}') if tokens.count('}') == 0: if tokens[-1] != '{': fail(f'{fileline}Closing brace missing: {" ".join(tokens)}') elif tokens.count('}') == 1: if tokens[-1] != '}': fail(f'{fileline}Closing brace must be last: {" ".join(tokens)}') else: fail(f'{fileline}One closing brace allowed: {" ".join(tokens)}') section_name = tokens[0] section_params = tokens[1 : tokens.index('{')] section_id = ' '.join([section_name] + section_params) if section_name.startswith('_'): # _name is protected fail(f'{fileline}Unknown section: {section_id}') if section_name in { 'template', 'target', 'group', }: if len(section_params) != 1: fail( f'{fileline}Section "{section_name}" must have single ' f'word name: {" ".join(tokens)}' ) elif section_params and section_name not in { 'snat', 'dnat', 'prerouting', 'postrouting', 'forward', 'input', 'output', }: fail( f'{fileline}Section "{section_name}" does not take ' f'parameters: {" ".join(tokens)}' ) return section_id def read_config(require_etc_config: bool) -> dict: """Read all config files to config dict: section -> lines[]. Files are read in alphabetical order, ignoring backup and hidden files. """ # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-statements # Find all config files share_config = find_config_files(CONFIG.share_dir, '*.conf') etc_config = find_config_files(CONFIG.etc_dir, '*.conf') if not etc_config and require_etc_config: fail( f'No configuration files "{CONFIG.etc_dir}/*.conf" found\n' '\n' 'See https://foomuuri.foobar.fi/latest/example/host-firewall/ ' 'for example\n' 'configuration.' ) # These characters form a single word in shlex wordchars = ''.join( char for code in range(33, 256) if (char := chr(code)) not in {'"', "'", ';', '{', '}'} ) # Read all config files config = {} # Final config dict section_line = {} # section_id -> filename_line for error messages section_id = None # Current section id (name + parameters) for filename in share_config + etc_config: try: content = filename.read_text(encoding='utf-8') except PermissionError as error: fail(f"File {filename}: Can't read: {error}") # Expand $(shell in configuration file. Do this for whole file instead # of single line so that command can return multiple lines. content = shell_expansion(content, f'File {filename}: ') # Parse single config file content continuation = '' for linenumber, line in enumerate(content.splitlines()): if line == '# foomuuri: not-conf': break # sysctl's 50-foomuuri.conf is not my config, undoc # Combine lines if there is \ at end of line if line.endswith('\\'): continuation += line[:-1] + ' ' continue line = continuation + line continuation = '' # Parse single line to list of words. Keep " as is, it can be # used to avoid macro expansion. fileline = f'File {filename} line {linenumber + 1}: ' try: lexer = shlex.shlex(line) lexer.wordchars = wordchars tokens = list(lexer) except ValueError as error: fail(f"{fileline}Can't parse line: {error}") if not tokens: continue # "}" is end of section if len(tokens) == 1 and tokens[0] == '}': if not section_id: fail(f'{fileline}Extra "}}"') section_id = None # Section start: # - "foo [parameters] {" # Or complete inline section: # - "bar [parameters] { baz }" elif '{' in tokens and (open_idx := tokens.index('{')) >= 1: section_id = _validate_section_format( section_id, tokens, fileline ) if section_id not in config: config[section_id] = [] section_line[section_id] = fileline if tokens[-1] == '}': # inline section if inline_content := tokens[open_idx + 1 : -1]: config[section_id].append((fileline, inline_content)) # ty: ignore section_id = None # "foo" which is not inside section elif not section_id: fail(f'{fileline}Unknown line: {" ".join(tokens)}') # "foo" inside section else: config[section_id].append((fileline, tokens)) # ty: ignore # End of file checks if continuation: fail(f'File {filename}: Continuation "\\" at end of file') if section_id: fail( f'File {filename}: Section "{section_id}" is missing "}}" at ' f'end of file' ) # Include section_id -> filename_line to config for error messages config['_section_line'] = section_line return config def parse_config_templates( config: dict, macros: Macros, macroline: dict[str, str] ) -> None: """Parse "template foo { ... }" rules and convert them to macros.""" names = [item for item in config if item.startswith('template ')] for name in names: lines = config.pop(name) fileline = config['_section_line'][name] if not lines: fail(f'{fileline}Template "{name[9:]}" is empty') macro_name = f'_template_{name[9:]}' macroline[macro_name] = fileline macros[macro_name] = [] for line in lines: if macros[macro_name]: macros[macro_name].append(';') macros[macro_name].extend(line[1]) def parse_config_macros(config: dict) -> Macros: """Parse macro{} from config. Recursively expand macro in macro{}.""" # pylint: disable=too-many-branches # Parse macro{} to dict macros = {} macroline = {} parse_config_templates(config, macros, macroline) for fileline, macro in config.pop('macro', []): key = macro[0] value = macro[1:] if not value: fail(f'{fileline}Macro "{key}" does not have value') macroline[key] = fileline if value[0] == '+': # append macros[key] = macros.get(key, []) + value[1:] else: if INTERNAL.command == 'check' and macros.get(key): warning( f'{fileline}Overwriting macro "{key}" ' f'with value "{" ".join(value)}"' ) macros[key] = value # overwrite # Expand macro in macro{}. Keep going as long as there was some expansion # done. This can happen if macro group contains multiple macros. # pylint: disable=too-many-nested-blocks # ruff: ignore[too-many-nested-blocks] while True: found = False # Expand each macro for macro, value in macros.items(): new_value = [] # Macro value can have ";". Check each group separately as it can # have prefix and suffix which must be copied to each expansion. for is_split, group in itertools.groupby( value, lambda spl: spl == ';' ): if is_split: new_value.append(';') continue # Check each item in group group = list(group) for index, word in enumerate(group): if word in macros: # Macro expansion is found! Expansion can have # ";", so add each part separately. for is_split2, group2 in itertools.groupby( macros[word], lambda spl: spl == ';' ): if is_split2: new_value.append(';') else: new_value.extend( group[:index] # prefix in original + list(group2) # expansion + group[index + 1 :] # suffix in original ) found = True break else: # No expansion in this group, add it as is new_value.extend(group) macros[macro] = new_value if macro in new_value: fail( f'{macroline[macro]}Macro "{macro}" expands to ' f'itself: {" ".join(value)} => {" ".join(new_value)}' ) if not found: # No new expansion was done return macros def macro_isdigit(word: str, separator: str) -> bool: """Check if word contains number after separator.""" if word.count(separator) != 1: return False return word.split(separator)[1].isdigit() def change_template_to_macro_in_rule( fileline: str, line: list[str], macros: Macros ) -> None: """Change "template foo" to "_template_foo".""" pos = 0 while pos < len(line) - 1: if line[pos] == 'template': del line[pos] macro_name = f'_template_{line[pos]}' if macro_name not in macros: fail(f'{fileline}Unknown template name: {line[pos]}') line[pos] = macro_name pos += 1 def expand_single_line( fileline: str, line: list[str], macros: Macros, ) -> typing.Optional[list[tuple[str, list[str]]]]: """Expand first macro in line. Repeat call to expand all. Expansion can return multiple lines. Returns None if no expansion was done. """ # Change "template foo" to macro call change_template_to_macro_in_rule(fileline, line, macros) # Iterate words in line for pos, word in enumerate(line): # Cleanup "-macro", "macro/24", "[macro]:123" and "macro:123" # to prefix/macro/suffix parts word_prefix = word_suffix = '' if word[0] == '-' and len(word) > 1: # Negative IP address word = word[1:] word_prefix = '-' if macro_isdigit(word, '/'): # Netmask word, word_suffix = word.split('/') word_suffix = f'/{word_suffix}' if ( word[0] == '[' and macro_isdigit(word, ']:') and not word_prefix and not word_suffix ): # [IPv6]:port word, word_suffix = word[1:].split(']:') word_prefix = '[' word_suffix = f']:{word_suffix}' if macro_isdigit(word, ':') and not word_suffix: # IPv4:port word, word_suffix = word.split(':') word_suffix = f':{word_suffix}' # Check cleaned value if word not in macros: continue # Found, add prefix/suffix to all macro's value mvalue = [] for item in macros[word]: if item == ';': mvalue.append(item) else: mvalue.append(f'{word_prefix}{item}{word_suffix}') # Expand mvalue to line and return list of expanded lines prefix = line[:pos] suffix = line[pos + 1 :] return [ (fileline, prefix + list(group) + suffix) for is_split, group in itertools.groupby( mvalue, lambda spl: spl == ';' ) if not is_split ] return None def expand_macros(config: dict) -> None: """Expand all macros in all config sections.""" macros = parse_config_macros(config) for section, orig_lines in config.items(): if section.startswith('_'): continue # Internally used section, skip if section in {'foomuuri', 'zone'}: # Don't expand macros in these sections, print warning instead. # This is for security reasons. Bad things will happen if # there is "xyz" in these sections and macro "xyz" is added # to default macros config. for line in orig_lines: for item in line[1]: if item in macros: warning( f'{line[0]}Macro "{item}" is not expanded in ' f'"{section}" section: {" ".join(line[1])}' ) continue new_lines = [] recursion_limit = collections.Counter() while orig_lines: # Get next line and expand macros there fileline, line = orig_lines.pop(0) expanded = expand_single_line(fileline, line, macros) # Repeat call if some expansion was done if expanded: orig_lines = expanded + orig_lines else: # Not found, go to next line new_lines.append((fileline, line)) # Check for expansion loop recursion_limit.update([fileline]) if recursion_limit[fileline] > CONFIG.recursion_limit: fail( f'{fileline}Possible macro or template loop ' f'in "{section}": {" ".join(line[:10])} ...' ) config[section] = new_lines def remove_quotes(config: dict) -> None: """Change "foo" and 'foo' to foo in config files. This function is called after macro expansion so that "ssh" is changed to ssh, not tcp 22. """ for section, lines in config.items(): if section.startswith('_'): continue for _dummy_fileline, line in lines: for index, item in enumerate(line): if len(item) >= 2 and ( (item[0] == item[-1] == '"') or (item[0] == item[-1] == "'") ): line[index] = item[1:-1] def parse_config_foomuuri(config: dict) -> None: """Parse foomuuri{} from config to CONFIG{}.""" # pylint: disable=too-many-branches for fileline, line in config.pop('foomuuri', []): name = line[0] value = ' '.join(line[1:]) if name == 'try-reload_timeout': # backward compatibility name = 'try_reload_timeout' warning( f'{fileline}foomuuri{{}} option "{line[0]}" is obsolete, ' f'use "{name}" instead' ) if name not in CONFIG: fail(f'{fileline}Unknown foomuuri{{}} option: {" ".join(line)}') try: if value.startswith('+ '): # append CONFIG.append_from_str(name, value[2:]) else: CONFIG.set_from_str(name, value) # overwrite except (ValueError, TypeError): fail( f'{fileline}Invalid foomuuri{{}} option value:' f' {" ".join(line)}' ) # Overrides from CLI arguments for name, value in INTERNAL.config_override.items(): CONFIG.set_from_str(name, str(value)) # Reinitialize syslogging, foomuuri{} may have a different syslog setting syslog_open_or_close() # Convert chain priority offset to nft if CONFIG.priority_offset == 0: INTERNAL.priority_offset = '' elif CONFIG.priority_offset > 0: INTERNAL.priority_offset = f' + {CONFIG.priority_offset}' else: INTERNAL.priority_offset = f' - {-CONFIG.priority_offset}' # Add "packets" to log rates for key in CONFIG: if key.startswith('log_') and re.fullmatch( r'\d+/(second|minute|hour) burst \d+', CONFIG[key] ): CONFIG[key] += ' packets' def parse_config_zones(config: dict) -> Zones: """Parse zone{} from config.""" zones = {} INTERNAL.catch_all_interface_zone = '' for fileline, line in config.pop('zone', []): if not Validators.str_zone_name(line[0]): fail(f'{fileline}Invalid zone name: {" ".join(line)}') if line[0] in zones: fail(f'{fileline}Zone is already defined: {line[0]}') interface = [item for item in line[1:] if item != '*'] zones[line[0]] = interface if '*' in line[1:]: if INTERNAL.catch_all_interface_zone: fail( f'{fileline}Catch all interface "*" is already assigned ' f'to zone "{INTERNAL.catch_all_interface_zone}": ' f'{" ".join(line)}' ) INTERNAL.catch_all_interface_zone = line[0] return zones def parse_config_zonemap(config: dict, zones: Zones) -> RuleList: """Parse zonemap{} rules from config.""" zonemap = [] for fileline, line in config.pop('zonemap', []): rule = parse_rule_line((fileline, line)) if not rule.new_dzone and not rule.new_szone: fail( f'{fileline}Zonemap without "new_dzone" or "new_szone" ' f'has no effect: {" ".join(line)}' ) for key in ('new_szone', 'new_dzone'): if ' ' in (rule[key] or ''): fail( f'{fileline}Zonemap "{key}" must be single zone: ' f'{" ".join(line)}' ) for key in ('szone', 'dzone', 'new_szone', 'new_dzone'): for value in (rule[key] or '').split(): if value not in zones: fail( f'{fileline}Zonemap "{key}" has unknown zone ' f'"{value}": {" ".join(line)}' ) zonemap.append(rule) INTERNAL.zonemap_loop_check = {} return zonemap def parse_config_special_chains(config: dict) -> SpecialChainRules: """Parse snat{}, dnat{}, prerouting{} etc. rules from config.""" chain_rules = { # output+mangle is needed for mark restore in output_special_chains() ('output', 'route', 'mangle' + INTERNAL.priority_offset): [], } for prefix, def_type, def_priority in ( ('snat', 'nat', 'srcnat'), ('dnat', 'nat', 'dstnat'), ('prerouting', 'filter', 'mangle'), ('postrouting', 'filter', 'mangle'), ('forward', 'filter', 'mangle'), ('input', 'filter', 'mangle'), ('output', 'route', 'mangle'), ('invalid', None, None), ('rpfilter', None, None), ('smurfs', None, None), ): for section in list(config): if not (section == prefix or section.startswith(f'{prefix} ')): continue fileline = config['_section_line'][section] items = section.split(' ', 2) if len(items) == 2: fail( f'{fileline}Invalid section: type or priority missing: ' f'{section}' ) if len(items) == 1: ftype = def_type priority = None if def_priority: priority = def_priority + INTERNAL.priority_offset else: ftype = items[1] priority = items[2] if ftype not in {'filter', 'nat', 'route'}: fail(f'{fileline}Invalid section type: {section}') lines = config.pop(section) chain_rules[prefix, ftype, priority] = [ parse_rule_line(line) for line in lines ] return chain_rules def parse_config_hook(config: dict) -> None: """Parse hook{} from config.""" for fileline, line in config.pop('hook', []): if line[0] not in { 'pre_start', 'post_start', 'pre_stop', 'post_stop', }: fail(f'{fileline}Unknown hook: {" ".join(line)}') INTERNAL[line[0]] = line[1:] def minimal_config(require_etc_config: bool = True) -> dict: """Read and parse minimal config.""" config = read_config(require_etc_config) expand_macros(config) remove_quotes(config) parse_config_foomuuri(config) return config def get_ip_family(value: str, *, strict: bool = False) -> int: """Detect if value is an IPv4 or IPv6 address, network or interval. Return 4, 6, or 0 if not detected. """ # pylint: disable=too-many-return-statements # Reject "fe80::1%foo};flush" in strict mode if strict and '%' in value: return 0 # -IP if not strict and value.startswith('-'): value = value[1:] # Negative is handled in single_or_set() # IPv6/-mask if not strict and value.count('/-') == 1: addr, maskstr = value.split('/-') with contextlib.suppress(ValueError): mask = int(maskstr) if 0 <= mask <= 128 and ipaddress.IPv6Address(addr): return 6 return 0 # Python's ipaddress library doesn't handle [IPv6] notation. # Strip [] before validating the address. nft handles [] fine, it will # strip them. if value.startswith('[') and ':' in value: if value.endswith(']'): # [IPv6] value = value[1:-1] elif ']/' in value: # [IPv6]/56 value = value[1:].replace(']/', '/') # IP/mask (network), IP (address) with contextlib.suppress(ValueError): net = ipaddress.ip_network(value, strict=False) return 4 if isinstance(net, ipaddress.IPv4Network) else 6 # IP-IP (interval) if value.count('-') == 1: addr_from, addr_to = value.split('-') with contextlib.suppress(ValueError): if ipaddress.IPv4Address(addr_from) <= ipaddress.IPv4Address( addr_to ): return 4 with contextlib.suppress(ValueError): if ipaddress.IPv6Address(addr_from) <= ipaddress.IPv6Address( addr_to ): return 6 return 0 def is_port(value: str, rule: Rule) -> bool: """Check if value is port number ("1", "1-2", "-3") or ICMP type. This function is used in parse_rule_line when parsing "tcp 22" config line shortcut. Comma notation "1,2" is accepted with warning message. It is undoc for backward compability and will result an error in some future version. """ # Strip "-" from negative format: "-3", "-1-2", "-echo-request" if value.startswith('-') and len(value) > 1: value = value[1:] # Verify each value for item in value.split(','): if rule.protocol in {'icmp', 'icmpv6'} and ( # See: "nft describe icmp type; nft describe icmpv6 type" item == 'redirect' or re.fullmatch(r'[a-z2]{2,}-[-a-z]{5,}', item) ): continue for number in item.split('-'): if not number.isnumeric(): return False # Warning if there is "," instead of " " if ',' in value: warning( f'{rule.fileline}Use "{value.replace(",", " ")}" when listing ' f'multiple ports: {" ".join(rule.line)}' ) return True def rule_item_only_one(rule: Rule, keyword: str, value: str) -> None: """Verify that keyword is set only once, or set to same value.""" old = rule.only_one.get(keyword, value) if old != value: fail( f"{rule.fileline}Rule's {keyword} is already set to " f'"{old}": {" ".join(rule.line)}' ) rule.only_one[keyword] = value def verify_rule_sanity(rule: Rule, fileline: str) -> None: """Do some basic verify that single rule is valid.""" # pylint: disable=too-many-branches for key in rule: value = rule[key] if ( value == '' # ruff: ignore[compare-to-empty-string] -- it can be None too and key not in {'nat_to', 'queue', 'counter', 'log', 'fileline', 'line'} ): fail(f'{fileline}"{key}" without value is not valid') for key in ( 'saddr_rate_name', 'daddr_rate_name', 'saddr_daddr_rate_name', 'helper', 'counter', 'mss', ): value = rule[key] or '' if ' ' in value: fail(f'{fileline}"{key}" must be single word: {value}') for key in ('iplist_add', 'iplist_update', 'iplist_delete'): value = rule[key] if value is None: continue if value.count(' ') != 1 or not value.startswith( ('saddr @', 'daddr @') ): fail( f'{fileline}"{key}" must be addrtype and iplist name: {value}' ) for key in ('global_rate', 'saddr_rate', 'daddr_rate', 'saddr_daddr_rate'): if not rule[key]: continue # Limit by packets: # # 3/second # 3/second burst 5 # 3/second burst 5 packets # over 3/second # over 3/second burst 5 # over 3/second burst 5 packets # # Limit by bytes: # # 10 mbytes/second # 10 mbytes/second burst 12000 kbytes # over 10 mbytes/second # over 10 mbytes/second burst 12000 kbytes # # Limit by connection count # # ct count 8 # ct count over 8 if re.fullmatch( r'(over )?\d+/(second|minute|hour) burst \d+', rule[key] ): rule[key] += ' packets' # Add missing "packets" if not ( re.fullmatch( r'(over )?' r'\d+/(second|minute|hour)' r'( burst \d+ packets)?', rule[key], ) or re.fullmatch( r'(over )?' r'\d+ [km]?bytes/(second|minute|hour)' r'( burst \d+ [km]?bytes)?', rule[key], ) or re.fullmatch(r'ct count (over )?\d+', rule[key]) ): fail(f'{fileline}Invalid "{key}" value: {rule[key]}') for basic, extra in ( ('protocol', 'sport'), ('protocol', 'dport'), ('protocol', 'helper'), ('dport', 'helper'), ('saddr_rate', 'saddr_rate_name'), ('saddr_rate', 'saddr_rate_mask'), ('daddr_rate', 'daddr_rate_name'), ('daddr_rate', 'daddr_rate_mask'), ('saddr_daddr_rate', 'saddr_daddr_rate_name'), ('saddr_daddr_rate', 'saddr_daddr_rate_mask'), ): if rule[extra] and not rule[basic]: fail(f'{fileline}"{extra}" without "{basic}" is not valid') if not rule.nat_to and rule.statement in { 'snat', 'dnat', 'snat_prefix', 'dnat_prefix', }: fail(f'{fileline}"{rule.statement}" without address is not valid') if rule.ct_status and rule.ct_status not in { 'expected', 'seen-reply', 'assured', 'confirmed', 'snat', 'dnat', 'dying', }: fail(f'{fileline}"Invalid "ct_status" value: {rule.ct_status}') def parse_rule_line(fileline_line: tuple[str, list[str]]) -> Rule: """Parse single config section line to Rule class. This parser is quite relaxed. Words can be in almost any order. For example, all following entries are equal: tcp 22 log <- preferred tcp 22 accept log accept tcp 22 log log tcp accept 22 """ # pylint: disable=too-many-branches # pylint: disable=too-many-statements fileline, line = fileline_line rule = Rule(fileline=fileline, line=line) keyword = None for item in line: if item == ';': fail( f'{fileline}";" is not supported in rule, split it to ' f'separate lines: {" ".join(line)}' ) # "tcp 22" is shortcut for "tcp dport 22" # "tcp < 1000" is shortcut for "tcp dport < 1000" if ( not keyword and rule.protocol and ( Validators.str_comparison_operator(item) or is_port(item, rule) ) ): keyword = 'dport' rule.plain = False # Version 0.29 deprecates 'to' after 'snat' statement, ignore it if keyword == 'nat_to' and not rule.nat_to and item == 'to': warning( f'{fileline}Obsolete keyword "to" after "snat/dnat": ' f'{" ".join(line)}' ) continue # First item after start keyword is always a parameter for it, except # for "log" or "counter". Log will have good default value if not # defined. Counter without parameter will create anonymous counter. if keyword and not rule[keyword] and keyword not in {'log', 'counter'}: rule[keyword] = item if keyword == 'protocol': # Single word only keyword = None # Non-start keywords elif item in { 'accept', 'drop', 'return', 'continue', 'masquerade', 'notrack', 'nftrace', }: rule_item_only_one(rule, 'statement', item) rule.statement = item rule.plain = False keyword = None elif item == 'reject': rule_item_only_one(rule, 'statement', item) rule.statement = 'reject with icmpx admin-prohibited' rule.plain = False keyword = None elif item in {'multicast', 'broadcast'}: rule_item_only_one(rule, 'cast', item) rule.cast = item rule.plain = False keyword = None elif item in {'tcp', 'udp', 'icmp', 'icmpv6', 'igmp', 'esp'}: # "igmp" and "esp" are for backward compability (v0.21), undoc rule_item_only_one(rule, 'protocol', item) rule.protocol = item rule.plain = False keyword = None elif item in {'ipv4', 'ipv6'}: rule[item] = True rule.plain = False keyword = None elif item in {'sipsec', 'dipsec'}: rule[item] = 'exists' rule.plain = False keyword = None elif item in {'-sipsec', '-dipsec'}: rule[item[1:]] = 'missing' rule.plain = False keyword = None elif item in {'conntrack', '-conntrack'}: rule_item_only_one(rule, 'conntrack', item) rule.after_conntrack = item == 'conntrack' rule.plain = False keyword = None # Start keywords elif item in {'snat', 'dnat', 'snat_prefix', 'dnat_prefix'}: rule_item_only_one(rule, 'statement', item) rule.statement = item rule.plain = False keyword = 'nat_to' rule.nat_to = rule.nat_to or '' elif item in { 'protocol', 'saddr', 'daddr', 'sport', 'dport', 'iifname', 'oifname', 'mac_saddr', 'mac_daddr', 'global_rate', 'saddr_rate', 'saddr_rate_mask', 'saddr_rate_name', 'daddr_rate', 'daddr_rate_mask', 'daddr_rate_name', 'saddr_daddr_rate', 'saddr_daddr_rate_mask', 'saddr_daddr_rate_name', 'uid', 'gid', 'szone', 'dzone', 'new_szone', 'new_dzone', 'counter', 'helper', 'log', 'log_level', 'nft', 'mss', 'template', 'queue', 'tproxy', 'mark_set', 'mark_match', 'priority_set', 'priority_match', 'iplist_add', 'iplist_update', 'iplist_delete', 'length', 'dscp', 'cgroup', 'ct_status', 'time', 'warning', # undoc, used in default.services.conf }: keyword = item rule[keyword] = rule[keyword] or '' if item == 'queue': # statement and start keyword rule_item_only_one(rule, 'statement', item) rule.statement = item if item == 'mss': # Conflicts with statement rule_item_only_one(rule, 'statement', item) if item not in {'counter', 'log', 'log_level'}: rule.plain = False # More parameters for keyword elif keyword: if rule[keyword]: rule[keyword] += ' ' rule[keyword] += item # Unknown word after non-start keyword else: fail(f"{fileline}Can't parse line: {' '.join(line)}") # Use no-op statement "continue" for plain "log" or "counter" rule, # everything else defaults to "accept". Also mark them as -conntrack # so that they really log/count everything. if rule.plain: rule.statement = 'continue' rule.after_conntrack = False verify_rule_sanity(rule, fileline) # Print warning, used mostly in old macros if rule.warning: warning(f'{fileline}{rule.warning}') return rule def parse_config_rules(config: dict) -> ZonePairRules: """Parse "zone-zone" rules from config. All other sections must be already parsed and removed from config. """ rules = {} for section, lines in config.items(): if section.startswith('_') or section in {'resolve', 'iplist'}: continue try: szone, dzone = section.split('-') except ValueError: fail( f'{config["_section_line"][section]}Unknown section: {section}' ) rules[szone, dzone] = [parse_rule_line(line) for line in lines] # Allow multicast + broadcast if last rule is "accept". # These can be added to the end as they are processed in # correct order in output_zone(). if rules[szone, dzone] and rules[szone, dzone][-1].line == ['accept']: rules[szone, dzone].extend( [ parse_rule_line(('', ['multicast'])), parse_rule_line(('', ['broadcast'])), ] ) return rules def filter_any_zonelist(rule: Rule, srcdst: str) -> tuple[bool, list[str]]: """Parse rule[szone] list and return pos/neg boolean + zonelist.""" if not rule[srcdst]: return False, [] # "not in empty list" == everything inside = True zonelist = [] for item in rule[srcdst].split(): if item.startswith('-'): if inside and zonelist: fail( f'{rule.fileline}Can\'t mix "+" and "-" items: ' f'{rule[srcdst]}' ) inside = False zonelist.append(item[1:]) else: if not inside: fail( f'{rule.fileline}Can\'t mix "+" and "-" items: ' f'{rule[srcdst]}' ) zonelist.append(item) return inside, zonelist def insert_single_any( any_rules: RuleList, rules: ZonePairRules, szone: str, dzone: str ) -> None: """Insert single any_rules to rules[(szone, dzone)].""" if szone == dzone == CONFIG.localhost_zone: return # Don't insert to localhost-localhost # Filter out "szone -public" when adding to zone "public-xxx" filtered = [] for rule in any_rules: inside, zonelist = filter_any_zonelist(rule, 'szone') if (szone in zonelist) != inside: continue inside, zonelist = filter_any_zonelist(rule, 'dzone') if (dzone in zonelist) != inside: continue filtered.append(rule) # Insert filtered rules to beginning if filtered: rules[szone, dzone] = filtered + rules.get((szone, dzone), []) def insert_any_zones(zones: Zones, rules: ZonePairRules) -> None: """Insert "any-any", "any-zone" and "zone-any" rules to "zone-zone" rules. These are inserted to beginning of zone-zone rules. """ for zone in zones: any_rules = rules.pop((zone, 'any'), []) # zone-any for dzone in zones: insert_single_any(any_rules, rules, zone, dzone) for zone in zones: any_rules = rules.pop(('any', zone), []) # any-zone for szone in zones: insert_single_any(any_rules, rules, szone, zone) any_rules = rules.pop(('any', 'any'), []) # any-any for szone in zones: for dzone in zones: insert_single_any(any_rules, rules, szone, dzone) def verify_config(config: dict, zones: Zones, rules: ZonePairRules) -> None: """Verify config data.""" if not zones: fail('No zones defined in section zone{}') localhost = CONFIG.localhost_zone if localhost not in zones: zones[localhost] = [] fail( f'{config["_section_line"]["zone"]}Zone "{localhost}" ' f'is missing from zone{{}}, adding it', fatal=False, ) if zones[localhost]: fail( f'{config["_section_line"]["zone"]}Zone "{localhost}" has ' f'interfaces "{" ".join(zones[localhost])}", ' f'it must be empty' ) if CONFIG.dbus_zone not in zones: warning( f'Config option dbus_zone value ' f'"{CONFIG.dbus_zone}" is missing from zone{{}}' ) # All zone-zone pairs must be known for szone, dzone in rules: if szone not in zones or dzone not in zones: fileline = config['_section_line'][f'{szone}-{dzone}'] fail(f'{fileline}Unknown zone-zone: {szone}-{dzone}') # Make sure all zone-zone pairs are defined. They are needed for # "ct established" return packets and for dynamic interface-to-zone # binding via D-Bus. # Add final rule to all zone-zone pairs, even if there already is # one. It will be optimized out later. for szone in zones: for dzone in zones: if (szone, dzone) not in rules: rules[szone, dzone] = [] if szone == dzone == localhost: rule = ['accept'] # localhost-localhost is accept elif szone == localhost: rule = ['reject', 'log'] # localhost-foo is reject else: rule = ['drop', 'log'] # everything else is drop rules[szone, dzone].append(parse_rule_line(('', rule))) def output_rate_names(rules: ZonePairRules) -> None: """Output empty saddr_rate sets to ruleset.""" counter = 1 already_added = set() for rulelist in rules.values(): for rule in rulelist: for rate in ('saddr_rate', 'daddr_rate', 'saddr_daddr_rate'): if not rule[rate]: continue # Rule with rate found. It can be pre-named or anonymous. setname = rule[f'{rate}_name'] if setname in already_added: continue # Pre-named and already added if not setname: # Anonymous - invent a name for it setname = rule[f'{rate}_name'] = f'_rate_set_{counter}' counter += 1 already_added.add(setname) # Output empty sets for IPv4 and IPv6. These will have # one minute timeout, unless rate is specified per hour. for ipv in (4, 6): out(f'set {setname}_{ipv} {{') if rate == 'saddr_daddr_rate': out(f'type ipv{ipv}_addr . ipv{ipv}_addr') else: out(f'type ipv{ipv}_addr') out(f'size {CONFIG.set_size}') if rule[rate].startswith('ct '): out('flags dynamic') elif 'hour' in rule[rate]: out('flags dynamic,timeout') out('timeout 1h') out('gc-interval 5m') # Default is timeout/4 else: out('flags dynamic,timeout') out('timeout 1m') out('}') def suffix_mask(value: str, compare: str) -> str: """Convert suffix mask "IP/-mask" to nft.""" if not compare: compare = '== ' addr, mask = value.split('/-') mask = f'{pow(2, int(mask)) - 1:x}' # As long hex splitted = '' # Convert to ":1234" parts while mask: splitted = f':{mask[-4:]}{splitted}' mask = mask[:-4] return f'& :{splitted} {compare}{addr}' # Quote value in single_or_set() if this regexp matches. QUOTE_VALUE = re.compile(r'[a-zA-Z ]') def single_or_set( data: typing.Union[str, list, set], fileline: str = '', quote: bool = False, ) -> str: """Convert data to nft string containing single item or set.""" # Convert to list if isinstance(data, str): values = data.split() elif isinstance(data, list): values = data else: values = list(data) # Handle equal / not equal / greater than / etc comparison operator compare = '' if len(values) > 1 and Validators.str_comparison_operator(values[0]): # Comparison operator as first item in list compare = '' if values[0] == '==' else f'{values[0]} ' values = values[1:] elif any(value.startswith('-') for value in values): # List of negative numbers "-1": use compare "!=" if not all(value.startswith('-') for value in values): fail(f'{fileline}Can\'t mix "+" and "-" items: {" ".join(values)}') compare = '!= ' values = [value.removeprefix('-') for value in values] # Quote interface names and similar items to avoid conflict with reserved # words. "inet" is a reserved word but can also be an interface name. # Do not quote numbers, numeric ranges or similar non-alpha values. if quote: values = [ f'"{value}"' if QUOTE_VALUE.search(value) else value for value in values ] # Single item if len(values) == 1 and ' ' not in values[0]: if '/-' in values[0]: return suffix_mask(values[0], compare) return compare + values[0] # nft doesn't support "saddr { @foo, @bar }" if any(value.startswith('@') for value in values): fail(f'{fileline}Only one @iplist can be used: {" ".join(values)}') # Multiple, use "{set}" return f'{compare}{{ {", ".join(sorted(set(values)))} }}' def netmask_to_and(masklist: str, ipv: int, fileline: str) -> str: """Parse "masklist 24 56" rule and return "and 255.255.255.0 " string. First value is mask for IPv4 and second is for IPv6. """ if not masklist: return '' masks = [int(item) for item in masklist.split() if item.isnumeric()] if len(masks) != 2 or masks[0] > 32 or masks[1] > 128: fail(f'{fileline}Invalid rate_mask: {masklist}') if ipv == 4: ipaddr = ipaddress.IPv4Network(f'0.0.0.0/{masks[0]}') return f'and {ipaddr.netmask} ' ipaddr = ipaddress.IPv6Network(f'::/{masks[1]}') return f'and {ipaddr.netmask} ' def limit_rate_or_ct(rate: str) -> str: """Return "limit rate x" or "ct count x" according to rate.""" if rate.startswith('ct '): return f'{rate} ' return f'limit rate {rate} ' def rule_rate_limit(rule: Rule, ipv: int) -> str: """Return rule's rate limits as nft update-command.""" if rule.global_rate: return limit_rate_or_ct(rule.global_rate) ret = '' for rate in ('saddr_rate', 'daddr_rate', 'saddr_daddr_rate'): rate_limit = rule[rate] if not rate_limit: continue rate_name = rule[f'{rate}_name'] rate_mask = rule[f'{rate}_mask'] # "update @foo { ip saddr " ret += 'add' if rate_limit.startswith('ct ') else 'update' ret += f' @{rate_name}_{ipv} {{ ' if 'saddr' in rate: ret += f'{"ip" if ipv == 4 else "ip6"} saddr ' ret += netmask_to_and(rate_mask, ipv, rule.fileline) if rate == 'saddr_daddr_rate': ret += '. ' if 'daddr' in rate: ret += f'{"ip" if ipv == 4 else "ip6"} daddr ' ret += netmask_to_and(rate_mask, ipv, rule.fileline) # "limit rate 3/second } " ret += f'{limit_rate_or_ct(rate_limit)}}} ' return ret def mark_set_argument(rule: Rule) -> str: """Convert "x" to "x", "x/y" to "mark and ~y or x".""" value = typing.cast('str', rule.mark_set) x_y = value.split('/') if len(x_y) > 2: fail(f'{rule.fileline}Invalid "mark_set" value: {value}') if len(x_y) == 1: return value # Convert "/0xff00" to "/0xffff00ff" so that same mask is used in # set and match operations in config. Nftables requires 0xffff00ff. try: mask = int(x_y[1], 0) # dec or hex except ValueError: fail(f'{rule.fileline}Invalid "mark_set" value: {value}') return f'meta mark & {hex(0xFFFFFFFF ^ mask)} | {x_y[0]}' def mark_match(rule: Rule) -> str: """Convert "x" to "== x", "x/y" to "and y == x". Negative "-x" can also be used to get "!= x". """ value = rule.mark_match if not value: return '' check = '==' if value.startswith('-'): check = '!=' value = value[1:] x_y = value.split('/') if len(x_y) > 2: fail(f'{rule.fileline}Invalid "mark_match" value: {rule.mark_match}') if len(x_y) == 1: return f'meta mark {check} {value} ' return f'meta mark & {x_y[1]} {check} {x_y[0]} ' def time_only_one( rule: Rule, oldvalue: typing.Optional[typing.Union[str, list[str]]], newvalue: typing.Union[str, list[str]], ) -> typing.Union[str, list[str]]: """Return newvalue if oldvalue is not set, else fail.""" if oldvalue or not newvalue: fail(f'{rule.fileline}Invalid "time" value: {rule.time}') return newvalue def time_match_single(rule: Rule, compare: str, value: list[str]) -> str: """Return single time compare+value as nft.""" if not compare and not value: return '' time_only_one(rule, None, value) # Check that value is set # Parse value to day/hour/time wday = None hour = None date = None for item in value: if item.lower() in { 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday', }: wday = time_only_one(rule, wday, item.capitalize()) elif ( re.fullmatch(r'\d\d:\d\d(:\d\d)?', item) # hh:mm:ss, hh:mm or re.fullmatch(r'\d\d:\d\d-\d\d:\d\d', item) # hh:mm-hh:mm ): hour = time_only_one(rule, hour, item) elif re.fullmatch(r'\d\d\d\d-\d\d-\d\d', item): # yyyy-mm-dd date = time_only_one(rule, date, item) else: fail(f'{rule.fileline}Invalid "time" value: {rule.time}') # Output as nft if date and hour: # Combine if both set date += f' {hour}' hour = None ret = '' if wday: ret += f'day {compare}"{wday}" ' if date: ret += f'time {compare}"{date}" ' if hour: if '-' in hour: # hh:mm-hh:mm may not be in ", others must ret += f'hour {compare}{hour} ' else: ret += f'hour {compare}"{hour}" ' return ret def time_match(rule: Rule) -> str: """Convert "time Saturday" to nft, supporting time/day/hour.""" if not rule.time: return '' compare = '' value = [] ret = '' for item in rule.time.split(): if Validators.str_comparison_operator(item): ret += time_match_single(rule, compare, value) compare = '' if item == '==' else f'{item} ' value = [] else: value.append(item) ret += time_match_single(rule, compare, value) return ret def cgroup_match(rule: Rule) -> str: """Parse cgroup to nft.""" cgroup = rule.cgroup if not cgroup: return '' # cgroup: numeric ("10", "15 16 20-30" or "-42") if ' ' in cgroup or re.search(r'^-?[0-9]', cgroup): return f'meta cgroup {single_or_set(cgroup, rule.fileline)} ' # cgroupv2: non-numeric, single value only ("system.slice/sshd.service") level = cgroup.count('/') + 1 return f'socket cgroupv2 level {level} "{cgroup}" ' def rule_statement( szone: str, dzone: str, rule: Rule, ipv: int, *, force_statement: typing.Optional[str] = None, do_lograte: bool = True, ) -> str: """Return rule's rate, log and statement as nft command.""" # pylint: disable=too-many-arguments # Map internal statement to nft statement if rule.mss: mss = 'rt mtu' if rule.mss == 'pmtu' else rule.mss statement = f'tcp flags syn tcp option maxseg size set {mss}' else: statement = force_statement or { 'snat_prefix': 'snat', 'dnat_prefix': 'dnat', 'nftrace': 'meta nftrace set 1', }.get(rule.statement, rule.statement) # queue can have optional flags if statement == 'queue' and rule[statement]: statement = f'{statement} {rule[statement]}' # Rate, counter and log goes before statement prefix = rule_rate_limit(rule, ipv) # "counter" in single rule line adds counter to it. # # "foomuuri { counter xxx }" can be used to add counters to all rules: # yes - add to all rules in all zone-zone # zone-zone - add to all rules in single zone-zone # zone-any - add to all rules in all zone-* # any-zone - add to all rules in all *-zone # Multiple zone-pairs can be defined checklist = {'yes', f'{szone}-{dzone}', f'{szone}-any', f'any-{dzone}'} if not rule.nft and ( rule.counter is not None # "counter" in this single rule or not checklist.isdisjoint(CONFIG.counter) # zone level enable ): prefix += 'counter ' if rule.counter: prefix += f'name "{rule.counter}" ' # "log" in rule will log all packets matching this rule. This is usually # used in final "drop log" rule. Default log prefix is # "zone-zone STATEMENT". if rule.log is not None or rule.log_level is not None: log_prefix = CONFIG.log_prefix if rule.log: if rule.log.startswith('+ '): log_prefix += rule.log[2:] else: log_prefix = rule.log log_prefix = log_prefix.replace('$(szone)', szone) log_prefix = log_prefix.replace('$(dzone)', dzone) log_prefix = log_prefix.replace( '$(statement)', statement.split()[0].upper() ) log_level = ( CONFIG.log_level if rule.log_level is None else rule.log_level ) log_nft = f'log prefix "{log_prefix} " {log_level}' if do_lograte and CONFIG.log_rate: # Limit maximum amount of logging to "foomuuri { log_rate }". # This is important to avoid overlogging (DoS or filesystem full). rate = ( f'update @_lograte_set_{ipv} ' f'{{ {"ip" if ipv == 4 else "ip6"} saddr ' f'limit rate {CONFIG.log_rate} }} ' ) if statement == 'continue': return f'{prefix}{rate}{log_nft}' logname = f'lograte_{len(INTERNAL.logrates) + 1}' INTERNAL.logrates[logname] = (f'{rate}{log_nft}', statement) return f'{prefix}jump {logname}' prefix += f'{log_nft} ' return prefix + statement def output_icmp(szone: str, dzone: str, rules: RuleList, ipv: int) -> None: """Find and parse icmp and icmpv6 rules. These must be handled before ct as "ct established" would accept ping floods. Default is to drop pings. """ has_ping_rule = False has_match_all = False if ipv == 4: icmp = 'icmp' ping = '8' else: icmp = 'icmpv6' ping = '128' for rule in rules: if rule.protocol != icmp: continue match = rule_matchers(rule, ipv, skip_icmp=False) if match is None: continue statement = rule_statement(szone, dzone, rule, ipv) out(f'{match}{statement}') # Continue to next rule if this wasn't type echo-request (ping) or all if ( rule.dport and ping not in rule.dport.split() and 'echo-request' not in rule.dport.split() ): continue # This rule was for ping, usually accepting non-flood pings. Add # explicit rule to drop overflow and all other pings. # Skip adding drop rule if this rule is "match all" as then drop is # never reached. This simple check doesn't catch all types of # "match all". has_ping_rule = True if match in { 'ip protocol icmp ', 'ip6 nexthdr icmpv6 ', f'{icmp} type {ping} ', f'{icmp} type echo-request ', } and statement.startswith( ('accept', 'drop', 'reject', 'jump', 'queue') ): has_match_all = True elif has_match_all: # Specific ping rule after match-all rule warning(f'{rule.fileline}Unreachable ping rule') # Overflow-pings must be dropped before ct if has_ping_rule and not has_match_all: out(f'{icmp} type echo-request drop') # Allow needed icmp out(f'jump allow_icmp_{ipv}') def parse_iplist(rule: Rule, direction: str, ipv: int) -> str: """Parse IP address list in rule[direction] to nft rule.""" iplist = rule[direction] if not iplist: return '' ips = [] for item in iplist.split(): if item.startswith(('@', '-@')): # "@foo" to "@foo_4" ips.append(f'{item}_{ipv}') else: ipv_addr = get_ip_family(item) if ipv_addr == ipv: # Address for this ipv - add to list ips.append(item) elif not ipv_addr: # Invalid IP address fail( f'{rule.fileline}Invalid IP address "{item}" in: {iplist}' ) # No matching addresses for this ipv family if not ips: raise ValueError # Return "ip saddr 10.2.3.4 " string return ( f'{"ip" if ipv == 4 else "ip6"} {direction} ' f'{single_or_set(ips, rule.fileline)} ' ) def parse_maclist(rule: Rule, direction: str) -> str: """Parse MAC address list in rule[direction] to nft rule.""" maclist = rule[direction] if not maclist: return '' macs = [] for item in maclist.split(): item = item.lower() if re.fullmatch(r'(-)?([0-9a-f]{2}:){5}[0-9a-f]{2}', item): macs.append(item) else: fail(f'{rule.fileline}Invalid MAC address "{item}" in: {maclist}') # Return "ether saddr 0a:00:27:00:00:00 " string return f'ether {direction[4:]} {single_or_set(macs, rule.fileline)} ' def parse_interface_names(rule: Rule) -> str: """Parse iifname/oifname to nft rule. Interface "lo" is special: - "iifname lo" or "iif lo" doesn't work in most chains. This is also visible in kernel logging: "IN=". Use "iif 0" instead. It works on all chains. - "oifname lo" never works. "oif 0" works on all but prerouting (dnat) as all packets have "OUT=" there. Use "fib daddr type local" which works on all chains. """ ret = '' for key in ('iifname', 'oifname'): if not rule[key]: continue if key == 'iifname' and rule[key] == 'lo': ret += 'iif 0 ' elif key == 'oifname' and rule[key] == 'lo': ret += 'fib daddr type local ' else: ret += f'{key} ' ret += single_or_set(rule[key], rule.fileline, quote=True) ret += ' ' return ret def parse_protocol_ports( rule: Rule, ipv: int, skip_icmp: bool = True ) -> typing.Optional[str]: """Parse tcp/udp sport/dport to nft rule. This can also handle rules like: - "tcp" without dport to nft "protocol tcp" - "protocol esp" to nft "protocol esp" - "protocol esp 123" to nft "esp spi 123" - "protocol vlan 123" to nft "vlan id 123" """ protocol = rule.protocol if not protocol or (skip_icmp and protocol in {'icmp', 'icmpv6'}): # Protocol is empty for rules like "drop log" and "dnat" # icmp is handled in output_icmp() return '' # ICMP is for IPv4, ICMPv6 is for IPv6. # IPv6 uses Multicast Listener Discovery ICMP instead of IGMP. if (ipv == 4 and protocol == 'icmpv6') or ( ipv == 6 and protocol in {'icmp', 'igmp'} ): return None ports = '' for key in ('sport', 'dport'): if rule[key]: protokey = key if key == 'dport': # Change Foomuuri's generic "dport" keyword to # nft's protocol-specific keyword protokey = { 'ip': 'protocol', 'ip6': 'nexthdr', 'ah': 'spi', 'esp': 'spi', 'comp': 'nexthdr', 'icmp': 'type', 'icmpv6': 'type', 'dst': 'nexthdr', 'frag': 'nexthdr', 'hbh': 'nexthdr', 'mh': 'nexthdr', 'rt': 'nexthdr', 'vlan': 'id', 'arp': 'htype', }.get(protocol, protokey) ports += ( f'{protocol} {protokey} ' f'{single_or_set(rule[key], rule.fileline)} ' ) if ports: return ports return f'{"ip protocol" if ipv == 4 else "ip6 nexthdr"} {protocol} ' def parse_iplist_to_single_ip( rule: Rule, key: str, value: str, ipv: int ) -> typing.Optional[str]: """Parse rule's iplist to single ipaddress, or None.""" target = [] for check in value.split(): if check.count(':') == 1 and get_ip_family(check.split(':')[0]) == 4: check_ipv = 4 # IPv4 address with port elif ( check.startswith('[') and ']:' in check and get_ip_family(check[1:].split(']:')[0]) == 6 ): check_ipv = 6 # IPv6 address with port else: check_ipv = get_ip_family(check) if check_ipv == 0: fail( f'{rule.fileline}Invalid IP address in "{key}" target: {check}' ) if check_ipv == ipv: target.append(check) if not target: # Nothing found for this ipv, don't generate rule return None if len(target) > 1: fail(f'{rule.fileline}Multiple "{key}" targets: {" ".join(target)}') return target[0] def parse_nat_to(rule: Rule, ipv: int) -> typing.Optional[str]: """Parse snat/dnat "to" rule to nft rule.""" if not rule.nat_to: return '' # "to" can be IPv4, IPv6 or both, find correct one target = parse_iplist_to_single_ip(rule, rule.statement, rule.nat_to, ipv) if not target: return None # Generate rule to_text = 'to' if rule.statement in {'snat_prefix', 'dnat_prefix'}: to_text = 'prefix to' return f' {"ip" if ipv == 4 else "ip6"} {to_text} {target}' def rule_matchers( # ruff: ignore[too-many-locals] rule: Rule, ipv: int, *, cast: typing.Optional[str] = None, skip_options: bool = True, skip_icmp: bool = True, ) -> typing.Optional[str]: """Parse rule's matchers to nft rule. Return value "None" is "no match, skip rule". """ # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-return-statements # pylint: disable=too-many-statements if cast is not None and rule.cast != cast: return None # multi/broadcast doesn't match if ipv == 6 and rule.cast == 'broadcast': return None # broadcast is ipv4 only if skip_icmp and rule.protocol in {'icmp', 'icmpv6'}: return None if skip_options and rule.mss: # Handled in output_options() return None # IPv4/6 specific rule? if ipv == 4 and rule.ipv6 and not rule.ipv4: return None if ipv == 6 and rule.ipv4 and not rule.ipv6: return None # Convert matchers to nft castmeta = '' if cast and rule.cast != 'unicast': castmeta = f'fib daddr type {rule.cast} ' ipsecmeta = '' if rule.sipsec: ipsecmeta += f'meta ipsec {rule.sipsec} ' if rule.dipsec: ipsecmeta += f'rt ipsec {rule.dipsec} ' ct_status = '' if rule.ct_status: ct_status = f'ct status {rule.ct_status} ' priority_match = '' if rule.priority_match: priority_match = f'meta priority "{rule.priority_match}" ' uid = '' if rule.uid: uid = ( f'meta skuid {single_or_set(rule.uid, rule.fileline, quote=True)} ' ) gid = '' if rule.gid: gid = ( f'meta skgid {single_or_set(rule.gid, rule.fileline, quote=True)} ' ) meta_set = '' if rule.mark_set: meta_set += ( f'meta mark set {mark_set_argument(rule)} ct mark set meta mark ' ) if rule.priority_set: meta_set += f'meta priority set "{rule.priority_set}" ' iplist_mangle = '' for operation in ('add', 'update', 'delete'): value = rule[f'iplist_{operation}'] if value and ' ' in value: addr, iplist_name = value.split(' ', 1) iplist_mangle += ( f'{operation} {iplist_name}_{ipv} ' f'{{ {"ip" if ipv == 4 else "ip6"} {addr} }} ' ) tproxy = '' if rule.tproxy: tproxy_to = parse_iplist_to_single_ip(rule, 'tproxy', rule.tproxy, ipv) if not tproxy_to: return None tproxy = f'tproxy {"ip" if ipv == 4 else "ip6"} to {tproxy_to} ' length = '' if rule.length: length = ( f'{"ip" if ipv == 4 else "ip6"} length ' f'{single_or_set(rule.length, rule.fileline)} ' ) dscp = '' if rule.dscp: dscp = ( f'{"ip" if ipv == 4 else "ip6"} dscp ' f'{single_or_set(rule.dscp, rule.fileline)} ' ) ifname = parse_interface_names(rule) addrlist = '' try: addrlist += parse_iplist(rule, 'saddr', ipv) addrlist += parse_iplist(rule, 'daddr', ipv) addrlist += parse_maclist(rule, 'mac_saddr') addrlist += parse_maclist(rule, 'mac_daddr') except ValueError: return None proto_ports = parse_protocol_ports(rule, ipv, skip_icmp=skip_icmp) if proto_ports is None: return None # Return matcher string return ( f'{ipsecmeta}{ct_status}{castmeta}{ifname}{addrlist}{proto_ports}' f'{cgroup_match(rule)}{time_match(rule)}{mark_match(rule)}' f'{priority_match}{length}{dscp}{uid}{gid}{tproxy}{meta_set}' f'{iplist_mangle}' ) def output_cast( cast: typing.Optional[str], szone: str, dzone: str, rules: RuleList, ipv: int, *, after_conntrack: bool = True, includes_this_rule: typing.Optional[str] = None, ) -> bool: """Output all uni/multi/broadcast rules for single zone-zone. Return True if "includes_this_rule" was added to ruleset. """ # pylint: disable=too-many-arguments has_mark_restore = False has_needle_rule = False for rule in rules: if rule.after_conntrack != after_conntrack: continue match = rule_matchers(rule, ipv, cast=cast) if match is None: continue if not match and cast is None and rule.cast != 'unicast': continue # Don't convert "multicast accept" to nft "accept" if rule.statement in {'snat', 'dnat', 'snat_prefix', 'dnat_prefix'}: fail( f'{rule.fileline}Statement "{rule.statement}" can\'t ' f'be used in "{szone}-{dzone}" section' ) statement = rule_statement( szone, dzone, rule, ipv, force_statement=rule.nft ) # Automatically restore mark from conntrack before mark match or set # is used. Mark set needs it too for OR-operations. if not has_mark_restore and (rule.mark_match or rule.mark_set): has_mark_restore = True out('meta mark set ct mark') final = f'{match}{statement}' out(final) if final == includes_this_rule: has_needle_rule = True # Does this rule need kernel helper? if rule.helper: if rule.helper.count('-') != 1: fail(f'{rule.fileline}Invalid helper name: {rule.helper}') INTERNAL.helpers.append( ( rule.helper, typing.cast('str', rule.protocol), typing.cast('str', rule.dport), ) ) kernelname = rule.helper.split('-')[0].replace('_', '-') out(f'ct helper "{kernelname}" {rule.statement}') return has_needle_rule def output_zonemap( zonemap: RuleList, szone: str, dzone: str, ipv: int ) -> None: """Output zonemap{} rules for this szone-dzone.""" for rule in zonemap: if rule.szone and szone not in rule.szone.split(): continue if rule.dzone and dzone not in rule.dzone.split(): continue new_szone = rule.new_szone or szone new_dzone = rule.new_dzone or dzone if new_szone == szone and new_dzone == dzone: continue match = rule_matchers(rule, ipv, skip_icmp=False) if match is None: continue out(f'{match}goto {new_szone}-{new_dzone}_{ipv}') # Check if there is reverse zonemap entry, creating a loop reverse = INTERNAL.zonemap_loop_check.get( (new_szone, new_dzone, szone, dzone) ) if reverse: warning( f'{rule.fileline}Possible zonemap loop in ' f'{szone}-{dzone}: {" ".join(rule.line)}' ) warning( f' Reverse zonemap in {new_szone}-{new_dzone}: ' f'{reverse.fileline}{" ".join(reverse.line)}' ) INTERNAL.zonemap_loop_check[szone, dzone, new_szone, new_dzone] = rule def output_options(szone: str, dzone: str, rules: RuleList, ipv: int) -> None: """Output "mss" etc options as first rules.""" for rule in rules: if rule.mss: match = rule_matchers(rule, ipv, skip_options=False) if match is None: continue statement = rule_statement( szone, dzone, rule, ipv, do_lograte=False ) out(f'{match}{statement}') def output_zone( zonemap: RuleList, szone: str, dzone: str, rules: RuleList, ipv: int, ) -> None: """Output single zone-zone_ipv4 nft chain.""" # Header + zonemap jumps + options out(f'chain {szone}-{dzone}_{ipv} {{') output_zonemap(zonemap, szone, dzone, ipv) output_options(szone, dzone, rules, ipv) # Rules with "-conntrack" or plain "log"/"counter" rule. Output these # before conntrack and icmp so that they see all traffic. output_cast(None, szone, dzone, rules, ipv, after_conntrack=False) # ICMP is special, keep it before ct output_icmp(szone, dzone, rules, ipv) # Connection tracking out('ct state vmap {') out('established : accept,') out('related : accept,') out('invalid : jump invalid_drop,') out(f'new : jump smurfs_{ipv},') out(f'untracked : jump smurfs_{ipv}') out('}') # Allow outgoing IGMP multicast membership reports and incoming IGMP # multicast query. output_chain = szone == CONFIG.localhost_zone input_chain = dzone == CONFIG.localhost_zone if ipv == 4 and output_chain and not input_chain: out('ip protocol igmp ip daddr 224.0.0.22 accept') # membership report if ipv == 4 and input_chain and not output_chain: out('ip protocol igmp ip daddr 224.0.0.1 accept') # query # Broadcast and multicast. # # "fib daddr type" works only for incoming packets so skip these if # szone=localhost (outgoing). # # "fib daddr type" works also for forwarding packets so write those rules # too (see below for zonemap reason). Multicast can't really be forwarded # as it is local to ip/netmask. Proxying is ok, where software listens one # interface and writes it to another. if not output_chain: has_multicast_accept = output_cast( 'multicast', szone, dzone, rules, ipv, includes_this_rule='fib daddr type multicast accept', ) has_broadcast_accept = output_cast( 'broadcast', szone, dzone, rules, ipv, includes_this_rule='fib daddr type broadcast accept', ) # Add multicast/broadcast drop if there was no "accept" for it. if ipv == 4: if not has_multicast_accept and not has_broadcast_accept: out('fib daddr type { broadcast, multicast } drop') elif has_multicast_accept and not has_broadcast_accept: out('fib daddr type broadcast drop') elif not has_multicast_accept and has_broadcast_accept: out('fib daddr type multicast drop') # else has both, don't output anything elif not has_broadcast_accept: # IPv6 doesn't have broadcast out('fib daddr type multicast drop') # Unicast. # # Broadcast/multicast is already handled above for incoming packets so # output only unicast here for incoming. # # For forward/output add multicast rules without multicast matcher. This # means that for forward packets both with and without "fib daddr type" # rules will be outputted. This is needed for complex zonemap mangling # where szone=myservice might actually be from localhost. output_cast('unicast' if input_chain else None, szone, dzone, rules, ipv) out('}') def output_zone_vmaps(zones: Zones, rules: ZonePairRules) -> None: """Output interface verdict maps to jump to correct zone-zone.""" # pylint: disable=too-many-branches # pylint: disable=too-many-statements # Vmap must have interval-flag if there is wildcard-interface ("eth*"). localhost = CONFIG.localhost_zone has_wildcard = any( '*' in interface for value in zones.values() for interface in value ) # Incoming zones out('map input_zones {') out('type ifname : verdict') if has_wildcard: out('flags interval') out('elements = {') out('"lo" : accept,') for zone, value in zones.items(): for interface in value: out(f'"{interface}" : jump {zone}-{localhost},') out('}') out('}') # Outgoing zones out('map output_zones {') out('type ifname : verdict') if has_wildcard: out('flags interval') out('elements = {') if len(rules[localhost, localhost]) > 1: # Jump to lo-lo if it has rules out(f'"lo" : jump {localhost}-{localhost},') else: out('"lo" : accept,') for zone, value in zones.items(): for interface in value: out(f'"{interface}" : jump {localhost}-{zone},') out('}') out('}') # Forwarding zones out('map forward_zones {') out('type ifname . ifname : verdict') if has_wildcard: out('flags interval') out('elements = {') out('"lo" . "lo" : accept,') for szone, svalue in zones.items(): for dzone, dvalue in zones.items(): for sinterface in svalue: for dinterface in dvalue: out( f'"{sinterface}" . "{dinterface}" : ' f'jump {szone}-{dzone},' ) out('}') out('}') # Forward with catch all interface if INTERNAL.catch_all_interface_zone: # From eth0 to * out('map forward_catchall_input_zones {') out('type ifname : verdict') if has_wildcard: out('flags interval') out('elements = {') out('"lo" : accept,') for zone, value in zones.items(): for interface in value: out( f'"{interface}" : jump ' f'{zone}-{INTERNAL.catch_all_interface_zone},' ) out('}') out('}') # From * to eth0 out('map forward_catchall_output_zones {') out('type ifname : verdict') if has_wildcard: out('flags interval') out('elements = {') out('"lo" : accept,') for zone, value in zones.items(): for interface in value: out( f'"{interface}" : jump ' f'{INTERNAL.catch_all_interface_zone}-{zone},' ) out('}') out('}') def output_zone2zone_rules(rules: ZonePairRules, zonemap: RuleList) -> None: """Output all zone-zone rules for both IPv4 and IPv6.""" for szone, dzone in rules: # Split to zone-zone_4 and zone-zone_6 out(f'chain {szone}-{dzone} {{') out('meta nfproto vmap {') out(f'ipv4 : goto {szone}-{dzone}_4,') out(f'ipv6 : goto {szone}-{dzone}_6') out('}') out('}') # IPv4 and IPv6 chains output_zone(zonemap, szone, dzone, rules[szone, dzone], 4) output_zone(zonemap, szone, dzone, rules[szone, dzone], 6) def output_rule_section_no_header(rules: RuleList, section: str) -> None: """Output snat, dnat, prerouting, postrouting, etc. rules inside chain.""" if not rules: return has_mark_restore = False ip_merger = set() for rule in rules: for ipv in (4, 6): to_rule = parse_nat_to(rule, ipv) if to_rule is None: # ipv == 6 and "nat to IPv4" mismatch continue match = rule_matchers( rule, ipv, skip_options=False, skip_icmp=False ) if match is None: continue statement = rule_statement( section.upper(), '', rule, ipv, force_statement=rule.nft, do_lograte=False, ) # Automatically restore mark from conntrack when needed if not has_mark_restore and (rule.mark_match or rule.mark_set): has_mark_restore = True out('meta mark set ct mark') # There are no separate IPv4/IPv6 chains so merge possible rules full_rule = f'{match}{statement}{to_rule}' if full_rule in ip_merger: continue ip_merger.add(full_rule) out(full_rule) def output_special_chains(chain_rules: SpecialChainRules) -> None: """Output snat, dnat, prerouting postrouting, etc. chain + rules.""" for prefix in ( 'snat', 'dnat', 'prerouting', 'postrouting', 'forward', 'input', 'output', ): for (section, ftype, priority), rules in chain_rules.items(): if prefix != section: continue # Output-chain must restore mark from conntrack if mark_set was # used for locally generated packets (usually in multiple ISP's # prerouting). # # Simply restore mark if mark_set was used in any chain. For that # reason output-chain should be outputted last. need_mark_restore = section == 'output' and any( 'ct mark set' in line for line in OUT ) if not rules and not need_mark_restore: continue hook_chain = { 'snat': 'postrouting', 'dnat': 'prerouting', }.get(section, section) hook_nft = f'type {ftype} hook {hook_chain} priority {priority}' chain_name = f'{ftype}_{hook_chain}_{priority}' chain_name = chain_name.replace(INTERNAL.priority_offset, '') chain_name = chain_name.replace('+', '') chain_name = chain_name.replace(' ', '_') chain_name = chain_name.replace('__', '_') out(f'chain {chain_name} {{') out(hook_nft) if need_mark_restore: out('meta mark set ct mark') output_rule_section_no_header(rules, section) out('}') def output_static_chain_logging(chain: str, statement: str) -> None: """Output logging rules for input/output/forward/invalid/smurfs chains.""" lograte = CONFIG[f'log_{chain}'] # Enabled in foomuuri{} if lograte == 'no': return if lograte == 'yes': # "yes" means use standard log rate lograte = CONFIG.log_rate flags = '' if chain == 'invalid' and 'group ' not in CONFIG.log_level: flags = ' flags ip options' # flags and group are mutually exclusive flags += f' {CONFIG.log_level}' chain = chain.upper() statement = statement.upper() if not lograte: out(f'log prefix "{chain} {statement} "{flags}') return out( f'update @_lograte_set_4 {{ ip saddr limit rate {lograte} }} ' f'log prefix "{chain} {statement} "{flags}' ) out( f'update @_lograte_set_6 {{ ip6 saddr limit rate {lograte} }} ' f'log prefix "{chain} {statement} "{flags}' ) def output_header(chain_rules: SpecialChainRules) -> None: """Output generic nft header.""" # pylint: disable=too-many-branches # pylint: disable=too-many-statements # Delete current foomuuri table and add new out('table inet foomuuri') out('delete table inet foomuuri') out('') out('table inet foomuuri {') out('') # Insert include files for filename in find_config_files( CONFIG.share_dir, '*.nft' ) + find_config_files(CONFIG.etc_dir, '*.nft'): try: lines = filename.read_text('utf-8').splitlines() except PermissionError as error: fail(f"File {filename}: Can't read: {error}") for line in lines: line = line.strip() if line: out(line) # Logging chains for chain in ('invalid', 'smurfs', 'rpfilter'): out(f'chain {chain}_drop {{') output_rule_section_no_header( chain_rules.get((chain, None, None), []), chain ) if chain == 'rpfilter': out('udp sport 67 udp dport 68 return') output_static_chain_logging(chain, 'drop') out('drop') out('}') # input/output/forward jump chains out('chain input {') out(f'type filter hook input priority filter{INTERNAL.priority_offset}') out('iifname vmap @input_zones') if INTERNAL.catch_all_interface_zone: out( f'jump {INTERNAL.catch_all_interface_zone}-{CONFIG.localhost_zone}' ) output_static_chain_logging('input', 'drop') out('drop') out('}') out('chain output {') out(f'type filter hook output priority filter{INTERNAL.priority_offset}') out('oifname vmap @output_zones') if INTERNAL.catch_all_interface_zone: out( f'jump {CONFIG.localhost_zone}-{INTERNAL.catch_all_interface_zone}' ) # IGMP membership report and IPv6 equivalent must be allowed here too as # D-Bus interface change event might not be processed yet. out('ip protocol igmp ip daddr 224.0.0.22 accept') out('ip6 saddr :: icmpv6 type mld2-listener-report accept') output_static_chain_logging('output', 'reject') out('reject with icmpx admin-prohibited') out('}') flowtable = {item for item in CONFIG.flowtable if '=' not in item} if len(flowtable) == 1 and flowtable != {'no'}: fail(f'Invalid "foomuuri {{ flowtable }}" value: {flowtable.pop()}') if len(flowtable) > 1: out('flowtable fastpath {') out(f'hook ingress priority filter{INTERNAL.priority_offset}') out(f'devices = {single_or_set(flowtable, quote=True)}') if 'hw_offload=yes' in CONFIG.flowtable: out('flags offload') if 'counter=yes' in CONFIG.flowtable: out('counter') out('}') out('chain forward {') out(f'type filter hook forward priority filter{INTERNAL.priority_offset}') if len(flowtable) > 1: out('flow add @fastpath') out('iifname . oifname vmap @forward_zones') if INTERNAL.catch_all_interface_zone: out('iifname vmap @forward_catchall_input_zones') # eth0 -> * out('oifname vmap @forward_catchall_output_zones') # * -> eth0 out( # * -> * f'jump {INTERNAL.catch_all_interface_zone}-' f'{INTERNAL.catch_all_interface_zone}' ) output_static_chain_logging('forward', 'drop') out('drop') out('}') # Rules starting with these matchers can be merged. # List them in preferred order. MERGES = [ 'fib daddr type multicast udp dport', 'fib daddr type broadcast udp dport', 'udp dport', 'udp sport', 'tcp dport', 'tcp sport', 'ct helper', ] def merge_accepts(accepts: list[str], linenum: int) -> int: """Sort and merge found accept rules.""" merge = {key: [] for key in MERGES[::-1]} ret = 0 for accept in accepts: for key, ports in merge.items(): # Check if accept line is: # "tcp dport number accept" # "tcp dport { number, number } accept" # "tcp dport number-number accept" regex = f'{key} (\\{{ )?([-\\d, ]+)( \\}})? accept' match = re.fullmatch(regex, accept) if match: # Add "22" from "tcp dport 22 accept" to merged ports.append(match.group(2)) break else: # Can't merge, output as is OUT.insert(linenum + ret, accept) ret += 1 # Output merged accept ports in preferred order for key, ports in merge.items(): if ports: OUT.insert(linenum, f'{key} {single_or_set(ports)} accept') ret += 1 return ret def optimize_accepts() -> None: """Optimize ruleset accepts. This will change multiple accepts to single accept using set. """ # Remove duplicate accept rules already_seen = set() linenum = 0 while linenum < len(OUT): line = OUT[linenum] if line in already_seen: # Already in seen, delete from ruleset del OUT[linenum] continue if line.endswith(' accept'): # Add accept to seen already_seen.add(line) else: # Something else, reset seen already_seen = set() linenum += 1 # Merge multiple "tcp daddr x accept" rules to single # rule "tcp daddr { x, y, z } accept". accepts = [] linenum = 0 while linenum < len(OUT): line = OUT[linenum] if line == 'continue': # No-op line generated by plain "counter" del OUT[linenum] elif line.endswith(' accept') and line.startswith( tuple(MERGES) + ('ip ', 'ip6 ') ): # Line is "tcp dport ... accept" accepts.append(line) del OUT[linenum] else: # Line is something else, output merged accepts linenum += merge_accepts(accepts, linenum) + 1 accepts = [] def optimize_jumps() -> None: """Optimize lograte jumps in ruleset. This will change zone-zone's final "drop log" rule to optimized version. """ linenum = 0 while linenum < len(OUT): line = OUT[linenum] if line.startswith('jump lograte_'): logname = line.split()[1] log_nft, statement = INTERNAL.logrates.pop(logname) OUT[linenum] = log_nft OUT.insert(linenum + 1, statement) linenum += 1 def optimize_final_rules() -> None: """Remove unreachable rules from chain after "drop" without matcher.""" linenum = 0 while linenum < len(OUT): if OUT[linenum].startswith(('accept', 'drop', 'reject', 'queue')): while OUT[linenum + 1] != '}': del OUT[linenum + 1] linenum += 1 def output_logrates() -> None: """Output non-optimized lograte entries as chains.""" for logname, (log_nft, statement) in INTERNAL.logrates.items(): out(f'chain {logname} {{') out(log_nft) out(statement) out('}') # Output empty lograte sets used by "foomuuri { log_rate }". for ipv in (4, 6): out(f'set _lograte_set_{ipv} {{') out(f'type ipv{ipv}_addr') out(f'size {CONFIG.set_size}') out('flags dynamic,timeout') out('timeout 1m') out('}') def output_iplist_sets(iplists: IPLists) -> None: """Output empty iplist{} sets.""" for name in sorted(iplists): for ipv in (4, 6): out(f'set {name[1:]}_{ipv} {{') out(f'type ipv{ipv}_addr') if iplists[name].options.dynamic: out(f'size {CONFIG.set_size}') out('flags dynamic,timeout') else: out('flags interval,timeout') if iplists[name].options.merge: out('auto-merge') if iplists[name].options.element_timeout: out(f'timeout {iplists[name].options.element_timeout}s') if iplists[name].options.element_timeout > 20 * 60: out('gc-interval 5m') out('}') def output_named_counters( rules: ZonePairRules, chain_rules: SpecialChainRules ) -> None: """Output named counters.""" # Collect all counter names names = set() for rulelist in itertools.chain(rules.values(), chain_rules.values()): for rule in rulelist: if rule.counter: names.add(rule.counter) # Output counters for name in sorted(names): out(f'counter {name} {{') values = INTERNAL.permanent_counter_values.get(name) if values: out(f'packets {values[0]} bytes {values[1]}') out('}') def output_helpers() -> None: """Output helpers.""" # Convert helper list to helper->proto->set(ports) dict helpers = {} for name, proto, ports in INTERNAL.helpers: if name not in helpers: helpers[name] = {} if proto not in helpers[name]: helpers[name][proto] = set() for port in ports.split(): helpers[name][proto].add(port) if not helpers: return # Output "ct helper" lines for name, protos in helpers.items(): kernelname = name.split('-')[0].replace('_', '-') out(f'ct helper {name} {{') for proto in protos: out(f'type "{kernelname}" protocol {proto}') out('}') # Output prerouting out('chain helper {') out( f'type filter hook prerouting priority filter' f'{INTERNAL.priority_offset}' ) for name, protos in helpers.items(): for proto, ports in protos.items(): out( f'{proto} dport {single_or_set(" ".join(ports))} ' f'ct helper set "{name}"' ) out('}') def output_rpfilter() -> None: """Prerouting chain to check rpfilter.""" if 'no' in CONFIG.rpfilter: return out('chain rpfilter {') out( f'type filter hook prerouting priority filter' f'{INTERNAL.priority_offset}' ) interfaces = '' if 'yes' not in CONFIG.rpfilter: # Specific interfaces? interfaces = f'iifname {single_or_set(CONFIG.rpfilter, quote=True)} ' out( f'{interfaces}fib saddr . mark . iif oif 0 meta ipsec missing ' f'jump rpfilter_drop' ) out('}') def output_footer() -> None: """Output generic ruleset footer.""" out('}') def save_file( filename: pathlib.PosixPath, lines: typing.Union[dict, typing.Iterable[str]], ) -> None: """Write lines to file. Use temporary file to make this atomic.""" if str(filename) == '/dev/null': return if isinstance(lines, dict): text = json.dumps(lines, separators=(',', ':')) else: text = '\n'.join(lines) + '\n' tmpfile = filename.with_suffix(f'{filename.suffix}.{os.getpid()}') try: tmpfile.unlink(missing_ok=True) tmpfile.write_text(text, 'utf-8') tmpfile.chmod(0o600) tmpfile.rename(filename) except PermissionError as error: fail(f"File {filename}: Can't write: {error}") except FileNotFoundError as error: warning(f"File {filename}: Can't write: {error}") def env_cleanup(text: str) -> str: """Allow only letters and numbers in text for environment variable.""" # Convert ä->a as isalpha('ä') is true value = unicodedata.normalize('NFKD', text) value = value.encode('ASCII', 'ignore').decode('utf-8') # Remove non-alphanumeric chars return ''.join(char if char.isalnum() else '_' for char in value) def save_final(filename: typing.Optional[pathlib.PosixPath]) -> list[str]: """Save final ruleset to file.""" # Convert to indented lines indent = 0 lines = [] for line in OUT: if line.startswith('}'): indent -= 1 if line: line = '\t' * indent + line lines.append(line) if line == '\t}': lines.append('') if line.endswith('{'): indent += 1 # Save to "next" file if filename: save_file(filename, lines) return lines def signal_childs() -> None: """Signal foomuuri-dbus and foomuuri-monitor to reload.""" for child in ('dbus', 'monitor'): # Read pid filename = CONFIG.run_dir / f'foomuuri-{child}.pid' try: pid = int(filename.read_text(encoding='utf-8')) except PermissionError as error: fail(f"File {filename}: Can't read: {error}") except (FileNotFoundError, ValueError): continue # Send reload-signal with contextlib.suppress(OSError): os.kill(pid, signal.SIGHUP) def apply_final() -> int: """Use final ruleset.""" # Check config good_file = state_file('good_file') next_file = state_file('next_file') if INTERNAL.command == 'check': if INTERNAL.root_power: # "nft check" requires root ret = run_program_rc( CONFIG.nft_bin + ['--check', '--file', next_file] ) else: ret = 0 warning('Not running as "root", skipping "nft check"') if ret: fail(f'Nftables failed to check ruleset, error code {ret}', False) else: verbose('check success', 0) return ret # Run pre_start / pre_stop hook run_program_rc(INTERNAL.get(f'pre_{INTERNAL.command}')) # Load "next" ret = run_program_rc( CONFIG.nft_bin + ['--file', next_file], print_output=False ) # Check failure if ret: fail(f'Failed to load ruleset to nftables, error code {ret}', False) return 1 # Success. Rename "next" to "good", signal dbus to reload and run hook if INTERNAL.command == 'start': good_file.unlink(missing_ok=True) next_file.rename(good_file) signal_childs() run_program_rc(INTERNAL.get(f'post_{INTERNAL.command}')) verbose(f'{INTERNAL.command} success', 0) return 0 def print_assembled_config( config: dict, zones: Zones, zonemap: RuleList, rules: ZonePairRules, chain_rules: SpecialChainRules, ) -> None: """Print assembled configuration in foomuuri.conf format.""" def print_section(name: str, lines: list, indent: int = 2) -> None: """Section print helper.""" if lines: print(f'{name} {{') print(*[f'{" " * indent}{line}' for line in lines], sep='\n') print('}') print('Assembled configuration << EOF') print_section( 'foomuuri', [ f'{option} {value}' for option, value in dataclasses.asdict(CONFIG).items() ], ) print_section( 'zone', [ f'{zone} {" ".join(interfaces)}' for zone, interfaces in zones.items() ], ) print_section( 'zonemap', [f'{" ".join(statement.line)}' for statement in zonemap] ) for chains, statements in chain_rules.items(): print_section( ' '.join(chain for chain in chains if chain is not None), [f'{" ".join(statement.line)}' for statement in statements], ) print_section( 'iplist', [f'{" ".join(line[1])}' for line in config.get('iplist', [])] ) for zone2zone, statements in rules.items(): print_section( '-'.join(zone2zone), [f'{" ".join(statement.line)}' for statement in statements], ) print_section( 'hook', [ f'{hook} {join_args(INTERNAL.get(hook))}' for hook in ( filter(lambda k: k.startswith(('pre_', 'post_')), INTERNAL) ) if INTERNAL.get(hook) ], ) print('EOF') def command_start() -> int: """Process "start" or "check" command.""" # Read full config config = minimal_config() zones = parse_config_zones(config) zonemap = parse_config_zonemap(config, zones) iplists = parse_config_iplist(config) chain_rules = parse_config_special_chains(config) parse_config_groups(config, parse_config_targets(config)) parse_config_hook(config) rules = parse_config_rules(config) # Also verify for unknown sections insert_any_zones(zones, rules) verify_config(config, zones, rules) # Keep named counter values when reloading if CONFIG.permanent_counter: INTERNAL.permanent_counter_values = get_counter_values() or {} # Generate output output_header(chain_rules) output_rate_names(rules) output_zone_vmaps(zones, rules) output_zone2zone_rules(rules, zonemap) output_special_chains(chain_rules) optimize_jumps() optimize_final_rules() optimize_accepts() output_logrates() output_iplist_sets(iplists) output_named_counters(rules, chain_rules) output_helpers() output_rpfilter() output_footer() # Add iplist elements from cache to output. Include only iplists # that are marked with "start=yes". if iplists_add_on_start := iplists.filter_names_by('start', True): iplist_output_values( iplists, IPListSourceCache.read(iplists), set(iplists), # All iplists are currently active iplists_add_on_start, False, ) # Print assembled configuration if CONFIG.verbose >= 2: print_assembled_config(config, zones, zonemap, rules, chain_rules) # Save known zones to file save_file(state_file('zone_file'), zones.keys()) # Save and apply generated ruleset save_final(state_file('next_file')) return apply_final() def command_stop() -> int: """Process "stop" command. This will remove all foomuuri rules.""" config = minimal_config() # Needed for pre_stop and post_stop hooks parse_config_hook(config) out('table inet foomuuri') out('delete table inet foomuuri') save_final(state_file('next_file')) return apply_final() def command_block() -> int: """Load "block all traffic" ruleset.""" minimal_config() return run_program_rc( CONFIG.nft_bin + ['--file', CONFIG.share_dir / 'block.fw'] ) def parse_active_interface_zone() -> dict[str, str]: """Parse current interface->zone mapping from active nft ruleset.""" data = nft_json('list map inet foomuuri input_zones') if not data: return {} ret = {} for item in data['nftables']: if 'map' in item: for interface, rule in item['map']['elem']: if interface != 'lo': ret[interface] = rule['jump']['target'].split('-')[0] return ret def command_status() -> int: """Print if Foomuuri is running, zone<->mapping, etc.""" # Get minimal config and interface status config = minimal_config() zones = parse_config_zones(config) zone_interface = parse_active_interface_zone() if INTERNAL.catch_all_interface_zone: zone_interface['*'] = INTERNAL.catch_all_interface_zone nft_tables = nft_json('list tables') or {} tables = { t['table'].get('name') for t in nft_tables.get('nftables', {}) if t.get('table') } # Running if 'foomuuri' not in tables: fail('Foomuuri is not running') return 1 print('Foomuuri is running') # D-Bus, Monitor for child in ('dbus', 'monitor'): filename = CONFIG.run_dir / f'foomuuri-{child}.pid' try: pid = int(filename.read_text(encoding='utf-8')) os.kill(pid, 0) print(f'Foomuuri-{child} is running, PID {pid}') except ( FileNotFoundError, ValueError, PermissionError, ProcessLookupError, ): print(f'Foomuuri-{child} is not running') # Check for multiple firewalls if 'firewalld' in tables: print() warning('Firewalld is running') # Zones if not zone_interface: print() print('Warning: There are no interfaces assigned to any zones') print() print('zone {') table = [] for zone in zones: interfaces = [ interface for interface, int_zones in zone_interface.items() if zone == int_zones ] table.append(('', zone, f'{" ".join(interfaces)}')) print_table(data=table, header=False) print('}') return 0 if HAVE_DBUS: class FoomuuriDbusException(dbus.DBusException): """Exception class for D-Bus interface.""" _dbus_error_name = 'fi.foobar.Foomuuri1.exception' class DbusCommon(dbus.service.Object): """D-Bus server and common functions.""" polkit = None def __init__( self, conn: typing.Any, object_path: str, zones: Zones ) -> None: """Initialize dbus.service.Object.""" dbus.service.Object.__init__( self, conn=conn, object_path=object_path ) # Save config data: zone list is static. self.zones = zones @staticmethod def apply_out() -> int: """Apply current OUT commands.""" lines = '\n'.join(save_final(None)) return run_program_rc( CONFIG.nft_bin + ['--file', '-'], stdin=lines ) def verify_polkit_privilege( self, sender: typing.Optional[str], privilege: str, ) -> None: """Verify authorization from PolicyKit.""" # Initialize polkit connections if not self.polkit: try: self.polkit = dbus.Interface( dbus.SystemBus().get_object( 'org.freedesktop.PolicyKit1', '/org/freedesktop/PolicyKit1/Authority', ), 'org.freedesktop.PolicyKit1.Authority', ) except dbus.DBusException: # No polkit, fallback to check that UID == 0 iface = dbus.Interface( dbus.SystemBus().get_object( 'org.freedesktop.DBus', '/org/freedesktop/DBus' ), 'org.freedesktop.DBus', ) with contextlib.suppress(ValueError): if int(iface.GetConnectionUnixUser(sender)) == 0: return raise FoomuuriDbusException( 'Authorization failed' ) from None # Verify authorization from polkit try: result = self.polkit.CheckAuthorization( ('system-bus-name', {'name': sender}), privilege, {}, 1, '', timeout=60, ) is_authorized = result[0] if is_authorized: return except dbus.DBusException as error: warning(str(error)) self.polkit = None # Reconnect on next call raise FoomuuriDbusException('Authorization failed') from None @staticmethod def remove_interface( interface_zone: dict[str, str], interface: str ) -> str: """Remove interface from all zones.""" # Get interface's current zone zone = interface_zone.get(interface) if not zone: return '' # Remove from input and output out( f'delete element inet foomuuri input_zones {{ "{interface}" }}' ) out( f'delete element inet foomuuri output_zones ' f'{{ "{interface}" }}' ) # Remove from forward for other in interface_zone: out( f'delete element inet foomuuri forward_zones ' f'{{ "{other}" . "{interface}" }}' ) if other != interface: out( f'delete element inet foomuuri forward_zones ' f'{{ "{interface}" . "{other}" }}' ) # Remove from catch all if INTERNAL.catch_all_interface_zone: out( f'delete element inet foomuuri ' f'forward_catchall_input_zones {{ "{interface}" }}' ) out( f'delete element inet foomuuri ' f'forward_catchall_output_zones {{ "{interface}" }}' ) return zone @staticmethod def add_interface( interface_zone: dict[str, str], interface: str, zone: str ) -> None: """Add interface to zone. It must be already removed from other zones. """ # Add to input and output out( f'add element inet foomuuri input_zones ' f'{{ "{interface}" : jump ' f'{zone}-{CONFIG.localhost_zone} }}' ) out( f'add element inet foomuuri output_zones ' f'{{ "{interface}" : jump ' f'{CONFIG.localhost_zone}-{zone} }}' ) # Add to forward for other, otherzone in interface_zone.items(): if other != interface: out( f'add element inet foomuuri forward_zones ' f'{{ "{other}" . "{interface}" : ' f'jump {otherzone}-{zone} }}' ) out( f'add element inet foomuuri forward_zones ' f'{{ "{interface}" . "{other}" : ' f'jump {zone}-{otherzone} }}' ) out( f'add element inet foomuuri forward_zones ' f'{{ "{interface}" . "{interface}" : jump {zone}-{zone} }}' ) # Add to catch all if INTERNAL.catch_all_interface_zone: out( f'add element inet foomuuri ' f'forward_catchall_input_zones ' f'{{ "{interface}" : jump ' f'{zone}-{INTERNAL.catch_all_interface_zone} }}' ) out( f'add element inet foomuuri ' f'forward_catchall_output_zones ' f'{{ "{interface}" : jump ' f'{INTERNAL.catch_all_interface_zone}-{zone} }}' ) def change_interface_zone( self, interface: str, new_zone: str ) -> tuple[str, str]: """Change interface to new_zone, or delete if new_zone is empty.""" interface, new_zone = str(interface), str(new_zone) if new_zone and new_zone not in self.zones: warning(f'Zone "{new_zone}" is unknown') raise FoomuuriDbusException(f'Zone "{new_zone}" is unknown') if interface == 'lo': # Interface "lo" must stay in "localhost" zone. # Other interfaces can be added to "localhost" only if # "localhost-localhost" section is defined. if new_zone != CONFIG.localhost_zone: warning( f'Can\'t change interface "lo" to zone "{new_zone}"' ) raise FoomuuriDbusException( f'Can\'t change interface "lo" to zone "{new_zone}"' ) return '', '' if interface == '*': warning('Can\'t modify catch all interface "*"') raise FoomuuriDbusException( 'Can\'t modify catch all interface "*"' ) OUT.clear() interface_zone = parse_active_interface_zone() old_zone = self.remove_interface(interface_zone, interface) if new_zone: self.add_interface(interface_zone, interface, new_zone) if self.apply_out(): raise FoomuuriDbusException( 'Failed to modify interface. See ' 'journal for more information.' ) return old_zone, new_zone def parse_default_zone(self, interface: str, zone: str) -> str: """Return zone, or dbus_zone if empty.""" interface, zone = str(interface), str(zone) if zone: return zone # Fallback to zones section, or to foomuuri.dbus_zone for key, value in self.zones.items(): if interface in value: return key return CONFIG.dbus_zone def method_get_zones(self) -> list[str]: """Get list of available zones. "localhost" can't have any interfaces so don't include it. """ return [ name for name in self.zones if name != CONFIG.localhost_zone ] def method_remove_interface(self, zone: str, interface: str) -> str: """Remove interface from zone, or from all if zone is empty. This is currently always handled as "from all". """ if not Validators.str_interface_name(interface): raise FoomuuriDbusException( f'Invalid interface name: {interface}' ) verbose(f'Interface "{interface}" remove from zone "{zone}"', 0) return self.change_interface_zone(interface, '')[0] def method_add_interface(self, zone: str, interface: str) -> str: """Add interface to zone. There can be only one zone per interface so it will be removed from previous zone if needed. """ if not Validators.str_interface_name(interface): raise FoomuuriDbusException( f'Invalid interface name: {interface}' ) zone = self.parse_default_zone(interface, zone) verbose(f'Interface "{interface}" add to zone "{zone}"', 0) return self.change_interface_zone(interface, zone)[1] def method_change_zone_of_interface( self, zone: str, interface: str ) -> str: """Change interface to zone.""" if not Validators.str_interface_name(interface): raise FoomuuriDbusException( f'Invalid interface name: {interface}' ) zone = self.parse_default_zone(interface, zone) verbose(f'Interface "{interface}" change to zone "{zone}"', 0) return self.change_interface_zone(interface, zone)[0] class DbusFoomuuri(DbusCommon): """D-Bus server for Foomuuri.""" # pylint: disable=invalid-name # dbus method names @dbus.service.method( 'fi.foobar.Foomuuri1.zone', in_signature='', out_signature='as', sender_keyword='sender', ) def getZones(self, sender: typing.Optional[str] = None) -> list[str]: """Get list of available zones.""" self.verify_polkit_privilege(sender, 'fi.foobar.Foomuuri1.info') return self.method_get_zones() @dbus.service.method( 'fi.foobar.Foomuuri1.zone', in_signature='ss', out_signature='s', sender_keyword='sender', ) def removeInterface( self, zone: str, interface: str, sender: typing.Optional[str] = None, ) -> str: """Remove interface from zone, or from all if zone is empty. Return: previous zone """ self.verify_polkit_privilege(sender, 'fi.foobar.Foomuuri1.config') return self.method_remove_interface(zone, interface) @dbus.service.method( 'fi.foobar.Foomuuri1.zone', in_signature='ss', out_signature='s', sender_keyword='sender', ) def addInterface( self, zone: str, interface: str, sender: typing.Optional[str] = None, ) -> str: """Add interface to zone. Return: new zone """ self.verify_polkit_privilege(sender, 'fi.foobar.Foomuuri1.config') return self.method_add_interface(zone, interface) @dbus.service.method( 'fi.foobar.Foomuuri1.zone', in_signature='ss', out_signature='s', sender_keyword='sender', ) def changeZoneOfInterface( self, zone: str, interface: str, sender: typing.Optional[str] = None, ) -> str: """Change interface to zone. Return: previous zone """ self.verify_polkit_privilege(sender, 'fi.foobar.Foomuuri1.config') return self.method_change_zone_of_interface(zone, interface) class DbusFirewalld(DbusCommon): """D-Bus server for firewalld emulation.""" # pylint: disable=invalid-name # dbus method names @dbus.service.method( 'org.fedoraproject.FirewallD1.zone', in_signature='', out_signature='as', sender_keyword='sender', ) def getZones(self, sender: typing.Optional[str] = None) -> list[str]: """Get list of available zones.""" self.verify_polkit_privilege(sender, 'fi.foobar.Foomuuri1.info') return self.method_get_zones() @dbus.service.method( 'org.fedoraproject.FirewallD1.zone', in_signature='ss', out_signature='s', sender_keyword='sender', ) def removeInterface( self, zone: str, interface: str, sender: typing.Optional[str] = None, ) -> str: """Remove interface from zone, or from all if zone is empty. Return: previous zone """ self.verify_polkit_privilege(sender, 'fi.foobar.Foomuuri1.config') return self.method_remove_interface(zone, interface) @dbus.service.method( 'org.fedoraproject.FirewallD1.zone', in_signature='ss', out_signature='s', sender_keyword='sender', ) def addInterface( self, zone: str, interface: str, sender: typing.Optional[str] = None, ) -> str: """Add interface to zone. Return: new zone """ self.verify_polkit_privilege(sender, 'fi.foobar.Foomuuri1.config') return self.method_add_interface(zone, interface) @dbus.service.method( 'org.fedoraproject.FirewallD1.zone', in_signature='ss', out_signature='s', sender_keyword='sender', ) def changeZoneOfInterface( self, zone: str, interface: str, sender: typing.Optional[str] = None, ) -> str: """Change interface to zone. Return: previous zone """ self.verify_polkit_privilege(sender, 'fi.foobar.Foomuuri1.config') return self.method_change_zone_of_interface(zone, interface) @dbus.service.method( 'org.fedoraproject.FirewallD1.zone', in_signature='s', out_signature='s', sender_keyword='sender', ) def getZoneOfSource( self, source: str, sender: typing.Optional[str] = None ) -> str: """Get current zone of source. These calls are used by netavark (run by Podman) to set container's IP address to "trusted" zone. There is no such thing in Foomuuri. """ self.verify_polkit_privilege(sender, 'fi.foobar.Foomuuri1.info') if not get_ip_family(source): raise FoomuuriDbusException('Invalid source address') return '' # It's not in any zone @dbus.service.method( 'org.fedoraproject.FirewallD1.zone', in_signature='ss', out_signature='s', sender_keyword='sender', ) def changeZoneOfSource( self, zone: str, source: str, sender: typing.Optional[str] = None ) -> str: """Change source to zone. See above getZoneOfSource().""" self.verify_polkit_privilege(sender, 'fi.foobar.Foomuuri1.config') raise FoomuuriDbusException( f'Not implemented: change {source} to "{zone}"' ) @dbus.service.method( 'org.fedoraproject.FirewallD1.zone', in_signature='ss', out_signature='s', sender_keyword='sender', ) def removeSource( self, zone: str, source: str, sender: typing.Optional[str] = None ) -> str: """Remove source from zone. See above getZoneOfSource().""" self.verify_polkit_privilege(sender, 'fi.foobar.Foomuuri1.config') raise FoomuuriDbusException( f'Not implemented: remove {source} from "{zone}"' ) def command_dbus() -> None: """Start D-Bus daemon.""" # Import gi here as it is heavy module to load. try: # ruff: ignore[import-outside-top-level] # pylint: disable=import-outside-toplevel import gi.repository.GLib except ImportError: fail('No python-gobject / python-gi installed') INTERNAL.keep_going = 1 while INTERNAL.keep_going: # Read minimal config config = minimal_config() zones = parse_config_zones(config) daemonize() # Initialize D-Bus dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) bus = dbus.SystemBus() # Foomuuri D-Bus calls try: foomuuri_name = dbus.service.BusName( 'fi.foobar.Foomuuri1', bus ) foomuuri_name.get_name() # Dummy call to get rid of pylint except dbus.exceptions.DBusException: fail("Can't bind to system D-Bus: fi.foobar.Foomuuri1") foomuuri_object = DbusFoomuuri(bus, '/fi/foobar/Foomuuri1', zones) # Firewalld emulation calls, if enabled in foomuuri{} config firewalld_object = None if CONFIG.dbus_firewalld == 'yes': try: firewalld_name = dbus.service.BusName( 'org.fedoraproject.FirewallD1', bus ) firewalld_name.get_name() # Dummy call for pylint except dbus.exceptions.DBusException: fail( "Can't bind to system D-Bus: " 'org.federaproject.FirewallD1' ) firewalld_object = DbusFirewalld( bus, '/org/fedoraproject/FirewallD1', zones ) # Define reload/stop signal handler mainloop = gi.repository.GLib.MainLoop() def signal_handler( sig: int, _dummy_frame: typing.Any, mainloop: typing.Any = mainloop, ) -> None: if sig == signal.SIGINT: INTERNAL.keep_going = 0 notify('STOPPING=1') else: notify('RELOADING=1') mainloop.quit() signal.signal(signal.SIGHUP, signal_handler) signal.signal(signal.SIGINT, signal_handler) save_file(CONFIG.run_dir / 'foomuuri-dbus.pid', [str(os.getpid())]) # Start processing messages verbose('D-Bus handler ready', 0) notify('READY=1') mainloop.run() # Reload signal received. Disconnect from D-Bus. foomuuri_object.remove_from_connection() del foomuuri_object del foomuuri_name if firewalld_object: firewalld_object.remove_from_connection() del firewalld_object del firewalld_name del bus def command_set() -> int: """Modify runtime config by calling D-Bus methods.""" if ( len(INTERNAL.parameters) != 4 or INTERNAL.parameters[0] != 'interface' or INTERNAL.parameters[2] != 'zone' ): command_help() interface = INTERNAL.parameters[1] zone = INTERNAL.parameters[3] try: bus = dbus.SystemBus() obj = bus.get_object('fi.foobar.Foomuuri1', '/fi/foobar/Foomuuri1') except dbus.exceptions.DBusException as error: fail(f"Can't connect to Foomuuri D-Bus: {error}") try: if zone in {'', '-'}: obj.removeInterface('', interface) else: obj.changeZoneOfInterface(zone, interface) except dbus.exceptions.DBusException as error: fail(f"Can't modify interface: {error}") return 0 else: # HAVE_DBUS is false def command_dbus() -> int: """Start D-Bus daemon.""" fail('No python-dbus installed') return 0 def command_set() -> int: """Modify runtime config by calling D-Bus methods.""" fail('No python-dbus installed') return 0 def remove_filters(text: str) -> str: """Remove tailing flags and filters from hostname or URL.""" return text.split('|', maxsplit=1)[0] def iplist_verify_filters(name: str, values: list[str], fileline: str) -> None: """Verify that iplist entry's filter is known.""" for value in values: for spec in value.split('|')[1:]: if spec == 'missing-ok': # Obsolete, replaced by "missing_ok=yes" continue if not spec.startswith( ( 'shell:', 'json:', 'html:', 'xml:', ) ): fail( f'{fileline}Unknown filter "|{spec}" in iplist ' f'"{name} {value}"', fatal=False, ) elif not spec.split(':', maxsplit=1)[1]: fail( f'{fileline}Filter "|{spec}" requires arguments ' f'in iplist "{name} {value}"', fatal=False, ) def resolve_one_hostname(hostname: str) -> typing.Optional[set]: """Resolve hostname and return its IP addresses.""" if ':' in hostname: # Allow "|missing-ok" but not "|json:" warning(f'Iplist filters are not supported for hostnames: {hostname}') with contextlib.suppress(socket.gaierror): return { addr[4][0] for addr in socket.getaddrinfo(remove_filters(hostname), None) } return None def active_sets() -> set[str]: """Return set names in currently active firewall.""" ret = set() data = nft_json('list sets table inet foomuuri') if not data: return ret for item in data['nftables']: if 'set' in item: ret.add(f'@{item["set"]["name"][:-2]}') return ret def get_url(url: str, options: IPListSourceOptions) -> typing.Optional[str]: """Download URL and return it as text, or None if failure.""" def get_response_content( response: typing.Any, max_size: int ) -> io.BytesIO: """Validate and return response content.""" if response.status != 200: raise ValueError(f'status code: {response.status}') content_length = int(response.headers.get('content-length', 0)) if content_length > max_size: raise ValueError( f'content length {content_length} exceeds {max_size}' ) content = io.BytesIO() for chunk in response.stream(INTERNAL.url_chunk_size): content.write(chunk) if content.tell() > max_size: raise ValueError(f'content length exceeds {max_size}') return content try: # urllib3 is optional, needed to download iplist with HTTP GET. # Import it here as it is heavy module to load. # ruff: ignore[import-outside-top-level] # pylint: disable=import-outside-toplevel import urllib3 with urllib3.PoolManager().request( method='GET', url=remove_filters(url), decode_content=False, preload_content=False, retries=urllib3.Retry(**INTERNAL.url_get_retries), timeout=urllib3.Timeout(**INTERNAL.url_get_timeout), ) as response: return ( get_response_content(response, options.max_size) .getvalue() .decode('utf-8', errors='ignore') ) except ImportError: warning( f'Can\'t download iplist URL "{remove_filters(url)}": ' f'No python-urllib3 installed' ) except (ValueError, urllib3.exceptions.HTTPError) as error: if not options.missing_ok: warning( f'Can\'t download iplist URL "{remove_filters(url)}": {error}' ) finally: with contextlib.suppress(NameError): response.drain_conn() response.release_conn() def get_file( wildcard: str, options: IPListSourceOptions, ) -> typing.Optional[str]: """Read all files and return them as single text.""" wildpath = pathlib.Path(remove_filters(wildcard)) filenames = sorted(wildpath.parent.glob(wildpath.name)) if not filenames: if not options.missing_ok: warning( f'Can\'t read iplist file "{remove_filters(wildcard)}": ' f'No such file' ) return None text = '' for filename in filenames: try: content = filename.read_text(encoding='utf-8') except PermissionError as error: warning(f'Can\'t read iplist file "{filename}": {error}') return None text = text + '\n' + content return text def parse_duration(timespec: str, fallback: int) -> int: """Parse timespec 1w2d3h4m5s to seconds.""" units = ( ('w', 'weeks'), ('d', 'days'), ('h', 'hours'), ('m', 'minutes'), ('s', 'seconds'), ) delta = collections.defaultdict(int) timespec = timespec.replace(' ', '').replace('_', '') for unit in units: pattern = rf'^(\d+){unit[0]}' if match := re.search(pattern, timespec): delta[unit[1]] = int(match[1]) timespec = re.sub(pattern, '', timespec, count=1) if delta and not timespec: with contextlib.suppress(OverflowError): return int(dt.timedelta(**delta).total_seconds()) return fallback def seconds_to_duration(seconds: int) -> str: """Convert seconds to duration string.""" days, seconds = divmod(seconds, 86400) hours, seconds = divmod(seconds, 3600) minutes, seconds = divmod(seconds, 60) if days: return f'{days}d{hours:02d}h{minutes:02d}m{seconds:02d}s' if hours: return f'{hours}h{minutes:02d}m{seconds:02d}s' if minutes: return f'{minutes}m{seconds:02d}s' return f'{seconds}s' def iterate_set_elements( data: typing.Optional[dict], ) -> typing.Iterator[tuple[str, typing.Optional[int]]]: """Iterate elements from "nft --json list set" output.""" for toplevel in (data or {}).get('nftables', []): for elem in toplevel.get('set', {}).get('elem', []): if isinstance(elem, dict) and 'elem' in elem: ipaddr = elem['elem']['val'] expire = elem['elem'].get('expires') else: ipaddr = elem expire = None if isinstance(ipaddr, str): yield ipaddr, expire elif 'range' in ipaddr: yield f'{ipaddr["range"][0]}-{ipaddr["range"][1]}', expire else: yield ( (f'{ipaddr["prefix"]["addr"]}/{ipaddr["prefix"]["len"]}'), expire, ) def command_iplist_status() -> int: """List iplist(s) name and entries count.""" config = minimal_config() known = parse_config_iplist(config) table = [('Iplist', 'Entries')] error = 0 for iplist_name in INTERNAL.parameters[1:] or sorted(known): iplist_name = iplist_name.removeprefix('@') if iplist_name.endswith(('_4', '_6')): data4 = nft_json(f'list set inet foomuuri {iplist_name}') data6 = {} else: data4 = nft_json(f'list set inet foomuuri {iplist_name}_4') data6 = nft_json(f'list set inet foomuuri {iplist_name}_6') if data4 is None and data6 is None: warning(f'Unknown iplist: @{iplist_name}') error = 1 continue count = len( list( itertools.chain( iterate_set_elements(data4), iterate_set_elements(data6) ) ) ) table.append((f'@{iplist_name}', int_to_human(count))) print_table(data=table, align='<>', header=True) return error def iplist_element_contains( element: str, needle: typing.Union[ipaddress.IPv4Network, ipaddress.IPv6Network], ) -> bool: """Check if needle is a subset of iplist element.""" if '-' in element: first, last = map(ipaddress.ip_address, element.split('-', 1)) else: network = ipaddress.ip_network(element, strict=False) first, last = network.network_address, network.broadcast_address return ( needle.version == first.version == last.version and first <= needle.network_address and needle.broadcast_address <= last ) def command_iplist_list() -> int: """List iplist entries filtered by iplist names or ipaddresses.""" config = minimal_config() known = parse_config_iplist(config) table = [('Iplist', 'IPAddress', 'Timeout')] error = 0 needles = [] iplists = [] needle_matched = False for arg in INTERNAL.parameters[1:]: if get_ip_family(arg) != 0: if any(c in {'-', '[', ']'} for c in arg): fail( 'Filtering by IP range, negative or bracketed value ' f'is not supported: {arg}' ) needles.append(ipaddress.ip_network(arg, strict=False)) else: iplists.append(arg) for iplist_name in iplists or sorted(known): iplist_name = iplist_name.removeprefix('@') if iplist_name.endswith(('_4', '_6')): data4 = nft_json(f'list set inet foomuuri {iplist_name}') data6 = {} else: data4 = nft_json(f'list set inet foomuuri {iplist_name}_4') data6 = nft_json(f'list set inet foomuuri {iplist_name}_6') if data4 is None and data6 is None: warning(f'Unknown iplist: @{iplist_name}') error = 1 continue elems = sorted(iterate_set_elements(data4)) + sorted( iterate_set_elements(data6) ) if needles: elems = [ elem for elem in elems if any( iplist_element_contains(elem[0], needle) for needle in needles ) ] needle_matched = needle_matched or bool(elems) if not elems and not needles: table.append((f'@{iplist_name}', '', '')) else: table.extend( ( f'@{iplist_name}', elem, '-' if timeout is None else seconds_to_duration(timeout), ) for elem, timeout in elems ) print_table(data=table, align='<<>', header=True) return error or int(bool(needles) and not needle_matched) def command_iplist_add() -> int: """Add entries to iplist.""" config = minimal_config() iplists = parse_config_iplist(config) cache = IPListSourceCache.read(iplists) iplist_name = Converters.canonical_iplist_name(INTERNAL.parameters[1]) if iplist_name not in iplists: fail(f'Unknown iplist: {iplist_name}') timeout = iplists[iplist_name].options.element_timeout if not timeout: timeout = max( iplists[iplist_name].options.url_timeout, iplists[iplist_name].options.dns_timeout, ) # Add entries to cache and apply it cache.add_manual(iplist_name, INTERNAL.parameters[2:], timeout) return iplist_output_and_apply( iplists, cache, {iplist_name}, {iplist_name}, ) def command_iplist_del() -> int: """Delete entries from iplist.""" config = minimal_config() iplists = parse_config_iplist(config) cache = IPListSourceCache.read(iplists) iplist_name = Converters.canonical_iplist_name(INTERNAL.parameters[1]) if iplist_name not in iplists: fail(f'Unknown iplist: {iplist_name}') # Delete entries from cache and apply it cache.delete_manual(iplist_name, INTERNAL.parameters[2:]) return iplist_output_and_apply( iplists, cache, {iplist_name}, {iplist_name}, ) def command_iplist_flush() -> int: """Delete all IP addresses from all or specific iplist(s).""" config = minimal_config() iplists = parse_config_iplist(config) cache = IPListSourceCache.read(iplists) update_only = set() target = set(INTERNAL.parameters[1:] or iplists) for name in target: iplist_name = Converters.canonical_iplist_name(name) if iplist_name not in iplists: fail(f'Unknown iplist: {iplist_name}') update_only.add(iplist_name) cache.flush_manual(iplist_name) return iplist_output_and_apply(iplists, cache, update_only, update_only) def lxml_parser( source: str, parser: typing.Any, path: str, text: str ) -> typing.Optional[str]: """Parse html/xml text with lxml.""" # pylint: disable=c-extension-no-member try: root = lxml.etree.fromstring(text, parser) nodes = root.xpath(path) except lxml.etree.LxmlError as error: warning(f'Failed to parse iplist xml/html "{source}": {error}') return None return '\n'.join(nodes) def iplist_apply_filters(source: str, content: str) -> str: """Apply filters to iplist content.""" text = content for spec in source.split('|')[1:]: if not text: break # pylint: disable=c-extension-no-member if spec.startswith('shell:'): try: text = run_program_pipe(shlex.split(spec[6:]), text) except ValueError: # "|shell:"foo" => "No closing quotation" warning(f'Invalid iplist filter: |{spec}') elif spec.startswith('json:'): text = run_program_pipe(['jq', '--raw-output', spec[5:]], text) elif spec.startswith('html:'): if HAVE_LXML: text = lxml_parser( source, lxml.etree.HTMLParser(), spec[5:], text ) else: warning('No python-lxml installed') elif spec.startswith('xml:'): if HAVE_LXML: text = lxml_parser( source, lxml.etree.XMLParser(), spec[4:], text ) else: warning('No python-lxml installed') return text or '' def iplist_parse_content(source: str, text: str) -> list[str]: """Parse downloaded iplist content to list of IP addresses.""" # Apply filters text = iplist_apply_filters(source, text) if not text: return [] # Collect IP addresses addresses = [] errors = 0 for line in text.splitlines(): for value in line.split(): if value.startswith(('#', ';')): # Remove comments break if get_ip_family(value, strict=True): addresses.append(value) else: errors += 1 if errors <= 5: # Print warning for first five errors warning( f'Unknown entry in iplist "{source}": {value[:64]}' ) if errors > 5: warning( f'{errors - 5} additional warnings suppressed for ' f'iplist "{source}"' ) return addresses # Map IP addresses to expiration timestamps. IPListAddresses = dict[str, int] class IPListSourceCacheEntry(typing.TypedDict, total=False): """Store persistent source cache entry.""" ip: IPListAddresses dirty: bool refresh: int # Timestamp when this entry was created class IPListSourceCache(collections.UserDict[str, IPListSourceCacheEntry]): """Manage persistent source cache entries.""" # Timeouts over expire_max are normalized to expire_never. expire_max = 2 * 365 * 86400 expire_never = 4_000_000_000 @staticmethod def _normalize_expire(now: int, timeout: int) -> int: """Add timeout to now, or return "never" for large timeouts.""" if timeout > IPListSourceCache.expire_max: return IPListSourceCache.expire_never return now + timeout def _ensure_entry(self, source: str) -> IPListSourceCacheEntry: """Return source cache entry, creating it if missing.""" return self.setdefault( source, {'ip': {}, 'dirty': False, 'refresh': int(time.time())}, ) def write(self, iplists: IPLists) -> None: """Remove transient metadata and save source cache to disk.""" # Remove empty sources that don't have missing_ok. # Empty missing_ok sources are kept in cache so that their # "refresh" timestamp is saved to cache. They will be then # refreshed on normal intervals, not on every run. for iplist in iplists.values(): if iplist.options.missing_ok: continue for source in iplist.sources: if source in self and not self[source]['ip']: verbose(f'Deleting empty "{source}" from iplist cache') del self[source] # Remove metadata and empty manual-lists for source, entry in list(self.items()): entry.pop('dirty', None) if source.startswith('manual.@'): entry.pop('refresh', None) if not entry['ip']: del self[source] save_file(state_file('iplist_cache_file'), self.data) def add_manual( self, iplist_name: str, addresses: typing.Iterable[str], timeout: int ) -> None: """Add addresses to manual source and update timeouts.""" now = int(time.time()) expire = self._normalize_expire(now, timeout) entry = self._ensure_entry(f'manual.{iplist_name}') for address in addresses: if get_ip_family(address, strict=True): entry['ip'][address] = expire entry['dirty'] = True continue parsed_timeout = parse_duration(address, 0) if not parsed_timeout: fail(f'Invalid IP address {address}') expire = self._normalize_expire(now, parsed_timeout) def delete_manual( self, iplist_name: str, addresses: typing.Iterable[str] ) -> None: """Delete addresses from manual source.""" entry = self._ensure_entry(f'manual.{iplist_name}') for address in addresses: entry['ip'].pop(address, None) entry['dirty'] = True def flush_manual(self, iplist_name: str) -> None: """Delete all addresses from manual source.""" self._clear_entry(f'manual.{iplist_name}') def _clear_entry(self, source: str) -> None: """Remove addresses from source cache entry and mark it dirty.""" entry = self._ensure_entry(source) entry['ip'].clear() entry['dirty'] = True def _expire_or_use_cache( self, source: str, options: IPListSourceOptions, now: int ) -> bool: """Expire timed-out source cache entry, or return True when usable.""" entry = self.get(source) if not entry: return False refresh = entry['refresh'] if now >= refresh + options.timeout: self._clear_entry(source) return False # Empty missing_ok source is used unless refresh is forced if not entry['ip'] and (not options.missing_ok or INTERNAL.force >= 0): return False if now >= refresh + options.refresh or INTERNAL.force >= 0: return False verbose(f'Using cached value for "{remove_filters(source)}"') return True def _append_or_overwrite( self, source: str, addresses: dict[str, int], options: IPListSourceOptions, default_is_overwrite: bool, now: int, ) -> None: """Append or overwrite addresses to source.""" # pylint: disable=too-many-arguments # pylint: disable=too-many-positional-arguments if options.overwrite is None: overwrite_mode = default_is_overwrite else: overwrite_mode = options.overwrite entry = self._ensure_entry(source) if overwrite_mode: entry['ip'] = addresses else: entry['ip'].update(addresses) entry['dirty'] = True entry['refresh'] = now def refresh_url_or_file( self, source: str, options: IPListSourceOptions ) -> None: """Refresh URL or file source with cache check.""" now = int(time.time()) if self._expire_or_use_cache(source, options, now): return value = ( get_url(source, options) if source.startswith(('https:', 'http:')) else get_file(source, options) ) if value is not None: # Replace list, don't append. These lists are always complete # address lists. expire = self._normalize_expire(now, options.timeout) addresses = dict.fromkeys( iplist_parse_content(source, value), expire ) self._append_or_overwrite(source, addresses, options, True, now) verbose( f'Iplist content for "{source}" refreshed, ' f'{len(addresses)} entries' ) if not addresses and not options.missing_ok: warning( f'No IP addresses listed in "{remove_filters(source)}"' ) def resolve_hostnames( self, hostnames: typing.Mapping[str, IPListSourceOptions] ) -> None: """Resolve hostnames and save their addresses to cache.""" # Collect list of hostnames to resolve now = int(time.time()) todo = { hostname for hostname, options in hostnames.items() if not self._expire_or_use_cache(hostname, options, now) } if not todo: return # Resolve them verbose(f'DNS lookup for: {", ".join(todo)}') with concurrent.futures.ThreadPoolExecutor() as executor: jobs = { executor.submit(resolve_one_hostname, hostname): hostname for hostname in todo } for future in concurrent.futures.as_completed(jobs): hostname = jobs[future] addrlist = future.result() if addrlist: # Append/update addresses, don't replace whole list. # Some cloud services will change server IP a lot, and some # will return only partial list of addresses. By appending # old addresses will still work until lookup timeout. expire = self._normalize_expire( now, hostnames[hostname].timeout ) addresses = dict.fromkeys(addrlist, expire) self._append_or_overwrite( hostname, addresses, hostnames[hostname], False, now, ) verbose( f'Hostname "{remove_filters(hostname)}" resolved ' f'to: {", ".join(addrlist)}' ) elif not hostnames[hostname].missing_ok: warning( f'No IP address found for hostname ' f'"{remove_filters(hostname)}" in iplist' ) def _read_old_format(self) -> None: """Read old iplist.fw (v0.27) files and convert them to new format. Example entries: # add element inet foomuuri fi_6 { 2a13:aec0::/32 timeout 10d } add element inet foomuuri foo_4 { 10.0.9.98 timeout 24h } add element inet foomuuri dynamic_4 { 10.0.0.4 timeout 798169s } # 2024-12-11T17:16:28 """ for file_key in ('resolve_file', 'iplist_file', 'iplist_manual_file'): try: content = state_file(file_key).read_text('utf-8') except OSError: continue # Silently ignore all errors for line in content.splitlines(): line = line.removeprefix('# ') items = line.split() if ( not line.startswith('add element inet') or len(items) not in {10, 12} or not get_ip_family(items[6]) ): continue source = f'manual.@{items[4][:-2]}' if len(items) == 12: # iplist_manual_file has expire timestamp. It's almost # in ISO 8601 format, but without timezone 'Z'. timeout = int( dt.datetime.fromisoformat(f'{items[11]}Z').timestamp() ) else: timeout = int(time.time()) + parse_duration(items[8], 0) self._ensure_entry(source)['ip'][items[6]] = timeout def _prune_source( self, source: str, iplists: IPLists, configured_sources: set[str] ) -> bool: """Delete legacy or unconfigured source, return True when deleted.""" if source.startswith('@'): del self[source] verbose(f'Deleting legacy iplist "{source}" from cache') return True if source.startswith('manual.@'): # Manually added iplist is kept when present in config configured = source[7:] in iplists else: # URL/file/hostname source is kept when present in config configured = source in configured_sources if not configured: # Source in cache, not in config del self[source] verbose(f'Deleting source "{source}" from cache, not in config') return True return False @classmethod def read(cls, iplists: IPLists) -> 'IPListSourceCache': """Read persistent source cache from disk.""" self = cls() filename = state_file('iplist_cache_file') try: self.update(json.loads(filename.read_text('utf-8'))) except (json.decoder.JSONDecodeError, UnicodeDecodeError) as error: warning(f'File {filename}: Broken JSON format: {error}') except FileNotFoundError: self._read_old_format() except OSError as error: fail(f"File {filename}: Can't read: {error}") configured_sources = { source for iplist in iplists.values() for source in iplist.sources } now = int(time.time()) for source in list(self): # Delete legacy or unconfigured source if self._prune_source(source, iplists, configured_sources): continue entry = self[source] # Top level refresh must be present if entry.get('refresh') is None: entry['refresh'] = now # Find and delete expired addresses for addr in [ addr for addr, timeout in entry['ip'].items() if now >= timeout ]: verbose(f'Deleting expired iplist "{source}" entry "{addr}"') del entry['ip'][addr] return self @dataclasses.dataclass(eq=False) class IPListValue: """Hold transient iplist addresses value.""" name: str addresses: IPListAddresses = dataclasses.field(default_factory=dict) dirty: bool = False def _copy_addresses( self, source_cache: IPListSourceCache, source: str ) -> None: """Copy addresses from source cache entry.""" if source not in source_cache: return if source_cache[source].get('dirty'): self.dirty = True for address, timeout in source_cache[source]['ip'].items(): old_timeout = self.addresses.get(address, timeout) self.addresses[address] = max(old_timeout, timeout) @classmethod def from_sources( cls, source_cache: IPListSourceCache, name: str, sources: typing.Iterable[str], ) -> 'IPListValue': """Build a transient iplist from cached and static sources.""" self = cls(name=name) static_addresses: list[str] = [] for source in sources: if get_ip_family(source, strict=True): static_addresses.append(source) else: self._copy_addresses(source_cache, source) for address in static_addresses: self.addresses[address] = IPListSourceCache.expire_never if static_addresses: self.dirty = True self._copy_addresses(source_cache, f'manual.{name}') return self def parse_config_iplist(config: dict) -> IPLists: """Parse iplist{} sections from config.""" # pylint: disable=too-many-branches def parse_option( options: IPListOptions, option: str, fileline: str ) -> bool: """Option parser helper.""" try: return options.parse_option(option) except AttributeError: fail(f'{fileline}Invalid iplist{{}} option name: {option}') except ValueError: fail(f'{fileline}Invalid iplist{{}} option value: {option}') return False default_options = IPListOptions() iplists = IPLists() if config.get('resolve'): warning('Section "resolve" is obsolete, use "iplist" instead') for fileline, line in config.get('resolve', []) + config.get('iplist', []): # Parse old default option syntax if line[0] in {'timeout', 'refresh'}: option = line[0] value = ' '.join(line[1:]) warning( f'{fileline}Iplist "{option}" is obsolete, use ' f'"dns_{option}" or "url_{option}" instead' ) parse_option(default_options, f'dns_{option}={value}', fileline) parse_option(default_options, f'url_{option}={value}', fileline) continue # Parse one or more 'equal-sign'-style default options in single line for index, default_option in enumerate(line): if not parse_option(default_options, default_option, fileline): if index == 0: break fail(f'{fileline}Unsupported syntax: {" ".join(line)}') else: continue # Parse 'space-separated'-style default option option = f'{line[0]}={" ".join(line[1:])}' if parse_option(default_options, option, fileline): continue # Is it an iplist? if not Validators.str_iplist_name(name := line[0]): fail(f'{fileline}Invalid iplist name: {" ".join(line)}') if name not in iplists: # Create new iplist with default_options collected so far iplists[name] = IPList( options=IPListOptions(*dataclasses.astuple(default_options)) ) if len(line) >= 3 and line[1] == '+': # Append iplist sources options = line[2:] else: if iplists[name].sources: # Overwrite iplist sources iplists[name].sources.clear() warning( f'{fileline}Overwriting iplist "{name}" with value ' f'"{" ".join(line[1:])}"' ) options = line[1:] # Parse the rest of iplist definition for option in options: # Ignore insignificant tokens if option in {'-', '+'}: continue # Inline option? if parse_option(iplists[name].options, option, fileline): continue # Add as a source iplists[name].sources.append(option) if '|missing-ok' in option: # Parse old style |filter iplists[name].options.missing_ok = True warning( f'{fileline}iplist{{}} filter "|missing-ok" in "{name}" ' f'is obsolete, use "missing_ok=yes" instead' ) iplist_verify_filters(name, iplists[name].sources, fileline) if CONFIG.verbose >= 2: for name, iplist in iplists.items(): verbose(f'{name:9s} {" ".join(iplist.sources)}') return iplists def iplist_collect_resolve_list( iplists: IPLists, update_only: set[str], ) -> tuple[dict[str, IPListSourceOptions], dict[str, IPListSourceOptions]]: """Collect URLs and hostnames to be resolved.""" sources = collections.defaultdict(IPListSourceOptions) hostnames = collections.defaultdict(IPListSourceOptions) for iplist_name, iplist in iplists.items(): if iplist_name not in update_only: continue for source in iplist.sources: if source.startswith(('https://', 'http://', '.', '/')): for option in ('timeout', 'refresh', 'max_size'): sources[source].update_if_less( option, iplist.options[f'url_{option}'] ) sources[source].set_option( 'overwrite', iplist.options.overwrite, source ) sources[source].set_option( 'missing_ok', iplist.options.missing_ok, source ) elif not get_ip_family(source, strict=True): for option in ('timeout', 'refresh'): hostnames[source].update_if_less( option, iplist.options[f'dns_{option}'] ) hostnames[source].set_option( 'overwrite', iplist.options.overwrite, source ) hostnames[source].set_option( 'missing_ok', iplist.options.missing_ok, source ) if CONFIG.verbose >= 2: verbose(f'Sources: {sources}') verbose(f'Hostnames: {hostnames}') return sources, hostnames def iplist_output_add_element( iplist: IPListValue, cmd_is_refresh: bool, now: int, ) -> None: """Output nft commands for a transient iplist. Auto-merge in nftables set requires that adds are done set by set, not mixing foo_4 and foo_6 adds. """ # Collect addresses to IPv4 and IPv6 sets updates = { 4: set(), 6: set(), } for address, timeout in iplist.addresses.items(): if ':' in address: updates[6].add((address, timeout)) else: updates[4].add((address, timeout)) # Output add commands set_name = iplist.name.removeprefix('@') for ipv, addresses in updates.items(): if cmd_is_refresh: out(f'flush set inet foomuuri {set_name}_{ipv}') if not addresses: continue out(f'add element inet foomuuri {set_name}_{ipv} {{') for address, timeout in sorted(addresses): # nft doesn't support "timeout 0s" so use "1s". This can happen # if address is expired after cache read and before writing it # to output. seconds = max(1, timeout - now) if seconds > IPListSourceCache.expire_max: out(f'{address},') else: out(f'{address} timeout {seconds}s,') out('}') def iplist_output_values( iplists: IPLists, source_cache: IPListSourceCache, currently_active: set[str], update_only: set[str], cmd_is_refresh: bool, ) -> int: """Output nft commands to add iplist values.""" now = int(time.time()) error = 0 for iplist_name in sorted(iplists): if iplist_name not in update_only: continue # Filtered away by command line parameter if iplist_name not in currently_active: warning( f'Iplist "{iplist_name}" does not exist in currently active ' f'firewall' ) continue # Build transient iplist from its sources sources = iplists[iplist_name].sources iplist = IPListValue.from_sources(source_cache, iplist_name, sources) # Check that this iplist should be updated if not iplist.dirty and INTERNAL.force < 0: verbose(f'Iplist "{iplist_name}" is clean, skipping update') continue # Print error if iplist had sources, but result is empty. # Don't print if called from "foomuuri start". if sources and not iplist.addresses and cmd_is_refresh: if iplists[iplist_name].options.missing_ok: warning(f'Iplist "{iplist_name}" is empty') else: fail(f'Iplist "{iplist_name}" is empty', False) error = 2 else: verbose( f'Updating iplist "{iplist_name}", ' f'{len(iplist.addresses)} entries' ) # Add elements to sets iplist_output_add_element(iplist, cmd_is_refresh, now) return error def iplist_output_and_apply( iplists: IPLists, source_cache: IPListSourceCache, currently_active: set[str], update_only: set[str], ) -> int: """Output and apply iplist values, save cache.""" # Generate nft commands output_error = iplist_output_values( iplists, source_cache, currently_active, update_only, True, ) if not OUT: # Nothing to do return output_error # There was some updates. Save cache to disk. source_cache.write(iplists) # Apply nft commands lines = '\n'.join(save_final(None)) nft_error = run_program_rc(CONFIG.nft_bin + ['--file', '-'], stdin=lines) if nft_error: fail( f'Failed to update iplists, nftables error code {nft_error}', False, ) return output_error | nft_error def command_iplist_refresh() -> int: """Refresh iplist entries.""" # Read minimal config and parse it config = minimal_config() iplists = parse_config_iplist(config) # Parse which iplists should be updated. Default is to update all iplists. # This can be filtered with "foomuuri iplist refresh IPLIST...". ret = 0 if len(INTERNAL.parameters) == 1: update_only = set(iplists) else: update_only = { Converters.canonical_iplist_name(name) for name in INTERNAL.parameters[1:] } for iplist_name in update_only: if iplist_name not in iplists: warning(f'Unknown iplist: {iplist_name}') ret = 1 # Read URLs and files to cache cache = IPListSourceCache.read(iplists) sources, hostnames = iplist_collect_resolve_list(iplists, update_only) for source, options in sources.items(): cache.refresh_url_or_file(source, options) # Resolve hostnames to cache cache.resolve_hostnames(hostnames) # Read active set names from currently running ruleset currently_active = active_sets() # Special case for testing (non-root) or faked nft_bin if not currently_active and ( not INTERNAL.root_power or CONFIG.nft_bin == ['true'] ): currently_active = set(iplists) # Make sure systemd's TimeoutStartSec doesn't trigger while updating # ruleset or writing cache file. notify('EXTEND_TIMEOUT_USEC=120000000') # 2 minutes # Apply updates to ruleset ret |= iplist_output_and_apply( iplists, cache, currently_active, update_only, ) notify('READY=1') notify('STOPPING=1') return ret def get_counter_values() -> typing.Optional[dict[str, tuple[int, int]]]: """Get named counter values from active ruleset.""" data = nft_json('list counters table inet foomuuri') if not data: # Foomuuri is not running return None counters = {} for toplevel in data.get('nftables', []): counter = toplevel.get('counter') name = (counter or {}).get('name') if not name: continue packet_value = counter.get('packets', 0) byte_value = counter.get('bytes', 0) counters[name] = (packet_value, byte_value) return counters def command_counter_list() -> int: """List named counters.""" # Read config if available for nft binary path. minimal_config(require_etc_config=False) list_params = INTERNAL.parameters[1:] # Parse counter data from kernel counters = get_counter_values() if counters is None: fail("Can't read counter data from kernel") return 1 if not counters: fail('No named counters defined in currently active firewall') # Print counter data table = [('Counter', 'Packets', 'Bytes')] error = 0 for name in list_params or counters: values = counters.get(name) if not values: warning(f'Unknown counter: {name}') error = 1 continue table.append((name, int_to_human(values[0]), int_to_human(values[1]))) print_table(data=table, align='<>>', header=True) return error def command_counter_reset() -> int: """Reset named counter values.""" # Read config if available for nft binary path. minimal_config(require_etc_config=False) command_params = INTERNAL.parameters[1:] # Reset counter values counters = get_counter_values() or {} error = 0 for name in command_params or counters: if name not in counters: warning(f'Unknown counter: {name}') error = 1 else: error |= nft_command( f'reset counter inet foomuuri {name}', print_output=False, ) return error def command_macro_list() -> int: """List default system macros and user defined macros. Don't require config files so that this can be run before initial configuration. """ macros = parse_config_macros(read_config(require_etc_config=False)) list_params = INTERNAL.parameters[1:] print('macro {') table = [ ('', macro, f'{" ".join(macros[macro])}') for macro in sorted(macros) if not macro.startswith('_template_') and ( not list_params or macro in list_params or any(item in list_params for item in macros[macro]) ) ] print_table(data=table, header=False) print('}') return 0 def command_ruleset_list() -> int: """List currently active ruleset.""" # List full ruleset if no zone-zone specified. Read config if available # for nft binary path. list_params = INTERNAL.parameters[1:] if not list_params: minimal_config(require_etc_config=False) return nft_command('list ruleset') # Read config for correct nft_bin value and zone-zone names config = minimal_config() zones = parse_config_zones(config) zonepairs = [f'{szone}-{dzone}' for szone in zones for dzone in zones] # List zone-zone rules error = 0 for zone in list_params: if zone not in zonepairs: warning(f'Unknown zone-zone: {zone}') error = 1 continue for ipv in (4, 6): error |= nft_command(f'list chain inet foomuuri {zone}_{ipv}') return error def command_subcommand() -> int: """Parse "foomuuri ruleset|iplist|..." subcommand.""" # Use default subcommand if not specified in command line if not INTERNAL.parameters: INTERNAL.parameters = [ { 'counter': 'list', 'iplist': 'status', 'macro': 'list', 'ruleset': 'list', }.get(INTERNAL.command) ] # Find correct handler. min_args includes "list" argument. handler, min_args = { ('counter', 'list'): (command_counter_list, 1), ('counter', 'reset'): (command_counter_reset, 1), ('iplist', 'add'): (command_iplist_add, 3), ('iplist', 'del'): (command_iplist_del, 3), ('iplist', 'flush'): (command_iplist_flush, 1), ('iplist', 'list'): (command_iplist_list, 1), ('iplist', 'refresh'): (command_iplist_refresh, 1), ('iplist', 'status'): (command_iplist_status, 1), ('macro', 'list'): (command_macro_list, 1), ('ruleset', 'list'): (command_ruleset_list, 1), }.get((INTERNAL.command, INTERNAL.parameters[0]), (None, 0)) if handler and len(INTERNAL.parameters) >= min_args: return handler() return command_help() def command_list() -> int: """Backward compability command handler.""" if not INTERNAL.parameters: warning( 'Command "foomuuri list" is obsolete, use ' '"foomuuri ruleset list" instead' ) return command_ruleset_list() if INTERNAL.parameters[0] == 'macro': warning( 'Command "foomuuri list macro" is obsolete, use ' '"foomuuri macro list" instead' ) return command_macro_list() if INTERNAL.parameters[0] == 'counter': warning( 'Command "foomuuri list counter" is obsolete, use ' '"foomuuri counter list" instead' ) return command_counter_list() warning( 'Command "foomuuri list" is obsolete, use ' '"foomuuri ruleset list" instead' ) INTERNAL.parameters = ['list'] + INTERNAL.parameters return command_ruleset_list() def command_reload() -> int: """Run start and refresh iplist.""" # Use same args args = [sys.argv[0]] args.extend(arg for arg in sys.argv[1:] if arg.startswith('--')) # Run commands for sub, fatal in ((['start'], True), (['iplist', 'refresh'], False)): ret = run_program_rc(args + sub) if ret and fatal: return ret return 0 def alarm_handler(_dummy_signum: int, _dummy_frame: typing.Any) -> None: """Signal handler to catch input timeout.""" raise KeyboardInterrupt def input_timeout(prompt: str, timeout: int) -> str: """Ask something from user with timeout.""" signal.signal(signal.SIGALRM, alarm_handler) signal.signal(signal.SIGHUP, alarm_handler) signal.signal(signal.SIGINT, alarm_handler) signal.signal(signal.SIGTERM, alarm_handler) signal.alarm(timeout) try: return input(prompt) except (KeyboardInterrupt, EOFError): print() print('Timeout') finally: signal.alarm(0) return '' def command_try_reload() -> int: """Reload, ask confirmation, optionally revert to previous rules. Revert is not fail safe. Things can break if: - Zone is added/deleted to config and NetworkManager sends new "change to zone" request via D-Bus. This happens only if user reconfigures NetworkManager or attach ethernet cable or similar. - Needed entry is removed from iplist config and foomuuri-iplist.timer updates iplist contents. There is a long expiry time for entries so there is plenty of time to fix config. It is recommended to fix config immediately and run "foomuuri reload" or "foomuuri try-reload". """ # Get copy of currently active ruleset minimal_config() old_rules = run_program_pipe( CONFIG.nft_bin + ['list', 'table', 'inet', 'foomuuri'], None, check_error=False, ) if not old_rules or 'netlink: Error' in old_rules: # Not running as root or similar error if old_rules: print(old_rules.strip()) fail('Failed to read old ruleset') return 1 # Load new ruleset error = command_reload() if error: return error # Verify from user if ruleset is ok timeout = CONFIG.try_reload_timeout print(f'You have {timeout} seconds to accept the changes.') answer = input_timeout( 'Can you establish NEW connections (yes/no)? ', timeout ) if answer.lower().startswith('y'): return 0 # Revert to old ruleset and return errorcode 2. Revert to empty # ruleset if Foomuuri was not running. if not old_rules.startswith('table inet foomuuri {'): old_rules = '' warning('Revert to empty ruleset. Foomuuri was not running.') else: warning('Revert to old config') save_file( state_file('good_file'), ['table inet foomuuri', 'delete table inet foomuuri', '', old_rules], ) return ( run_program_rc(CONFIG.nft_bin + ['--file', state_file('good_file')]) + 2 ) def seconds_to_human(seconds: int) -> str: """Convert seconds int to human readable "19 days, 18:37" format.""" day = seconds // 86400 hour = (seconds // 3600) % 24 minute = (seconds // 60) % 60 second = seconds % 60 if day: return f'{day} days, {hour:02d}:{minute:02d}:{second:02d}' return f'{hour:02d}:{minute:02d}:{second:02d}' def monitor_state_command( targets: dict, groups: dict, cfg: dict, grouptarget: str, name: str ) -> None: """Run command if group/target state changes.""" # pylint: disable=too-many-locals # Log state change updown = 'up' if cfg['state'] else 'down' now = time.time() prev = cfg.get('state_time') history = None if prev: seconds = int(now - prev + 0.5) extra = f'previous change was {seconds_to_human(seconds)} ago' if grouptarget == 'target': for cons in range(cfg['history_size']): if cfg['history'][-1 - cons] != cfg['state']: break historycount = cfg['history'].count(cfg['state']) extra = ( f'{extra}, consecutive_{updown} {cons}, ' f'history_{updown} {historycount}' ) history = ''.join('.' if item else '!' for item in cfg['history']) else: extra = 'startup change' verbose(f'{grouptarget} {name} changed state to {updown}, {extra}', 0) if history: verbose(f'{grouptarget} {name} history: {history}', 0) cfg['state_time'] = cfg['last_down_interval'] = now monitor_run_command( targets, groups, cfg, grouptarget, name, updown, extra, history ) def monitor_run_command( # ruff: ignore[too-many-positional-arguments] targets: dict, groups: dict, cfg: dict, grouptarget: str, name: str, updown: str, extra: str, history: typing.Optional[str], ) -> None: """Run monitor's external command if configured. It will receive current state change event info and all states in environment variables. """ # pylint: disable=too-many-arguments # pylint: disable=too-many-positional-arguments proc = cfg[f'command_{updown}'] if not proc: return env = { # Change state event 'FOOMUURI_CHANGE_TYPE': grouptarget, 'FOOMUURI_CHANGE_NAME': env_cleanup(name), 'FOOMUURI_CHANGE_STATE': updown, 'FOOMUURI_CHANGE_LOG': extra, 'FOOMUURI_CHANGE_HISTORY': history or '', # List of configured targets 'FOOMUURI_ALL_TARGET': ' '.join(env_cleanup(item) for item in targets), # List of configured groups 'FOOMUURI_ALL_GROUP': ' '.join(env_cleanup(item) for item in groups), } for target, icfg in targets.items(): env[f'FOOMUURI_TARGET_{env_cleanup(target)}'] = ( 'up' if icfg['state'] else 'down' ) for group, icfg in groups.items(): env[f'FOOMUURI_GROUP_{env_cleanup(group)}'] = ( 'up' if icfg.get('state', True) else 'down' ) run_program_rc(proc, env=env) def monitor_down_interval( targets: dict, groups: dict, cfg: dict, grouptarget: str, name: str ) -> None: """Group/target is still down, run command in regular intervals.""" now = time.time() if now < cfg['last_down_interval'] + cfg['down_interval']: return cfg['last_down_interval'] = now seconds = int(now - cfg['state_time'] + 0.5) extra = f'previous change was {seconds_to_human(seconds)} ago' verbose(f'{grouptarget} {name} is still down, {extra}', 0) monitor_run_command( targets, groups, cfg, grouptarget, name, 'down_interval', extra, None ) def monitor_update_groups(targets: dict, groups: dict) -> None: """Update all group statuses. On startup make decision and send event for all groups after first reply from any target. """ for group, cfg in groups.items(): any_up = any(targets[target]['state'] for target in cfg['target']) state = cfg.get('state', not any_up) # Undef in startup if not any_up: if state: # Change from up to down cfg['state'] = False monitor_state_command(targets, groups, cfg, 'group', group) else: # Still down, check interval monitor_down_interval(targets, groups, cfg, 'group', group) elif any_up and not state: # Change from down to up cfg['state'] = True monitor_state_command(targets, groups, cfg, 'group', group) def monitor_update_target( targets: dict, groups: dict, target: str, state: bool ) -> None: """Add state to target's history and change its state.""" # Add new state to end of history cfg = targets[target] startup_change = False if 'history' not in cfg: # First reply ever, fill history and force change event cfg['history'] = [True] * cfg['history_size'] startup_change = True cfg['history'] = cfg['history'][1:] + [state] # Target state can't change if added state is same as current state if cfg['state'] == state and not startup_change: if not state: monitor_down_interval(targets, groups, cfg, 'target', target) return # Check if target state is changed count_up = sum(cfg['history']) if cfg['state']: # Currently up. Target goes down if: # - history has too many downs # OR # - last n items were down if cfg['history_size'] - count_up >= cfg['history_down'] or not any( cfg['history'][-cfg['consecutive_down'] :] ): cfg['state'] = False monitor_state_command(targets, groups, cfg, 'target', target) elif startup_change: # Always send an event on startup monitor_state_command(targets, groups, cfg, 'target', target) elif count_up >= cfg['history_up'] and all( cfg['history'][-cfg['consecutive_up'] :] ): # Currently down. Target goes up if: # - history has enough ups # AND # - last n items were up cfg['state'] = True monitor_state_command(targets, groups, cfg, 'target', target) def monitor_parse_line( targets: dict, groups: dict, target: str, line: str ) -> None: """Parse result line from monitor's command.""" verbose(f'target {target}: {line}', 2) stat_ms = None # Value for statistics, assume error if line in {'OK', 'ERROR'}: # Generic "OK" or "ERROR" reply state = line == 'OK' if state: stat_ms = 1 # There is no time, use 1 elif line.startswith('ICMP Unreachable (Communication Administratively'): # fping reply if packet is rejected state = False elif 'xmt/rcv/%loss' in line: # fping with "--squiet" if '0/0/0%' in line: return # --squiet is less than --interval, ignore line state = '/100%' not in line # It's up if loss is less than 100% if state: with contextlib.suppress(ValueError): stat_ms = float(line.rsplit('/', maxsplit=1)[-1]) else: # fping with "--interval" match = re.fullmatch(r'[^ ]+ : \[\d+\], (.+)', line) if not match: return # No match, ignore line result = match.group(1) state = ' bytes, ' in result and ' ms (' in result if state: with contextlib.suppress(ValueError): stat_ms = float(result.split()[2]) # Update target state monitor_update_target(targets, groups, target, state) # Add result to statistics cfg = targets[target] cfg['time'] = (cfg['time'] + [stat_ms])[-cfg['statistics_size'] :] def command_option_values(cmd: list[str]) -> dict[str, typing.Optional[str]]: """Quick and dirty parser for "--foo n" and "--foo=n".""" ret = {} for index, item in enumerate(cmd): if not item.startswith('--'): continue if '=' in item: item, value = item.split('=', 1) else: try: value = cmd[index + 1] except IndexError: value = None ret[item] = value return ret def mangle_fping_command(target: str, cfg: dict) -> list[str]: """Mangle fping's command line to contain needed options.""" cmd = cfg['command'] if 'fping' not in cmd[0]: return cmd options = command_option_values(cmd) # --squiet works even if interface is down or missing. Automatically # use it if missing (1s) or --interval=n000 is specified. # Use plain --interval if value is not exact second. squiet = options.get('--squiet', '') interval = options.get('--interval') or '' # ty workaround if not squiet and not interval: # Nothing, use 1s / 1000ms cmd = cmd[:1] + ['--squiet=1', '--interval=1000'] + cmd[1:] elif squiet and not interval: # squiet specified, use it as interval cmd = cmd[:1] + [f'--interval={squiet}000'] + cmd[1:] elif not squiet and interval.endswith('000'): # Use interval as squiet cmd = cmd[:1] + [f'--squiet={interval[:-3]}'] + cmd[1:] elif squiet and interval: with contextlib.suppress(ValueError): if int(squiet) * 1000 != int(interval): warning( f'Mismatching "--squiet" and "--interval" values in ' f'target "{target}": {" ".join(cmd)}' ) # Add --loop if missing to get continous reports if '--loop' not in options: cmd = cmd[:1] + ['--loop'] + cmd[1:] return cmd def monitor_start_targets(targets: dict, groups: dict) -> None: """Start pinging all targets.""" started_something = False for target, cfg in targets.items(): # Check if target is already running, or still waiting for restart if cfg.get('proc') or cfg['proc_restart'] > time.time(): continue # Assume it is up on first startup if 'state' not in cfg: cfg['state'] = True # Start command (pylint: disable=consider-using-with) cmd = mangle_fping_command(target, cfg) verbose(f'target {target} command: {" ".join(map(str, cmd))}', 2) try: cfg['proc'] = subprocess.Popen( cmd, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf-8', ) verbose(f'target {target} monitoring command started') started_something = True except OSError as error: verbose(f'target {target} monitoring command failed: {error}', 0) cfg['proc'] = None cfg['proc_restart'] = time.time() + 30 # Mark it as down immediately cfg['history'] = [False] * cfg['history_size'] monitor_update_target(targets, groups, target, False) # Small initial sleep so that first read_target() will receive more # replies in one go. This helps to get more "target up" events before # "group up" events. if started_something: time.sleep(0.5) def monitor_close_target(targets: dict, groups: dict, target: str) -> None: """Target monitoring command died. Mark it as down.""" # Terminate proc and schedule restart verbose( f'target {target} monitoring command died, restarting it in 30s', 0 ) cfg = targets[target] cfg['proc'].terminate() cfg['proc'].wait(timeout=0.1) cfg['proc'] = None cfg['proc_restart'] = time.time() + 30 cfg['time'] = [] # Mark it as down immediately cfg['history'] = [False] * cfg['history_size'] monitor_update_target(targets, groups, target, False) def monitor_read_targets(targets: dict, groups: dict) -> None: """Read incoming lines from all targets.""" # (Re)Start targets if not running monitor_start_targets(targets, groups) # Wait for incoming lines from any client, up to 10 seconds readfd = [item['proc'].stdout for item in targets.values() if item['proc']] poll = select.select(readfd, [], [], 10.0) # Read all incoming lines for readfd in poll[0]: for target, cfg in targets.items(): if cfg['proc'] and cfg['proc'].stdout == readfd: line = readfd.readline() if line: monitor_parse_line(targets, groups, target, line.strip()) else: monitor_close_target(targets, groups, target) break # Update all groups monitor_update_groups(targets, groups) def monitor_terminate_targets(targets: dict) -> None: """Terminate all subprocesses.""" for cfg in targets.values(): if cfg['proc']: cfg['proc'].terminate() for cfg in targets.values(): if cfg['proc']: cfg['proc'].wait(timeout=0.1) def parse_config_targets(config: dict) -> dict: """Parse "target foo { ... }" entries from config.""" targets = {} names = [item for item in config if item.startswith('target ')] for name in names: name = name[7:] cfg = targets[name] = { 'command': [], 'command_up': [], 'command_down': [], 'command_down_interval': [], 'consecutive_up': 20, # last n were UP => UP 'consecutive_down': 10, # last n were DOWN => DOWN 'history_up': 80, # count of UPs => n => UP 'history_down': 30, # count of DOWNs >= n => DOWN 'history_size': 100, 'statistics_size': 300, # Keep last 300 results 'statistics_interval': 60, # Write it once a minute 'down_interval': 600, # How often to run command_down_interval # Internal items, don't set in config: # 'state': True, # Up or down # 'state_time': time(), # Timestamp for last state change # 'last_down_interval': time(), # Still down timestamp # 'history': [True] * history_size, # 'proc': Popen() 'proc_restart': 0, # Start immediately 'time': [], # Statistics } fileline = '' for fileline, line in config.pop(f'target {name}'): if line[0] not in cfg: fail(f'{fileline}Unknown keyword: {" ".join(line)}') if isinstance(cfg[line[0]], list): cfg[line[0]] = line[1:] else: try: cfg[line[0]] = int(' '.join(line[1:])) except ValueError: fail(f'{fileline}Invalid value: {" ".join(line)}') # Verify section if not cfg['command']: fail(f'{fileline}Missing "command" keyword in "target {name}"') for key in ( 'consecutive_up', 'consecutive_down', 'history_up', 'history_down', ): if ( cfg[key] # ty: ignore[unsupported-operator] > cfg['history_size'] ): fail( f'{fileline}{key} is larger than history_size in ' f'"target {name}": {cfg[key]} > {cfg["history_size"]}' ) if ( cfg['history_size'] # ty: ignore[unsupported-operator] - cfg['history_up'] >= cfg['history_down'] ): warning( f'{fileline}Possible up-down loop in ' f'"target {name}": history_size {cfg["history_size"]} - ' f'history_up {cfg["history_up"]} >= ' f'history_down {cfg["history_down"]}' ) return targets def parse_config_groups(config: dict, targets: dict) -> dict: """Parse "group foo { ... }" entries from config.""" groups = {} names = [item for item in config if item.startswith('group ')] for name in names: name = name[6:] cfg = groups[name] = { 'target': [], 'command_up': [], 'command_down': [], 'command_down_interval': [], 'down_interval': 600, # How often to run command_down_interval } fileline = '' for fileline, line in config.pop(f'group {name}'): if line[0] not in cfg: fail(f'{fileline}Unknown keyword: {" ".join(line)}') if isinstance(cfg[line[0]], list): cfg[line[0]] = line[1:] else: try: cfg[line[0]] = int(' '.join(line[1:])) except ValueError: fail(f'{fileline}Invalid value: {" ".join(line)}') # Verify section target = cfg.get('target') if not target: fail(f'{fileline}Missing "target" keyword in "group {name}"') for item in target: # ty: ignore[not-iterable] if item not in targets: fail(f'{fileline}Undefined target "{item}" in "group {name}"') return groups def monitor_write_statistics(targets: dict, groups: dict) -> None: """Write statistics file.""" stats = {} for group, cfg in groups.items(): stats[group] = { 'type': 'group', # Config 'target': cfg['target'], # State 'state': cfg['state'], 'history': [], # Dummy fields to be compatible with targets 'time': [], } for target, cfg in targets.items(): stats[target] = { 'type': 'target', # Config 'consecutive_up': cfg['consecutive_up'], 'consecutive_down': cfg['consecutive_down'], 'history_up': cfg['history_up'], 'history_down': cfg['history_down'], # State 'state': cfg['state'], 'history': cfg.get('history', []), 'time': cfg['time'], # fping time } save_file(state_file('monitor_statistics_file'), stats) def command_monitor() -> int: """Monitor targets and run command when their state changes up or down.""" INTERNAL.keep_going = 1 while INTERNAL.keep_going: # Read minimal config config = minimal_config() targets = parse_config_targets(config) groups = parse_config_groups(config, targets) # Exit silently with OK if no targets defined in configuration if not targets: notify('READY=1') notify('STOPPING=1') return 0 daemonize() # Define reload/stop signal handler def signal_handler(sig: int, _dummy_frame: typing.Any) -> None: if sig == signal.SIGINT: INTERNAL.keep_going = 0 notify('STOPPING=1') else: INTERNAL.keep_going = 2 notify('RELOADING=1') INTERNAL.keep_going = 1 signal.signal(signal.SIGHUP, signal_handler) signal.signal(signal.SIGINT, signal_handler) save_file(CONFIG.run_dir / 'foomuuri-monitor.pid', [str(os.getpid())]) # Start monitoring verbose('Target monitor ready', 0) notify('READY=1') stat_interval = min( cfg['statistics_interval'] for cfg in targets.values() ) next_stat = time.time() + stat_interval / 5 # Early first write while INTERNAL.keep_going == 1: monitor_read_targets(targets, groups) # Periodic statistics file update if stat_interval and time.time() >= next_stat: next_stat += stat_interval monitor_write_statistics(targets, groups) monitor_terminate_targets(targets) return 0 def command_help(error: bool = True) -> int: """Print command line help.""" print(f"""Foomuuri {VERSION} Usage: {sys.argv[0]} [OPTIONS] COMMAND Available COMMANDs: start Load configuration files and generate ruleset stop Remove ruleset reload Same as start, followed by iplist refresh try-reload Same as reload, ask confirmation to keep new config status Show current status: running, zone-interface mapping check Verify configuration files block Load "block all traffic" ruleset ruleset list [ZONE-ZONE]... List whole active ruleset or for ZONE-ZONE only macro list [NAME | VALUE]... List all macros or macros with specified NAME(s) or VALUE(s) counter list [COUNTER]... List all or specified named COUNTER(s) counter reset [COUNTER]... Reset all or specified named COUNTER(s) iplist status [IPLIST]... List number of entries of all or specified IPLIST(s) iplist list [IPLIST]... [IPADDRESS]... List entries of all or specified IPLIST(s) with IPADDRESS(es) iplist add IPLIST [TIMEOUT] IPADDRESS [IPADDRESS]... Add or refresh IPADDRESS(es) to IPLIST iplist del IPLIST IPADDRESS [IPADDRESS]... Delete IPADDRESS(es) from IPLIST iplist flush [IPLIST]... Delete all added IP addresses from all or specified IPLIST(s) iplist refresh [IPLIST]... Refresh all or specified IPLIST(s) now set interface INTERFACE zone {{ZONE | -}} Change INTERFACE to ZONE, or remove from all zones Available OPTIONS: --version Print version --verbose Verbose output --quiet Be quiet --force Force some operations, don\'t check anything --soft Don\'t force operations, check more --fork Fork as a background daemon process --syslog Enable syslog logging --set=OPTION=VALUE Set foomuuri{{}} config OPTION to VALUE Internally used commands: dbus Start D-Bus daemon monitor Start target monitor daemon""") if error: sys.exit(1) return 0 def parse_command_line() -> None: """Parse command line to INTERNAL.command and INTERNAL.parameters.""" # pylint: disable=too-many-branches for arg in sys.argv[1:]: if arg == '--help': INTERNAL.command = 'help' elif arg == '--version': INTERNAL.command = 'version' elif arg == '--verbose': option = arg[2:] CONFIG[option] += 1 INTERNAL.config_override[option] = ( int(INTERNAL.config_override.get(option, 0)) + 1 ) elif arg == '--quiet': option = 'verbose' CONFIG[option] -= 1 INTERNAL.config_override[option] = ( int(INTERNAL.config_override.get(option, 0)) - 1 ) elif arg in {'--fork', '--syslog'}: CONFIG[arg[2:]] = 1 INTERNAL.config_override[arg[2:]] = 1 elif arg == '--force': INTERNAL.force += 1 elif arg == '--soft': INTERNAL.force -= 1 elif arg.startswith('--set='): if arg.count('=') == 1: fail(f'Invalid syntax for --set=OPTION=VALUE: {arg}') _, option, value = arg.split('=', 2) if option not in CONFIG: fail(f'Unknown foomuuri{{}} option: {option}') try: CONFIG.set_from_str(option, value) INTERNAL.config_override[option] = value except ValueError: fail(f'Invalid foomuuri{{}} option {option} value: {value}') elif arg.startswith('--'): fail(f'Unknown option: {arg}') elif not INTERNAL.command: INTERNAL.command = arg else: INTERNAL.parameters.append(arg) def run_command() -> int: """Run CONFIG[_command].""" # Help/version doesn't need root if not INTERNAL.command or INTERNAL.command == 'help': return command_help(error=False) if INTERNAL.command == 'version': print(VERSION) return 0 # Initialize syslogging syslog_open_or_close() # Warning if not running as root INTERNAL.root_power = not os.getuid() if not INTERNAL.root_power: warning('Foomuuri should be run as "root"') # Run command handler, allow_parameters = { 'start': (command_start, False), 'stop': (command_stop, False), 'reload': (command_reload, False), 'try-reload': (command_try_reload, False), 'status': (command_status, False), 'check': (command_start, False), 'block': (command_block, False), 'ruleset': (command_subcommand, True), 'macro': (command_subcommand, True), 'counter': (command_subcommand, True), 'iplist': (command_subcommand, True), 'list': (command_list, True), 'set': (command_set, True), 'dbus': (command_dbus, False), 'monitor': (command_monitor, False), }.get(INTERNAL.command, (None, False)) if not handler: return fail(f'Unknown command: {INTERNAL.command}') if INTERNAL.parameters and not allow_parameters: return command_help() return handler() or 0 def main() -> int: """Parse command line and run command.""" parse_command_line() return run_command() if __name__ == '__main__': try: sys.exit(main()) except BrokenPipeError: # Python flushes standard streams on exit; redirect remaining output # to devnull to avoid another BrokenPipeError at shutdown os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno()) sys.exit(1) foomuuri-0.34/src/tests/000077500000000000000000000000001526436212200152415ustar00rootroot00000000000000foomuuri-0.34/src/tests/__init__.py000066400000000000000000000011771526436212200173600ustar00rootroot00000000000000"""Hack to import foomuuri without .py extension and proper package tree.""" import sys from importlib.abc import Loader from importlib.machinery import SourceFileLoader from importlib.util import module_from_spec, spec_from_loader if ( # ruff: ignore[non-empty-init-module] ( spec := spec_from_loader( 'foomuuri', SourceFileLoader('foomuuri', './src/foomuuri') ) ) and (foomuuri := module_from_spec(spec)) and isinstance(spec.loader, Loader) ): spec.loader.exec_module(foomuuri) sys.modules['foomuuri'] = foomuuri else: raise SystemError('Failed to add foomuuri to sys.modules') foomuuri-0.34/src/tests/test_converters.py000066400000000000000000000014221526436212200210430ustar00rootroot00000000000000"""Basic unit tests of Converters.""" # pylint: disable=import-error import unittest from foomuuri import Converters class TestConverters(unittest.TestCase): """Test converter helpers.""" def test_canonical_iplist_name(self): """Test canonical_iplist_name.""" self.assertEqual(Converters.canonical_iplist_name('@foo'), '@foo') self.assertEqual(Converters.canonical_iplist_name('foo'), '@foo') def test_str_yes_no_to_bool(self): """Test str_yes_no_to_bool.""" self.assertTrue(Converters.str_yes_no_to_bool('yes')) self.assertFalse(Converters.str_yes_no_to_bool('no')) self.assertRaises(ValueError, Converters.str_yes_no_to_bool, '') self.assertRaises(ValueError, Converters.str_yes_no_to_bool, 'invalid') foomuuri-0.34/src/tests/test_duration.py000066400000000000000000000033051526436212200205000ustar00rootroot00000000000000"""Basic unit tests of parse_duration() and seconds_to_duration().""" # pylint: disable=import-error import unittest from foomuuri import parse_duration, seconds_to_duration class TestParseDuration(unittest.TestCase): """Test parse_duration().""" def test_valid_duration(self): """Test valid outcomes.""" self.assertEqual(parse_duration(' 0s ', fallback=None), 0) self.assertEqual(parse_duration(' 1s ', fallback=0), 1) self.assertEqual(parse_duration(' 1h 1m ', fallback=0), 3660) self.assertEqual(parse_duration(' 1w1d1h1m1s ', fallback=0), 694861) self.assertEqual(parse_duration(' 1w_1d_1h_1m_1s', fallback=0), 694861) def test_invalid_duration(self): """Test invalid outcomes.""" self.assertIsNone(parse_duration('', fallback=None)) self.assertIsNone(parse_duration('1day', fallback=None)) self.assertIsNone(parse_duration('1y', fallback=None)) self.assertIsNone(parse_duration('s', fallback=None)) self.assertIsNone(parse_duration('1s1h', fallback=None)) class TestSecondsToDuration(unittest.TestCase): """Test seconds_to_duration().""" def test_valid_seconds(self): """Test valid outcomes.""" self.assertEqual(seconds_to_duration(0), '0s') self.assertEqual(seconds_to_duration(1), '1s') self.assertEqual(seconds_to_duration(42), '42s') self.assertEqual(seconds_to_duration(61), '1m01s') self.assertEqual(seconds_to_duration(3600), '1h00m00s') self.assertEqual(seconds_to_duration(99999), '1d03h46m39s') self.assertEqual(seconds_to_duration(999999), '11d13h46m39s') self.assertEqual(seconds_to_duration(9999999), '115d17h46m39s') foomuuri-0.34/src/tests/test_ipaddress.py000066400000000000000000000070731526436212200206370ustar00rootroot00000000000000"""Basic unit tests for get_ip_family().""" # pylint: disable=import-error import unittest from foomuuri import get_ip_family class TestIsIpAddress(unittest.TestCase): """Basic unit tests for get_ip_family().""" def test_ipv4_address(self): """Test for IPv4 address.""" self.assertEqual(get_ip_family(''), 0) self.assertEqual(get_ip_family('[127.0.0.1]'), 0) self.assertEqual(get_ip_family('127.0.0.1'), 4) self.assertEqual(get_ip_family('-127.0.0.1'), 4) def test_ipv4_network(self): """Test for IPv4 network.""" self.assertEqual(get_ip_family(''), 0) self.assertEqual(get_ip_family('127.0.0.0/-16'), 0) self.assertEqual(get_ip_family('127.0.0.0/8'), 4) self.assertEqual(get_ip_family('127.0.0.1/8'), 4) self.assertEqual(get_ip_family('-127.0.0.0/8'), 4) def test_ipv4_range(self): """Test for IPv4 range.""" self.assertEqual(get_ip_family('-'), 0) self.assertEqual(get_ip_family('127.0.0.0/8-127.0.0.0/8'), 0) self.assertEqual(get_ip_family('127.0.0.2-127.0.0.1'), 0) self.assertEqual(get_ip_family('127.0.0.1-127.0.0.1'), 4) self.assertEqual(get_ip_family('127.0.0.1-127.0.0.2'), 4) def test_ipv6_address(self): """Test for IPv6 address.""" self.assertEqual(get_ip_family(''), 0) self.assertEqual(get_ip_family('::'), 6) self.assertEqual(get_ip_family('[::]'), 6) self.assertEqual(get_ip_family('-::'), 6) def test_ipv6_network(self): """Test for IPv6 network.""" self.assertEqual(get_ip_family(''), 0) self.assertEqual(get_ip_family('::/129'), 0) self.assertEqual(get_ip_family('[::]/-64'), 0) self.assertEqual(get_ip_family('::/64'), 6) self.assertEqual(get_ip_family('::1/64'), 6) self.assertEqual(get_ip_family('::/-64'), 6) self.assertEqual(get_ip_family('[::]/64'), 6) self.assertEqual(get_ip_family('-::/64'), 6) def test_ipv6_range(self): """Test for IPv6 range.""" self.assertEqual(get_ip_family('-'), 0) self.assertEqual(get_ip_family('1::/64-1::/64'), 0) self.assertEqual(get_ip_family('::-::/-64'), 0) self.assertEqual(get_ip_family('::2-::1'), 0) self.assertEqual(get_ip_family('::-::'), 6) self.assertEqual(get_ip_family('::1-::2'), 6) def test_strict(self): """Test address detection for strict=True.""" self.assertEqual(get_ip_family('-127.0.0.1', strict=True), 0) self.assertEqual(get_ip_family('-127.0.0.0/24', strict=True), 0) self.assertEqual(get_ip_family('-::', strict=True), 0) self.assertEqual(get_ip_family('-::/64', strict=True), 0) self.assertEqual(get_ip_family('::/-64', strict=True), 0) self.assertEqual(get_ip_family('::%interface', strict=True), 0) self.assertEqual(get_ip_family('127.0.0.1', strict=True), 4) self.assertEqual(get_ip_family('127.0.0.0/24', strict=True), 4) self.assertEqual(get_ip_family('127.0.0.1/8', strict=True), 4) self.assertEqual(get_ip_family('127.0.0.1-127.0.0.2', strict=True), 4) self.assertEqual(get_ip_family('::', strict=True), 6) self.assertEqual(get_ip_family('[::]', strict=True), 6) self.assertEqual(get_ip_family('::/64', strict=True), 6) self.assertEqual(get_ip_family('::1/64', strict=True), 6) self.assertEqual(get_ip_family('[::/64]', strict=True), 6) self.assertEqual(get_ip_family('[::]/64', strict=True), 6) self.assertEqual(get_ip_family('::1-::2', strict=True), 6) foomuuri-0.34/src/tests/test_iplist.py000066400000000000000000000037221526436212200201620ustar00rootroot00000000000000"""Basic unit tests of IPList classes.""" # pylint: disable=invalid-name,import-error import collections.abc import unittest from foomuuri import IPList, IPLists class TestIPLists(unittest.TestCase): """Test IPLists.""" def setUp(self): """Prepare test fixture.""" self.iplists = IPLists() self.sources_foo = ['https://foob.ar', 'https://foob.az'] self.iplists['@foo'] = IPList(sources=self.sources_foo) self.sources_bar = ['https://arbo.of', 'https://azbp.of'] self.iplists['@bar'] = IPList(sources=self.sources_bar) self.iplists['@bar'].options.start = False def test_index_getter(self): """Test index getter by iplist name.""" self.assertEqual(self.iplists['@foo'].sources, self.sources_foo) self.assertEqual(self.iplists['@bar'].sources, self.sources_bar) def test_iter_names(self): """Test iterator over iplist names.""" self.assertEqual(list(self.iplists), ['@foo', '@bar']) def test_items(self): """Test iterable over iplist entries (name, value tuples).""" self.assertIsInstance(self.iplists.items(), collections.abc.Iterable) self.assertListEqual( list(self.iplists.items()), [ ('@foo', self.iplists['@foo']), ('@bar', self.iplists['@bar']), ], ) def test_values(self): """Test iterable over iplist entries values.""" self.assertIsInstance(self.iplists.values(), collections.abc.Iterable) self.assertListEqual( list(self.iplists.values()), [ self.iplists['@foo'], self.iplists['@bar'], ], ) def test_filter_names_by(self): """Test set of iplist names with specific iplist option value.""" self.assertIsInstance(self.iplists.filter_names_by('start', True), set) self.assertEqual(self.iplists.filter_names_by('start', True), {'@foo'}) foomuuri-0.34/src/tests/test_iplist_cache_append_or_overwrite.py000066400000000000000000000067531526436212200254510ustar00rootroot00000000000000"""Test IPListSourceCache._append_or_overwrite().""" # pylint: disable=invalid-name,import-error import time import unittest import foomuuri from src.tests.test_iplist_cache_refresh import source_cache class TestAppendOrOverwrite(unittest.TestCase): """Test IPListSourceCache._append_or_overwrite().""" def setUp(self): """Prepare test fixtures.""" self.now = int(time.time()) self.expire = self.now + 1000 self.source = 'https://foo.bar/iplist' def run_append_or_overwrite( self, cache, addresses, overwrite=None, default_is_overwrite=True, ): """Call _append_or_overwrite() with common fixtures.""" options = foomuuri.IPListSourceOptions() options.overwrite = overwrite # pylint: disable=protected-access # ruff: ignore[private-member-access] cache._append_or_overwrite( self.source, addresses, options, default_is_overwrite, self.now ) return cache[self.source] def cached_cache(self): """Return cache with one cached address for self.source.""" return source_cache( self.source, ip={'10.0.0.1': self.now}, refresh=self.now ) def test_default_overwrite_replaces(self, *_): """Test default overwrite mode replaces cached addresses.""" cache = self.cached_cache() entry = self.run_append_or_overwrite( cache, {'10.0.0.9': self.expire}, default_is_overwrite=True ) self.assertEqual(entry['ip'], {'10.0.0.9': self.expire}) def test_default_append_merges(self, *_): """Test default append mode merges addresses to cached ones.""" cache = self.cached_cache() entry = self.run_append_or_overwrite( cache, {'10.0.0.9': self.expire}, default_is_overwrite=False ) self.assertEqual( entry['ip'], {'10.0.0.1': self.now, '10.0.0.9': self.expire} ) def test_overwrite_true_overrides_append_default(self, *_): """Test overwrite=yes replaces despite append default.""" cache = self.cached_cache() entry = self.run_append_or_overwrite( cache, {'10.0.0.9': self.expire}, overwrite=True, default_is_overwrite=False, ) self.assertEqual(entry['ip'], {'10.0.0.9': self.expire}) def test_overwrite_false_overrides_overwrite_default(self, *_): """Test overwrite=no appends despite overwrite default.""" cache = self.cached_cache() entry = self.run_append_or_overwrite( cache, {'10.0.0.9': self.expire}, overwrite=False, default_is_overwrite=True, ) self.assertEqual( entry['ip'], {'10.0.0.1': self.now, '10.0.0.9': self.expire} ) def test_marks_dirty_and_refresh(self): """Test entry is marked dirty and stamped with refresh time.""" cache = self.cached_cache() entry = self.run_append_or_overwrite(cache, {'10.0.0.9': self.expire}) self.assertTrue(entry['dirty']) self.assertEqual(entry['refresh'], self.now) def test_creates_missing_entry(self): """Test missing source entry is created.""" cache = foomuuri.IPListSourceCache() entry = self.run_append_or_overwrite(cache, {'10.0.0.9': self.expire}) self.assertEqual(entry['ip'], {'10.0.0.9': self.expire}) self.assertTrue(entry['dirty']) self.assertEqual(entry['refresh'], self.now) foomuuri-0.34/src/tests/test_iplist_cache_build.py000066400000000000000000000043711526436212200224650ustar00rootroot00000000000000"""Test IPListValue.from_sources().""" # pylint: disable=invalid-name,import-error import copy import unittest import foomuuri class TestIPListValueBuild(unittest.TestCase): """Test IPListValue.from_sources().""" def test_static_addresses(self): """Test static address sources use permanent expiration.""" source_cache = foomuuri.IPListSourceCache() iplist = foomuuri.IPListValue.from_sources( source_cache, '@foo', ['10.0.0.1', '2001:db8::1'] ) self.assertEqual( iplist.addresses, { '10.0.0.1': foomuuri.IPListSourceCache.expire_never, '2001:db8::1': foomuuri.IPListSourceCache.expire_never, }, ) self.assertTrue(iplist.dirty) self.assertEqual(dict(source_cache), {}) def test_rebuilds_and_merges_sources(self): """Test rebuilding an iplist from cached and manual sources. Existing transient iplist addresses are not read or overwritten. """ source_cache = foomuuri.IPListSourceCache( { '@foo': { 'ip': {'192.0.2.99': 456}, 'dirty': False, 'refresh': 100, }, 'foo.example': { 'ip': { '10.0.0.1': 200, '192.0.2.1': 250, }, 'dirty': True, 'refresh': 100, }, 'manual.@foo': { 'ip': { '192.0.2.1': 300, '2001:db8::2': 400, }, 'dirty': True, 'refresh': 100, }, } ) snapshot = copy.deepcopy(source_cache) iplist = foomuuri.IPListValue.from_sources( source_cache, '@foo', ['foo.example', '10.0.0.1'] ) self.assertEqual( iplist.addresses, { '10.0.0.1': foomuuri.IPListSourceCache.expire_never, '192.0.2.1': 300, '2001:db8::2': 400, }, ) self.assertTrue(iplist.dirty) self.assertEqual(source_cache, snapshot) foomuuri-0.34/src/tests/test_iplist_cache_read.py000066400000000000000000000115621526436212200223010ustar00rootroot00000000000000"""Test IPListSourceCache.read().""" # pylint: disable=invalid-name,import-error import json import time import unittest.mock import foomuuri class TestIPListSourceCacheRead(unittest.TestCase): """Test IPListSourceCache.read().""" def setUp(self): """Define common cache read data.""" self.filename = unittest.mock.Mock() self.now = int(time.time()) self.iplists = foomuuri.IPLists() self.iplists['@known'] = foomuuri.IPList() def _read(self, data, sources): """Read cache using common fixtures helper.""" self.filename.read_text.return_value = json.dumps(data) self.iplists['@known'].sources = sources with ( unittest.mock.patch( 'foomuuri.state_file', return_value=self.filename ), unittest.mock.patch('foomuuri.verbose') as verbose, ): cache = foomuuri.IPListSourceCache.read(self.iplists) return cache, verbose def test_cache_read_prunes_sources(self): """Test cache read removes legacy and unconfigured sources.""" data = { '@unknown': { 'ip': {'10.0.0.1': foomuuri.IPListSourceCache.expire_never}, }, 'manual.@unknown': { 'ip': {'10.0.0.2': foomuuri.IPListSourceCache.expire_never}, }, 'manual.@known': { 'ip': {'10.0.0.4': foomuuri.IPListSourceCache.expire_never}, 'refresh': 1, }, 'broken': {'dirty': False}, '@known': { 'ip': {'10.0.0.3': foomuuri.IPListSourceCache.expire_never}, 'refresh': 1, }, 'https://foo.bar/removed.txt': { 'ip': {'10.0.0.9': foomuuri.IPListSourceCache.expire_never}, 'refresh': 100, }, 'https://foo.bar/empty.txt': { 'ip': {}, 'refresh': 100, }, 'manual.@empty': { 'ip': {}, 'refresh': 100, }, } cache, verbose = self._read( data, [ 'https://foo.bar/empty.txt', 'https://foo.bar/empty2.txt', 'https://foo.bar/list2.txt', ], ) verbose.assert_has_calls( [ unittest.mock.call( 'Deleting legacy iplist "@unknown" from cache' ), unittest.mock.call( 'Deleting source "manual.@unknown" from cache, ' 'not in config' ), unittest.mock.call( 'Deleting source "broken" from cache, not in config' ), unittest.mock.call( 'Deleting legacy iplist "@known" from cache' ), unittest.mock.call( 'Deleting source "https://foo.bar/removed.txt" ' 'from cache, not in config', ), unittest.mock.call( 'Deleting source "manual.@empty" from cache, ' 'not in config', ), ] ) self.assertEqual(verbose.call_count, 6) self.assertEqual( cache, { 'manual.@known': { 'ip': { '10.0.0.4': foomuuri.IPListSourceCache.expire_never }, 'refresh': 1, }, 'https://foo.bar/empty.txt': { 'ip': {}, 'refresh': 100, }, }, ) def test_cache_read_removes_expired_addresses(self): """Test cache read removes expired addresses.""" data = { 'https://foo.bar/list.txt': { 'ip': { '10.0.0.1': 0, '10.0.0.2': self.now, '10.0.0.3': self.now + 3600, }, 'refresh': 1, }, } cache, verbose = self._read( data, [ 'https://foo.bar/list.txt', ], ) verbose.assert_has_calls( [ unittest.mock.call( 'Deleting expired iplist "https://foo.bar/list.txt" ' 'entry "10.0.0.1"' ), unittest.mock.call( 'Deleting expired iplist "https://foo.bar/list.txt" ' 'entry "10.0.0.2"' ), ] ) self.assertEqual(verbose.call_count, 2) self.assertEqual( cache, { 'https://foo.bar/list.txt': { 'ip': {'10.0.0.3': self.now + 3600}, 'refresh': 1, }, }, ) foomuuri-0.34/src/tests/test_iplist_cache_refresh.py000066400000000000000000000254421526436212200230260ustar00rootroot00000000000000"""Test IPListSourceCache operations.""" # pylint: disable=invalid-name,import-error import copy import time import unittest.mock import foomuuri from foomuuri import INTERNAL as BASE_INTERNAL def source_cache(*sources, ip=None, refresh=None): """Return source cache with one entry per source.""" entry = {'ip': {} if ip is None else ip, 'dirty': False} if refresh is not None: entry['refresh'] = refresh return foomuuri.IPListSourceCache( {source: copy.deepcopy(entry) for source in sources} ) @unittest.mock.patch( 'foomuuri.INTERNAL', new_callable=lambda: copy.deepcopy(BASE_INTERNAL) ) class TestSourceCacheRefreshURLOrFile(unittest.TestCase): """Test IPListSourceCache.refresh_url_or_file().""" def setUp(self): """Prepare test fixtures.""" self.now = int(time.time()) self.url = 'https://foo.bar/iplist' @staticmethod def run_refresh( # pylint: disable=too-many-arguments source, cache, content, force=0, *, timeout=1000, refresh=1000, missing_ok=None, overwrite=None, ): """Call IPListSourceCache.refresh_url_or_file().""" foomuuri.INTERNAL.force = force options = foomuuri.IPListSourceOptions() options.timeout = timeout options.refresh = refresh options.missing_ok = missing_ok options.overwrite = overwrite with ( unittest.mock.patch( 'foomuuri.get_url', return_value=content ) as get_url, unittest.mock.patch('foomuuri.warning') as warning, unittest.mock.patch('foomuuri.verbose') as verbose, ): cache.refresh_url_or_file(source, options) return cache.get(source), warning, verbose, get_url def test_empty_url_missing_ok(self, *_): """Test no warning for empty source with missing_ok=yes.""" cache = source_cache( self.url, ip={'10.0.0.1': self.now}, refresh=self.now ) entry, warning, verbose, _ = self.run_refresh( source=self.url, cache=cache, content='', missing_ok=True ) self.assertFalse(entry['ip']) self.assertTrue(entry['dirty']) warning.assert_not_called() verbose.assert_called_once_with( f'Iplist content for "{self.url}" refreshed, 0 entries' ) def test_empty_url_no_missing_ok(self, *_): """Test warning for empty source without missing_ok.""" cache = source_cache( self.url, ip={'10.0.0.1': self.now}, refresh=self.now ) entry, warning, verbose, _ = self.run_refresh( source=self.url, cache=cache, content='', missing_ok=None ) self.assertFalse(entry['ip']) self.assertTrue(entry['dirty']) warning.assert_called_once_with( f'No IP addresses listed in "{self.url}"' ) verbose.assert_called_once_with( f'Iplist content for "{self.url}" refreshed, 0 entries' ) def test_empty_no_missing_ok_soft(self, *_): """Test empty cache without missing_ok is fetched, --soft(force<0).""" cache = source_cache(self.url, refresh=self.now) entry, warning, verbose, get_url = self.run_refresh( source=self.url, cache=cache, content='10.0.0.9\n', force=-1, missing_ok=None, ) self.assertIn('10.0.0.9', entry['ip']) self.assertTrue(entry['dirty']) get_url.assert_called_once() warning.assert_not_called() verbose.assert_called_once_with( f'Iplist content for "{self.url}" refreshed, 1 entries' ) def test_empty_missing_ok_cached(self, *_): """Test empty cache with missing_ok is reused, --soft(force<0).""" cache = source_cache(self.url, refresh=self.now) entry, warning, verbose, get_url = self.run_refresh( source=self.url, cache=cache, content='10.0.0.9\n', force=-1, missing_ok=True, ) self.assertFalse(entry['ip']) self.assertFalse(entry['dirty']) get_url.assert_not_called() warning.assert_not_called() verbose.assert_called_once_with(f'Using cached value for "{self.url}"') def test_empty_missing_ok_force(self, *_): """Test forced empty missing_ok source refresh (force>=0).""" cache = source_cache(self.url, refresh=self.now) entry, warning, verbose, get_url = self.run_refresh( source=self.url, cache=cache, content='10.0.0.9\n', missing_ok=True, ) self.assertIn('10.0.0.9', entry['ip']) self.assertTrue(entry['dirty']) get_url.assert_called_once() warning.assert_not_called() verbose.assert_called_once_with( f'Iplist content for "{self.url}" refreshed, 1 entries' ) def test_empty_missing_ok_past_refresh(self, *_): """Test refresh expiry of empty missing_ok cache, --soft(force<0).""" cache = source_cache(self.url, refresh=self.now - 1500) entry, warning, verbose, get_url = self.run_refresh( source=self.url, cache=cache, content='10.0.0.9\n', force=-1, missing_ok=True, timeout=2000, refresh=1000, ) self.assertIn('10.0.0.9', entry['ip']) self.assertTrue(entry['dirty']) get_url.assert_called_once() warning.assert_not_called() verbose.assert_called_once_with( f'Iplist content for "{self.url}" refreshed, 1 entries' ) def test_overwrite_false_appends(self, *_): """Test overwrite=no appends fetched addresses to cached ones.""" cache = source_cache( self.url, ip={'10.0.0.1': self.now}, refresh=self.now ) entry, warning, verbose, _ = self.run_refresh( source=self.url, cache=cache, content='10.0.0.9\n', overwrite=False, ) self.assertIn('10.0.0.1', entry['ip']) self.assertIn('10.0.0.9', entry['ip']) self.assertTrue(entry['dirty']) warning.assert_not_called() verbose.assert_called_once_with( f'Iplist content for "{self.url}" refreshed, 1 entries' ) def test_nonempty_url_no_missing_ok(self, *_): """Test forced refresh of non-empty cache (force>=0).""" cache = source_cache( self.url, ip={'10.0.0.1': self.now}, refresh=self.now ) entry, warning, verbose, _ = self.run_refresh( source=self.url, cache=cache, content='10.0.0.9\n' ) self.assertNotIn('10.0.0.1', entry['ip']) self.assertIn('10.0.0.9', entry['ip']) self.assertTrue(entry['dirty']) warning.assert_not_called() verbose.assert_called_once_with( f'Iplist content for "{self.url}" refreshed, 1 entries' ) def test_url_not_cached(self, *_): """Test uncached source is fetched.""" cache = source_cache() entry, warning, verbose, _ = self.run_refresh( source=self.url, cache=cache, content='10.0.0.9\n' ) self.assertIn('10.0.0.9', entry['ip']) self.assertTrue(entry['dirty']) warning.assert_not_called() verbose.assert_called_once_with( f'Iplist content for "{self.url}" refreshed, 1 entries' ) def test_url_cached_soft(self, *_): """Test reusing cache values with --soft(force<0).""" cache = source_cache( self.url, ip={'10.0.0.1': self.now}, refresh=self.now ) entry, warning, verbose, get_url = self.run_refresh( source=self.url, cache=cache, content='10.0.0.9\n', force=-1 ) self.assertIn('10.0.0.1', entry['ip']) self.assertNotIn('10.0.0.9', entry['ip']) self.assertFalse(entry['dirty']) get_url.assert_not_called() warning.assert_not_called() verbose.assert_called_once_with(f'Using cached value for "{self.url}"') def test_url_cached_soft_past_refresh(self, *_): """Test refresh expiry of non-empty cache, --soft(force<0).""" cache = source_cache( self.url, ip={'10.0.0.1': self.now}, refresh=self.now - 2000 ) entry, warning, verbose, get_url = self.run_refresh( source=self.url, cache=cache, content='10.0.0.9\n', force=-1, ) self.assertIn('10.0.0.9', entry['ip']) self.assertNotIn('10.0.0.1', entry['ip']) self.assertTrue(entry['dirty']) get_url.assert_called_once() warning.assert_not_called() verbose.assert_called_once_with( f'Iplist content for "{self.url}" refreshed, 1 entries' ) def test_url_fetch_fail(self, *_): """Test cache keeps old values when fetch fails.""" cache = source_cache( self.url, ip={'10.0.0.1': self.now}, refresh=self.now ) entry, warning, verbose, _ = self.run_refresh( source=self.url, cache=cache, content=None ) self.assertIn('10.0.0.1', entry['ip']) self.assertFalse(entry['dirty']) warning.assert_not_called() verbose.assert_not_called() def test_url_fetch_fail_expired(self, *_): """Test expired cache entry removal when fetch fails.""" cache = source_cache( self.url, ip={'10.0.0.1': self.now}, refresh=self.now - 1000 ) entry, warning, verbose, _ = self.run_refresh( source=self.url, cache=cache, content=None ) self.assertFalse(entry['ip']) self.assertTrue(entry['dirty']) warning.assert_not_called() verbose.assert_not_called() def test_file_source(self, *_): """Test local file source is read with get_file().""" file_path = './test.iplist' cache = source_cache( file_path, ip={'10.0.0.1': self.now}, refresh=self.now ) options = foomuuri.IPListSourceOptions() options.timeout = 1000 with ( unittest.mock.patch( 'foomuuri.get_file', return_value='10.0.0.7\n' ) as get_file, unittest.mock.patch('foomuuri.get_url') as get_url, unittest.mock.patch('foomuuri.warning') as warning, unittest.mock.patch('foomuuri.verbose') as verbose, ): cache.refresh_url_or_file(file_path, options) entry = cache[file_path] self.assertIn('10.0.0.7', entry['ip']) self.assertTrue(entry['dirty']) get_file.assert_called_once_with(file_path, options) get_url.assert_not_called() warning.assert_not_called() verbose.assert_called_once_with( f'Iplist content for "{file_path}" refreshed, 1 entries' ) foomuuri-0.34/src/tests/test_iplist_cache_resolve.py000066400000000000000000000201641526436212200230430ustar00rootroot00000000000000"""Test IPListSourceCache operations.""" # pylint: disable=invalid-name,import-error import copy import time import unittest.mock import foomuuri from foomuuri import INTERNAL as BASE_INTERNAL @unittest.mock.patch( 'foomuuri.INTERNAL', new_callable=lambda: copy.deepcopy(BASE_INTERNAL) ) class TestSourceCacheResolveHostnames(unittest.TestCase): """Test IPListSourceCache hostname resolution behaviour.""" def setUp(self): """Prepare test fixtures.""" self.now = int(time.time()) @staticmethod def source_options(timeout=1000, missing_ok=None, overwrite=None): """Return IPListSourceOptions instance.""" options = foomuuri.IPListSourceOptions() options.timeout = timeout options.missing_ok = missing_ok options.overwrite = overwrite return options @staticmethod def run_resolve(hostnames, cache, addresses=None, force=0): """Call IPListSourceCache.resolve_hostnames().""" # addresses: dict, hostname -> set of resolved IPs if addresses is None: addresses = {} foomuuri.INTERNAL.force = force cache = foomuuri.IPListSourceCache(cache) with ( unittest.mock.patch( 'foomuuri.resolve_one_hostname', side_effect=lambda name: addresses.get(name, set()), ), unittest.mock.patch('foomuuri.warning') as warning, unittest.mock.patch('foomuuri.verbose') as verbose, ): cache.resolve_hostnames(hostnames) return cache, warning, verbose def test_resolve_hostname_ok(self, *_): """Test hostname resolution and IP address caching.""" cache, warning, verbose = self.run_resolve( {'foo.bar': self.source_options(missing_ok=None)}, cache=foomuuri.IPListSourceCache(), addresses={'foo.bar': {'10.0.0.5'}}, ) entry = cache['foo.bar'] self.assertEqual(entry['ip']['10.0.0.5'], entry['refresh'] + 1000) self.assertTrue(entry['dirty']) warning.assert_not_called() verbose.assert_any_call('Hostname "foo.bar" resolved to: 10.0.0.5') def test_resolve_hostname_expire_never(self, *_): """Test very large timeout sets expiry to "never".""" cache, _, _ = self.run_resolve( { 'foo.bar': self.source_options( foomuuri.IPListSourceCache.expire_max + 1 ) }, cache=foomuuri.IPListSourceCache(), addresses={'foo.bar': {'10.0.0.9'}}, ) self.assertEqual( cache['foo.bar']['ip']['10.0.0.9'], foomuuri.IPListSourceCache.expire_never, ) def test_resolve_hostname_failed_warns(self, *_): """Test warning when hostname resolves to nothing.""" cache, warning, _ = self.run_resolve( {'foo.bar': self.source_options(missing_ok=None)}, cache=foomuuri.IPListSourceCache(), ) self.assertNotIn('foo.bar', cache) warning.assert_called_once_with( 'No IP address found for hostname "foo.bar" in iplist' ) def test_resolve_hostname_failed_missing_ok(self, *_): """Test no warning when missing_ok hostname resolves to nothing.""" cache, warning, _ = self.run_resolve( {'foo.bar': self.source_options(missing_ok=True)}, cache=foomuuri.IPListSourceCache(), ) self.assertNotIn('foo.bar', cache) warning.assert_not_called() def test_resolve_hostname_overwrite_replaces(self, *_): """Test overwrite=yes replace cached addresses instead of append.""" cache = foomuuri.IPListSourceCache( { 'foo.bar': { 'ip': {'10.0.0.1': 0}, 'dirty': False, 'refresh': self.now, } } ) cache, _, _ = self.run_resolve( {'foo.bar': self.source_options(overwrite=True)}, cache=foomuuri.IPListSourceCache(cache), addresses={'foo.bar': {'10.0.0.9'}}, ) self.assertEqual(set(cache['foo.bar']['ip']), {'10.0.0.9'}) def test_resolve_hostname_appends(self, *_): """Test hostname resolution appends IP addresses to cache.""" cache = foomuuri.IPListSourceCache( { 'foo.bar': { 'ip': {'10.0.0.1': 0}, 'dirty': False, 'refresh': self.now, } } ) cache, _, _ = self.run_resolve( {'foo.bar': self.source_options(missing_ok=True)}, cache=foomuuri.IPListSourceCache(cache), addresses={'foo.bar': {'10.0.0.9'}}, ) self.assertIn('10.0.0.1', cache['foo.bar']['ip']) self.assertIn('10.0.0.9', cache['foo.bar']['ip']) self.assertTrue(cache['foo.bar']['dirty']) def test_resolve_hostname_expired_cleared(self, *_): """Test resolution clears expired cached IP addresses.""" cache = foomuuri.IPListSourceCache( { 'foo.bar': { 'ip': {'10.0.0.1': 0}, 'dirty': False, 'refresh': self.now - 2000, } } ) cache, _, _ = self.run_resolve( {'foo.bar': self.source_options(timeout=1000)}, cache=foomuuri.IPListSourceCache(cache), addresses={'foo.bar': {'10.0.0.9'}}, ) self.assertNotIn('10.0.0.1', cache['foo.bar']['ip']) self.assertIn('10.0.0.9', cache['foo.bar']['ip']) def test_resolve_hostname_expired_soft_reresolves(self, *_): """Test --soft(force<0) mode re-resolves expired cache entry.""" cache = foomuuri.IPListSourceCache( { 'foo.bar': { 'ip': {'10.0.0.1': 0}, 'dirty': False, 'refresh': self.now - 2000, } } ) cache, warning, verbose = self.run_resolve( {'foo.bar': self.source_options(timeout=1000)}, cache=foomuuri.IPListSourceCache(cache), addresses={'foo.bar': {'10.0.0.9'}}, force=-1, ) self.assertNotIn('10.0.0.1', cache['foo.bar']['ip']) self.assertIn('10.0.0.9', cache['foo.bar']['ip']) warning.assert_not_called() verbose.assert_any_call('Hostname "foo.bar" resolved to: 10.0.0.9') def test_resolve_hostname_cached_soft(self, *_): """Test no resolution with fresh cache and --soft(force<0).""" cache = foomuuri.IPListSourceCache( { 'foo.bar': { 'ip': {'10.0.0.1': 0}, 'dirty': False, 'refresh': self.now, } } ) cache, warning, verbose = self.run_resolve( {'foo.bar': self.source_options(missing_ok=True)}, cache=foomuuri.IPListSourceCache(cache), addresses={'foo.bar': {'10.0.0.9'}}, force=-1, ) self.assertEqual(cache['foo.bar']['ip'], {'10.0.0.1': 0}) verbose.assert_called_once_with('Using cached value for "foo.bar"') warning.assert_not_called() def test_resolve_hostnames_all_cached_no_lookup(self, *_): """Test no resolution when all hostnames cached in soft mode.""" names = ('foo.bar', 'baz.qux') cache = foomuuri.IPListSourceCache( { name: { 'ip': {'10.0.0.1': 0}, 'dirty': False, 'refresh': self.now, } for name in names } ) snapshot = copy.deepcopy(cache) cache, warning, verbose = self.run_resolve( {name: self.source_options() for name in names}, cache=foomuuri.IPListSourceCache(cache), addresses={name: {'10.0.0.9'} for name in names}, force=-1, ) self.assertEqual(cache, snapshot) warning.assert_not_called() for call in verbose.call_args_list: self.assertNotIn('DNS lookup for:', call.args[0]) foomuuri-0.34/src/tests/test_iplist_cache_write.py000066400000000000000000000052241526436212200225160ustar00rootroot00000000000000"""Test IPListSourceCache.write().""" # pylint: disable=invalid-name,import-error import unittest import unittest.mock import foomuuri class TestSourceCacheWrite(unittest.TestCase): """Test IPListSourceCache.write().""" def test_write_removes_empty_entries(self): """Test write removes empty entries and transient metadata.""" cache = foomuuri.IPListSourceCache( { 'https://foo.bar/empty.txt': { 'ip': {}, 'dirty': True, 'refresh': 100, }, 'https://foo.bar/empty2.txt': { 'ip': {}, 'dirty': True, 'refresh': 100, }, 'https://foo.bar/keep.txt': { 'ip': {}, 'dirty': True, 'refresh': 100, }, 'manual.@empty': { 'ip': {}, 'dirty': True, 'refresh': 100, }, 'https://foo.bar/list.txt': { 'ip': {'10.0.0.1': 200}, 'dirty': True, 'refresh': 100, }, 'manual.@source': { 'ip': {'10.0.0.2': 300}, 'dirty': True, 'refresh': 100, }, } ) written_cache = { 'https://foo.bar/keep.txt': {'ip': {}, 'refresh': 100}, 'https://foo.bar/list.txt': { 'ip': {'10.0.0.1': 200}, 'refresh': 100, }, 'manual.@source': {'ip': {'10.0.0.2': 300}}, } filename = unittest.mock.Mock() iplists = foomuuri.IPLists() iplists['@missingok'] = foomuuri.IPList( options=foomuuri.IPListOptions(missing_ok=True), sources=['https://foo.bar/keep.txt'], ) iplists['@remove'] = foomuuri.IPList( options=foomuuri.IPListOptions(missing_ok=False), sources=['https://foo.bar/empty.txt'], ) iplists['@remove2'] = foomuuri.IPList( options=foomuuri.IPListOptions(missing_ok=None), sources=['https://foo.bar/empty2.txt'], ) with ( unittest.mock.patch('foomuuri.state_file', return_value=filename), unittest.mock.patch('foomuuri.save_file') as save_file, ): cache.write(iplists) self.assertEqual( cache, written_cache, ) save_file.assert_called_once_with( filename, written_cache, ) foomuuri-0.34/src/tests/test_iplist_list.py000066400000000000000000000102571526436212200212160ustar00rootroot00000000000000"""Test iplist list filtering.""" # pylint: disable=import-error,invalid-name # ruff: file-ignore[invalid-argument-name] import copy import unittest from unittest import mock import foomuuri from foomuuri import INTERNAL as BASE_INTERNAL @mock.patch( 'foomuuri.INTERNAL', new_callable=lambda: copy.deepcopy(BASE_INTERNAL) ) class TestIPListElementContains(unittest.TestCase): """Test iplist list filtering by IP address.""" def test_address_network_match(self, *_): """Test address, containing network and host bits.""" self.assertTrue( foomuuri.iplist_element_contains( '10.0.0.0/24', foomuuri.ipaddress.ip_network('10.0.0.1') ) ) self.assertTrue( foomuuri.iplist_element_contains( '10.0.0.0/24', foomuuri.ipaddress.ip_network('10.0.0.99/24', strict=False), ) ) self.assertFalse( foomuuri.iplist_element_contains( '10.0.0.1/32', foomuuri.ipaddress.ip_network('10.0.0.0/31') ) ) def test_range(self, *_): """Test range (interval) elements.""" self.assertTrue( foomuuri.iplist_element_contains( '10.0.0.10-10.0.0.20', foomuuri.ipaddress.ip_network('10.0.0.15'), ) ) self.assertFalse( foomuuri.iplist_element_contains( '10.0.0.10-10.0.0.20', foomuuri.ipaddress.ip_network('10.0.0.20/31'), ) ) def test_ip_versions_mismatch(self, *_): """Test IPv4/IPv6 mismatch.""" self.assertFalse( foomuuri.iplist_element_contains( '10.0.0.0/24', foomuuri.ipaddress.ip_network('2001:db8::1') ) ) @mock.patch( 'foomuuri.INTERNAL', new_callable=lambda: copy.deepcopy(BASE_INTERNAL) ) class TestCommandIPListList(unittest.TestCase): """Test command_iplist_list() filtering and rc.""" @staticmethod def run_command(INTERNAL, parameters, elements): """Run command with mocked configuration and nft output.""" INTERNAL.parameters = parameters data = {'nftables': [{'set': {'elem': elements}}]} with ( mock.patch('foomuuri.minimal_config', return_value={}), mock.patch( 'foomuuri.parse_config_iplist', return_value={'@foo': {}} ), mock.patch('foomuuri.nft_json', side_effect=[data, {}]), mock.patch('foomuuri.print_table'), ): return foomuuri.command_iplist_list() def test_matching_needle_returns_success(self, INTERNAL): """Return 0 when needle matches an element.""" self.assertEqual( self.run_command( INTERNAL, ['list', '@foo', '10.0.0.99/24'], ['10.0.0.0/24'], ), 0, ) def test_nonmatching_needle_returns_error(self, INTERNAL): """Return 1 when no element contains any needle.""" self.assertEqual( self.run_command( INTERNAL, ['list', '@foo', '192.0.2.1'], ['10.0.0.0/24'] ), 1, ) def test_no_needle_returns_success(self, INTERNAL): """Return zero for ordinary unfiltered listing.""" self.assertEqual( self.run_command(INTERNAL, ['list', '@foo'], ['10.0.0.1']), 0 ) def test_unsupported_filter_fails(self, INTERNAL): """Test unsupported IP filtering fails with exact error message.""" filters = ( '10.0.0.1-10.0.0.2', '-10.0.0.1', '[2001:db8::1]', ) for value in filters: with self.subTest(value=value): with ( mock.patch( 'foomuuri.fail', side_effect=SystemExit ) as fail, self.assertRaises(SystemExit), ): self.run_command(INTERNAL, ['list', '@foo', value], []) fail.assert_called_once_with( 'Filtering by IP range, negative or bracketed value ' f'is not supported: {value}' ) foomuuri-0.34/src/tests/test_iplist_output.py000066400000000000000000000102241526436212200215750ustar00rootroot00000000000000"""Test iplist_output_values().""" # pylint: disable=invalid-name,import-error import copy import time import unittest.mock import foomuuri from foomuuri import INTERNAL as BASE_INTERNAL from src.tests.test_iplist_cache_refresh import source_cache @unittest.mock.patch( 'foomuuri.INTERNAL', new_callable=lambda: copy.deepcopy(BASE_INTERNAL) ) class TestIPListOutputValues(unittest.TestCase): """Test iplist_output_values().""" def setUp(self): """Prepare test fixtures.""" self.now = int(time.time()) self.url = 'https://foo.bar/iplist' @staticmethod def run_output( sources, cache, iplist_name, force=0, missing_ok=None, ): """Call iplist_output_values() with mocked IO.""" foomuuri.INTERNAL.command = 'iplist' foomuuri.INTERNAL.force = force iplists = foomuuri.IPLists() options = foomuuri.IPListSourceOptions() options.missing_ok = missing_ok iplists[iplist_name] = foomuuri.IPList( options=options, sources=sources, ) update_only = {iplist_name} currently_active = {iplist_name} with ( unittest.mock.patch('foomuuri.warning') as warning, unittest.mock.patch('foomuuri.fail') as fail, unittest.mock.patch('foomuuri.verbose') as verbose, unittest.mock.patch('foomuuri.OUT', new=[]), ): ret = foomuuri.iplist_output_values( iplists, cache, currently_active, update_only, True ) return ret, warning, fail, verbose def test_set_single_missing_ok_empty(self, *_): """Test warning and rc 0 for missing_ok source with empty content.""" cache = source_cache(self.url) ret, warning, fail, verbose = self.run_output( sources=[self.url], cache=cache, iplist_name='@foo', missing_ok=True, ) self.assertEqual(ret, 0) warning.assert_called_once_with('Iplist "@foo" is empty') fail.assert_not_called() verbose.assert_not_called() def test_set_single_no_missing_ok_empty(self, *_): """Test fail and rc 2 for source with empty content.""" cache = source_cache(self.url) ret, warning, fail, verbose = self.run_output( sources=[self.url], cache=cache, iplist_name='@foo', ) self.assertEqual(ret, 2) fail.assert_called_once_with('Iplist "@foo" is empty', False) warning.assert_not_called() verbose.assert_not_called() def test_set_clean_soft(self, *_): """Test verbose skip update message with --soft (force<0).""" cache = source_cache(self.url) ret, warning, fail, verbose = self.run_output( sources=[self.url], cache=cache, iplist_name='@foo', force=-1, ) self.assertEqual(ret, 0) warning.assert_not_called() fail.assert_not_called() verbose.assert_called_once_with( 'Iplist "@foo" is clean, skipping update' ) def test_set_dirty_soft_updates(self, *_): """Test dirty cache updates with --soft (force<0).""" cache = source_cache(self.url, ip={'10.0.0.1': self.now}) cache[self.url]['dirty'] = True ret, warning, fail, verbose = self.run_output( sources=[self.url], cache=cache, iplist_name='@foo', force=-1, ) self.assertEqual(ret, 0) warning.assert_not_called() fail.assert_not_called() verbose.assert_called_once_with('Updating iplist "@foo", 1 entries') def test_set_nonempty_updates(self, *_): """Test verbose update message for non-empty set.""" cache = source_cache(self.url, ip={'10.0.0.1': self.now}) ret, warning, fail, verbose = self.run_output( sources=[self.url], cache=cache, iplist_name='@foo', ) self.assertEqual(ret, 0) warning.assert_not_called() fail.assert_not_called() verbose.assert_called_once_with('Updating iplist "@foo", 1 entries') foomuuri-0.34/src/tests/test_iplist_source_options.py000066400000000000000000000057531526436212200233230ustar00rootroot00000000000000"""Unit tests of IPListSourceOptions.set_option().""" # pylint: disable=invalid-name,import-error import unittest.mock from foomuuri import IPListSourceOptions class TestIPListSourceOptionsSetOption(unittest.TestCase): """Test IPListSourceOptions set_option.""" @staticmethod def run_set_option(name, new_value, source='https://foo.bar'): """Call IPListSourceOptions.set_option() with mocked warning.""" options = IPListSourceOptions() with unittest.mock.patch('foomuuri.warning') as warning: options.set_option(name, new_value, source) return options, warning def test_set_over_default_none(self): """Test setting True or False over None.""" for value in (True, False): with self.subTest(msg=''): options, warning = self.run_set_option('overwrite', value) self.assertEqual(options.overwrite, value) warning.assert_not_called() def test_set_none_over_none(self): """Test setting None over None.""" options, warning = self.run_set_option('overwrite', None) self.assertIsNone(options.overwrite) warning.assert_not_called() def test_set_same_value(self): """Test setting True/False over the same value.""" for value in (True, False): with self.subTest(msg=f'Setting {value} to {value}'): options = IPListSourceOptions() options.overwrite = value with unittest.mock.patch('foomuuri.warning') as warning: options.set_option('overwrite', value, 'https://foo.bar') self.assertEqual(options.overwrite, value) warning.assert_not_called() def test_conflicting_value_warning(self): """Test flipping True<->False value emits warning.""" options = IPListSourceOptions() options.overwrite = True for value in (True, False): with self.subTest(msg=f'Flipping {not value} to {value}'): options.overwrite = not value with unittest.mock.patch('foomuuri.warning') as warning: options.set_option('overwrite', value, 'https://foo.bar') self.assertEqual(options.overwrite, value) warning.assert_called_once_with( 'Conflicting iplist source option "overwrite" for "https://foo.bar"' ) def test_conflicting_none_keeps_value(self): """Test setting None over True/False prints warning and keeps value.""" for value in (True, False): options = IPListSourceOptions() options.overwrite = value with unittest.mock.patch('foomuuri.warning') as warning: options.set_option('overwrite', None, 'https://foo.bar') self.assertEqual(options.overwrite, value) warning.assert_called_once_with( 'Conflicting iplist source option "overwrite" for ' '"https://foo.bar"' ) foomuuri-0.34/src/tests/test_is_port.py000066400000000000000000000034651526436212200203410ustar00rootroot00000000000000"""Basic unit tests for is_port().""" # pylint: disable=import-error import unittest.mock from foomuuri import Rule, is_port class TestIsPort(unittest.TestCase): """Basic unit tests for is_port().""" def run_is_port( self, protocol: str, value: str, expected: bool, warn_output: bool = False, ) -> None: """Run single test.""" with unittest.mock.patch('foomuuri.warning') as warning: rule = Rule(protocol=protocol, fileline='', line=[]) ret = is_port(value, rule) self.assertEqual(ret, expected) if warn_output: warning.assert_called_once() else: warning.assert_not_called() def test_valid(self): """Test valid values.""" for item in ( 'tcp 22', 'tcp 1-3', 'tcp -2', 'icmp 8', 'icmp -8', 'icmp redirect', 'icmp echo-request', 'icmp -echo-request', ): protocol, value = item.split(' ', 1) self.run_is_port(protocol, value, True) def test_invalid(self): """Test invalid values.""" for item in ( 'tcp -', 'tcp a', 'tcp 1.1', 'tcp 1-', 'tcp -1-', 'tcp foo', 'tcp ,', 'tcp 9,', 'icmp foo', ): protocol, value = item.split(' ', 1) self.run_is_port(protocol, value, False) def test_warning(self): """Test accepted values with warning output.""" for item in ( 'tcp 1,2', 'tcp 1,2-3', 'icmp redirect,8', ): protocol, value = item.split(' ', 1) self.run_is_port(protocol, value, True, True) foomuuri-0.34/src/tests/test_join_args.py000066400000000000000000000010661526436212200206300ustar00rootroot00000000000000"""Basic unit tests of join_args().""" # pylint: disable=import-error import pathlib import unittest from foomuuri import join_args class TestJoinArgs(unittest.TestCase): """Test join_args().""" def test_valid(self): """Test valid outcomes.""" self.assertEqual(join_args([]), '') self.assertEqual(join_args(['ab', 'cd']), 'ab cd') self.assertEqual(join_args(['ab', pathlib.PosixPath('cd')]), 'ab cd') def test_invalid(self): """Test invalid outcomes.""" self.assertRaises(TypeError, join_args, 'ab') foomuuri-0.34/src/tests/test_parse_command_line.py000066400000000000000000000171461526436212200225020ustar00rootroot00000000000000"""Basic unit tests of parse_command_line().""" # pylint: disable=invalid-name,import-error # ruff: file-ignore[invalid-argument-name] import copy import unittest import unittest.mock from foomuuri import CONFIG as BASE_CONFIG from foomuuri import INTERNAL as BASE_INTERNAL from foomuuri import parse_command_line @unittest.mock.patch( 'foomuuri.CONFIG', new_callable=lambda: copy.deepcopy(BASE_CONFIG) ) @unittest.mock.patch( 'foomuuri.INTERNAL', new_callable=lambda: copy.deepcopy(BASE_INTERNAL) ) class TestParseCommandLine(unittest.TestCase): """Test parse_command_line().""" @unittest.mock.patch('sys.argv', ['foomuuri']) def test_no_arguments(self, INTERNAL, *_): """Test no arguments.""" parse_command_line() self.assertEqual(INTERNAL.command, '') self.assertEqual(INTERNAL.parameters, []) @unittest.mock.patch('sys.argv', ['foomuuri', '--help']) def test_option_help(self, INTERNAL, *_): """Test --help option (sets 'help' command).""" parse_command_line() self.assertEqual(INTERNAL.command, 'help') self.assertEqual(INTERNAL.parameters, []) @unittest.mock.patch('sys.argv', ['foomuuri', '--version']) def test_option_version(self, INTERNAL, *_): """Test --version option (sets 'version' command).""" parse_command_line() self.assertEqual(INTERNAL.command, 'version') self.assertEqual(INTERNAL.parameters, []) @unittest.mock.patch( 'sys.argv', ['foomuuri', '--verbose', 'command', 'arg1', '--force', 'arg2'], ) def test_command_and_options(self, INTERNAL, CONFIG): """Test command with arguments, mixed with options.""" parse_command_line() self.assertEqual(INTERNAL.command, 'command') self.assertEqual(INTERNAL.parameters, ['arg1', 'arg2']) self.assertEqual(INTERNAL.force, 1) self.assertEqual(CONFIG.verbose, 1) self.assertEqual(INTERNAL.config_override['verbose'], 1) @unittest.mock.patch('sys.argv', ['foomuuri', '--syslog', '--fork']) def test_options_syslog_fork(self, INTERNAL, CONFIG): """Test --syslog and --fork options.""" parse_command_line() self.assertEqual(CONFIG.syslog, 1) self.assertEqual(CONFIG.fork, 1) self.assertEqual(INTERNAL.config_override['syslog'], 1) self.assertEqual(INTERNAL.config_override['fork'], 1) self.assertEqual(INTERNAL.command, '') self.assertEqual(INTERNAL.parameters, []) @unittest.mock.patch('sys.argv', ['foomuuri', '--verbose']) def test_option_verbose(self, INTERNAL, CONFIG): """Test --verbose option.""" parse_command_line() self.assertEqual(CONFIG.verbose, 1) self.assertEqual(INTERNAL.config_override['verbose'], 1) self.assertEqual(INTERNAL.command, '') self.assertEqual(INTERNAL.parameters, []) @unittest.mock.patch('sys.argv', ['foomuuri', '--verbose', '--verbose']) def test_options_verbose_verbose(self, INTERNAL, CONFIG): """Test --verbose --verbose options.""" parse_command_line() self.assertEqual(CONFIG.verbose, 2) self.assertEqual(INTERNAL.config_override['verbose'], 2) self.assertEqual(INTERNAL.command, '') self.assertEqual(INTERNAL.parameters, []) @unittest.mock.patch('sys.argv', ['foomuuri', '--quiet']) def test_option_quiet(self, INTERNAL, CONFIG): """Test --quiet option.""" parse_command_line() self.assertEqual(CONFIG.verbose, -1) self.assertEqual(INTERNAL.config_override['verbose'], -1) self.assertEqual(INTERNAL.command, '') self.assertEqual(INTERNAL.parameters, []) @unittest.mock.patch('sys.argv', ['foomuuri', '--verbose', '--quiet']) def test_options_verbose_quiet(self, INTERNAL, CONFIG): """Test --verbose --quiet options.""" parse_command_line() self.assertEqual(CONFIG.verbose, 0) self.assertEqual(INTERNAL.config_override['verbose'], 0) self.assertEqual(INTERNAL.command, '') self.assertEqual(INTERNAL.parameters, []) @unittest.mock.patch('sys.argv', ['foomuuri', '--force']) def test_option_force(self, INTERNAL, *_): """Test --force option.""" parse_command_line() self.assertEqual(INTERNAL.force, 1) self.assertEqual(INTERNAL.command, '') self.assertEqual(INTERNAL.parameters, []) @unittest.mock.patch('sys.argv', ['foomuuri', '--soft']) def test_option_soft(self, INTERNAL, *_): """Test --soft option.""" parse_command_line() self.assertEqual(INTERNAL.force, -1) self.assertEqual(INTERNAL.command, '') self.assertEqual(INTERNAL.parameters, []) @unittest.mock.patch('sys.argv', ['foomuuri', '--set']) @unittest.mock.patch('foomuuri.fail', side_effect=SystemExit) def test_option_set_unknown(self, fail, *_): """Test --set option without suffix.""" self.assertRaises(SystemExit, parse_command_line) fail.assert_called_with('Unknown option: --set') @unittest.mock.patch('sys.argv', ['foomuuri', '--set=']) @unittest.mock.patch('foomuuri.fail', side_effect=SystemExit) def test_option_set_invalid_syntax(self, fail, *_): """Test incomplete --set option syntax.""" self.assertRaises(SystemExit, parse_command_line) fail.assert_called_with( 'Invalid syntax for --set=OPTION=VALUE: --set=' ) @unittest.mock.patch('sys.argv', ['foomuuri', '--set=unknown=value']) @unittest.mock.patch('foomuuri.fail', side_effect=SystemExit) def test_option_set_unknown_option(self, fail, *_): """Test --set option with unknown foomuuri{} CONFIG option.""" self.assertRaises(SystemExit, parse_command_line) fail.assert_called_with('Unknown foomuuri{} option: unknown') @unittest.mock.patch('sys.argv', ['foomuuri', '--set=priority_offset=yes']) @unittest.mock.patch('foomuuri.fail', side_effect=SystemExit) def test_option_set_invalid_value(self, fail, *_): """Test --set option with invalid value.""" self.assertRaises(SystemExit, parse_command_line) fail.assert_called_with( 'Invalid foomuuri{} option priority_offset value: yes' ) @unittest.mock.patch( 'sys.argv', ['foomuuri', '--set=priority_offset=42', '--set=set_size=1'], ) def test_options_set_valid(self, INTERNAL, CONFIG): """Test --set options with valid foomuuri{} CONFIG option/value.""" parse_command_line() self.assertEqual(CONFIG.priority_offset, 42) self.assertEqual(CONFIG.set_size, 1) self.assertEqual(INTERNAL.command, '') self.assertEqual(INTERNAL.parameters, []) @unittest.mock.patch('sys.argv', ['foomuuri', '--unknown']) @unittest.mock.patch('foomuuri.fail', side_effect=SystemExit) def test_unknown_option(self, fail, *_): """Test unknown option.""" self.assertRaises(SystemExit, parse_command_line) fail.assert_called_with('Unknown option: --unknown') @unittest.mock.patch('sys.argv', ['foomuuri', 'command', '-h', '-v']) def test_option_short(self, INTERNAL, *_): """Test short options. They are parsed as parameters.""" parse_command_line() self.assertEqual(INTERNAL.command, 'command') self.assertEqual(INTERNAL.parameters, ['-h', '-v']) @unittest.mock.patch('sys.argv', ['foomuuri', 'command', 'a', '-', 'b']) def test_option_hyphen(self, INTERNAL, *_): """Test hyphen. It is parsed as parameter.""" parse_command_line() self.assertEqual(INTERNAL.command, 'command') self.assertEqual(INTERNAL.parameters, ['a', '-', 'b']) foomuuri-0.34/src/tests/test_parse_config_foomuuri.py000066400000000000000000000121741526436212200232430ustar00rootroot00000000000000"""Basic unit tests of parse_command_line().""" # pylint: disable=invalid-name,import-error # ruff: file-ignore[invalid-argument-name] import contextlib import copy import unittest import unittest.mock from foomuuri import CONFIG as BASE_CONFIG from foomuuri import INTERNAL as BASE_INTERNAL from foomuuri import minimal_config @unittest.mock.patch( 'foomuuri.INTERNAL', new_callable=lambda: copy.deepcopy(BASE_INTERNAL) ) @unittest.mock.patch( 'foomuuri.CONFIG', new_callable=lambda: copy.deepcopy(BASE_CONFIG) ) class TestParseConfigFoomuuri(unittest.TestCase): """Test parse_config_foomuuri().""" @staticmethod @contextlib.contextmanager def mock_config_foomuuri(option, value): """Return context manager of foomuuri.find_config_files() patch. foomuuri.find_config_files() is patched to return generated foomuuri configuration file, containing foomuuri{} section with single option/value. """ # MagicMock of pathlib.PosixPAth returning desired foomuuri config config_file = unittest.mock.MagicMock() config_file.read_text.return_value = f""" foomuuri {{ {option} {value} }} """ # foomuuri.find_config_files() is called by foomuuri.read_config() # to load configuration files. # read_config() tries loading share_config first, and etc_config then. # We only care about etc_config, therefore side_effect list passed # to foomuuri.find_config_files mock patch has two elemeents, # [] - empty share_config, and [config_file] - generated config. with unittest.mock.patch( 'foomuuri.find_config_files', side_effect=[[], [config_file]] ): yield config_file @unittest.mock.patch('foomuuri.warning') def test_try_reload_timeout(self, warning, CONFIG, *_): """Test try-reload_timeout backwards compatible option.""" with self.mock_config_foomuuri( 'try-reload_timeout', 69, ) as config_file: _ = minimal_config() self.assertEqual(CONFIG.try_reload_timeout, 69) warning.assert_called_once_with( f'File {config_file} line 3: foomuuri{{}} option ' f'"try-reload_timeout" is obsolete, use ' f'"try_reload_timeout" instead' ) def test_append_value(self, CONFIG, *_): """Test appending of option value.""" with self.mock_config_foomuuri('log_level', '+ append'): _ = minimal_config() self.assertEqual(CONFIG.log_level, 'level info flags skuid append') def test_unknown_option(self, *_): """Test assigning unknown option.""" with ( self.mock_config_foomuuri( 'unknown_option', 'value' ) as config_file, unittest.mock.patch( 'foomuuri.fail', side_effect=SystemExit ) as fail, ): self.assertRaises(SystemExit, minimal_config) fail.assert_called_once_with( f'File {config_file} line 3: Unknown foomuuri{{}}' ' option: unknown_option value' ) def test_invalid_value(self, *_): """Test assigning invalid value.""" with ( self.mock_config_foomuuri( 'priority_offset', 'not_an_int' ) as config_file, unittest.mock.patch( 'foomuuri.fail', side_effect=SystemExit ) as fail, ): self.assertRaises(SystemExit, minimal_config) fail.assert_called_once_with( f'File {config_file} line 3: Invalid foomuuri{{}}' ' option value: priority_offset not_an_int' ) def test_config_override(self, CONFIG, INTERNAL): """Test overriding of CONFIG from config_override.""" def test_override(option, override, config): with self.mock_config_foomuuri(option, '0'): INTERNAL.config_override[option] = override _ = minimal_config() self.assertEqual(CONFIG[option], config) # Overriding with correct value type (str) test_override(option='priority_offset', override='69', config=69) # Overriding with invalid value type (int). test_override(option='priority_offset', override=69, config=69) def test_postprocess_priority_offset(self, _, INTERNAL): """Test postprocessing of priority_offset option.""" def test_priority_offset(config, internal): with self.mock_config_foomuuri('priority_offset', config): _ = minimal_config() self.assertEqual(INTERNAL.priority_offset, internal) test_priority_offset(config=-1, internal=' - 1') test_priority_offset(config=0, internal='') test_priority_offset(config=1, internal=' + 1') def test_postprocess_log_rates(self, CONFIG, *_): """Test postprocessing of log_* rates options.""" with self.mock_config_foomuuri('log_rate', '1/second burst 3'): _ = minimal_config() self.assertTrue(CONFIG.log_rate.endswith(' packets')) foomuuri-0.34/src/tests/test_parse_config_iplist.py000066400000000000000000000404511526436212200227010ustar00rootroot00000000000000"""Basic unit tests of parse_config_iplist().""" # pylint: disable=invalid-name,import-error # ruff: file-ignore[invalid-argument-name] import contextlib import copy import unittest import unittest.mock from foomuuri import CONFIG as BASE_CONFIG from foomuuri import INTERNAL as BASE_INTERNAL from foomuuri import ( IPListOptions, minimal_config, parse_config_iplist, parse_duration, ) @unittest.mock.patch( 'foomuuri.INTERNAL', new_callable=lambda: copy.deepcopy(BASE_INTERNAL) ) @unittest.mock.patch( 'foomuuri.CONFIG', new_callable=lambda: copy.deepcopy(BASE_CONFIG) ) class TestIplistParseConfig(unittest.TestCase): """Test parse_config_iplist().""" @staticmethod @contextlib.contextmanager def mock_config_foomuuri(section: str = 'iplist', content: str = ''): """Return context manager of foomuuri.find_config_files() patch. foomuuri.find_config_files() is patched to return generated foomuuri configuration file, containing arbitrary section {} with specified content. """ # consult tests/test_parse_config_foomuuri.py for mock explanation config_file = unittest.mock.MagicMock() config_file.read_text.return_value = f""" {section} {{ {content} }} """ with unittest.mock.patch( 'foomuuri.find_config_files', side_effect=[[], [config_file]] ): yield config_file @unittest.mock.patch('foomuuri.warning') def test_obsolete_syntax(self, warning, *_): """Test obsolete resolve{} section and timeout/refresh options.""" with self.mock_config_foomuuri( section='resolve', content=""" timeout 1d 5m refresh 0d 30m @foo https://foob.ar/ """, ) as config_file: iplists = parse_config_iplist(minimal_config()) options = iplists['@foo'].options self.assertEqual(options.dns_timeout, parse_duration('1d 5m', 0)) self.assertEqual(options.url_timeout, parse_duration('1d 5m', 0)) self.assertEqual(options.dns_refresh, parse_duration('0d 30m', 0)) self.assertEqual(options.url_refresh, parse_duration('0d 30m', 0)) self.assertEqual(iplists['@foo'].sources, ['https://foob.ar/']) warning.assert_has_calls( [ unittest.mock.call( 'Section "resolve" is obsolete, use "iplist" instead', ), unittest.mock.call( f'File {config_file} line 4: Iplist "timeout" is ' 'obsolete, use "dns_timeout" or "url_timeout" instead', ), unittest.mock.call( f'File {config_file} line 5: Iplist "refresh" is ' 'obsolete, use "dns_refresh" or "url_refresh" instead', ), ] ) @unittest.mock.patch('foomuuri.warning') @unittest.mock.patch('foomuuri.fail', side_effect=SystemExit) def test_obsolete_syntax_fail(self, fail, warning, *_): """Test iplist{} section with invalid obsolete timeout option.""" with self.mock_config_foomuuri( section='iplist', content=""" timeout # value is missing """, ) as config_file: with self.assertRaises(SystemExit): _ = parse_config_iplist(minimal_config()) warning.assert_called_once_with( f'File {config_file} line 4: Iplist "timeout" is obsolete, ' 'use "dns_timeout" or "url_timeout" instead', ) fail.assert_called_once_with( f'File {config_file} line 4: Invalid iplist{{}} option ' 'value: dns_timeout=' ) @unittest.mock.patch('foomuuri.warning') def test_obsolete_missing_ok_filter(self, warning, *_): """Test conversion of obsolete |missing-ok filter to option.""" with self.mock_config_foomuuri( section='iplist', content=""" @foo https://foo.bar|missing-ok """, ) as config_file: iplists = parse_config_iplist(minimal_config()) self.assertTrue(iplists['@foo'].options.missing_ok) self.assertEqual( iplists['@foo'].sources, ['https://foo.bar|missing-ok'] ) warning.assert_called_once_with( f'File {config_file} line 4: iplist{{}} filter ' '"|missing-ok" in "@foo" is obsolete, ' 'use "missing_ok=yes" instead' ) def test_empty_iplist_definition(self, *_): """Test empty iplist definition.""" with self.mock_config_foomuuri( section='iplist', content=""" @foo @foo - """, ): iplists = parse_config_iplist(minimal_config()) self.assertEqual(iplists['@foo'].sources, []) def test_valid_default_options(self, *_): """Test valid default options, space and equal-sign styles.""" with self.mock_config_foomuuri( section='iplist', content=""" url_max_size 1024 url_timeout=69d url_refresh=42d dns_timeout 1d 2h # space-separated option value is # supported by parse_duration() @foo https://foob.ar/ """, ): iplists = parse_config_iplist(minimal_config()) options = iplists['@foo'].options self.assertEqual(options.url_timeout, parse_duration('69d', 0)) self.assertEqual(options.url_refresh, parse_duration('42d', 0)) self.assertEqual(options.dns_timeout, parse_duration('1d 2h', 0)) self.assertEqual(options.url_max_size, 1024) def test_single_line_multiple_valid_default_options(self, *_): """Test mulitple valid default options on single line.""" with self.mock_config_foomuuri( section='iplist', content=""" url_timeout=69d url_refresh=42d @foo https://foob.ar/ """, ): iplists = parse_config_iplist(minimal_config()) options = iplists['@foo'].options self.assertEqual(options.url_timeout, parse_duration('69d', 0)) self.assertEqual(options.url_refresh, parse_duration('42d', 0)) @unittest.mock.patch('foomuuri.fail', side_effect=SystemExit) def test_first_invalid_default_option(self, fail, *_): """Test first invalid default option on single line.""" with self.mock_config_foomuuri( section='iplist', content=""" invalid url_timeout=2d url_refresh=1d @foo https://foob.ar/ """, ) as config_file: with self.assertRaises(SystemExit): _ = parse_config_iplist(minimal_config()) fail.assert_called_once_with( f'File {config_file} line 4: Invalid iplist{{}} option name: ' 'invalid=url_timeout=2d url_refresh=1d' ) @unittest.mock.patch('foomuuri.fail', side_effect=SystemExit) def test_second_invalid_default_option(self, fail, *_): """Test second invalid default option on single line.""" with self.mock_config_foomuuri( section='iplist', content=""" url_timeout=2d invalid url_refresh=1d @foo https://foob.ar/ """, ) as config_file: with self.assertRaises(SystemExit): _ = parse_config_iplist(minimal_config()) fail.assert_called_once_with( f'File {config_file} line 4: Unsupported syntax: ' 'url_timeout=2d invalid url_refresh=1d' ) @unittest.mock.patch('foomuuri.fail', side_effect=SystemExit) def test_invalid_space_style_default_option_name(self, fail, *_): """Test invalid space-style default option name.""" with self.mock_config_foomuuri( section='iplist', content=""" url_min_size 1024 """, ) as config_file: with self.assertRaises(SystemExit): _ = parse_config_iplist(minimal_config()) fail.assert_called_once_with( f'File {config_file} line 4: Invalid iplist{{}} option name: ' 'url_min_size=1024' ) @unittest.mock.patch('foomuuri.fail', side_effect=SystemExit) def test_invalid_space_style_default_option_value(self, fail, *_): """Test invalid space-style default option value.""" with self.mock_config_foomuuri( section='iplist', content=""" url_max_size abcd """, ) as config_file: with self.assertRaises(SystemExit): _ = parse_config_iplist(minimal_config()) fail.assert_called_once_with( f'File {config_file} line 4: Invalid iplist{{}} option value: ' 'url_max_size=abcd' ) @unittest.mock.patch('foomuuri.fail', side_effect=SystemExit) def test_invalid_default_option_name(self, fail, *_): """Test invalid default option name.""" with self.mock_config_foomuuri( section='iplist', content=""" invalid_option=whatever_value """, ) as config_file: with self.assertRaises(SystemExit): _ = parse_config_iplist(minimal_config()) fail.assert_called_once_with( f'File {config_file} line 4: Invalid iplist{{}} option name: ' 'invalid_option=whatever_value' ) @unittest.mock.patch('foomuuri.fail', side_effect=SystemExit) def test_invalid_default_option_value(self, fail, *_): """Test invalid default option value.""" with self.mock_config_foomuuri( section='iplist', content=""" url_max_size=abcd """, ) as config_file: with self.assertRaises(SystemExit): _ = parse_config_iplist(minimal_config()) fail.assert_called_once_with( f'File {config_file} line 4: Invalid iplist{{}} option value: ' 'url_max_size=abcd' ) def test_start_merge_url_size_options(self, *_): """Test start, merge, url_size options.""" with self.mock_config_foomuuri( section='iplist', content=""" -start -merge url_max_size=1024 @foo https://foob.ar/ @bar https://b.ar/ start=no merge=no @baz https://b.az/ start=yes merge=yes url_max_size=2048 """, ): iplists = parse_config_iplist(minimal_config()) default_options = IPListOptions() # Testing applcation as default options # Asserting applied options differ from default values self.assertNotEqual( iplists['@foo'].options.start, default_options.start ) self.assertNotEqual( iplists['@foo'].options.merge, default_options.merge ) self.assertNotEqual( iplists['@foo'].options.url_max_size, default_options.url_max_size, ) # Asserting applied option values are exactly as set self.assertFalse(iplists['@foo'].options.start) self.assertFalse(iplists['@foo'].options.merge) self.assertFalse(iplists['@foo'].options.dynamic) self.assertEqual(iplists['@foo'].options.url_max_size, 1024) # Alternative start/merge syntax (false) self.assertFalse(iplists['@bar'].options.start) self.assertFalse(iplists['@bar'].options.merge) # Alternative start/merge syntax (true) self.assertTrue(iplists['@baz'].options.start) self.assertTrue(iplists['@baz'].options.merge) # Iplist url_max_size overrides default option self.assertTrue(iplists['@baz'].options.url_max_size, 2048) @unittest.mock.patch('foomuuri.warning') def test_iplist_sources_overwrite(self, warning, *_): """Test overwriting of iplist sources.""" with self.mock_config_foomuuri( section='iplist', content=""" @foo https://foob.ar/ @foo https://rabo.of/ """, ) as config_file: iplists = parse_config_iplist(minimal_config()) self.assertEqual( iplists['@foo'].sources, ['https://rabo.of/'], ) warning.assert_called_with( f'File {config_file} line 5: Overwriting iplist "@foo" ' 'with value "https://rabo.of/"' ) def test_iplist_sources_append(self, *_): """Test appending of iplist sources.""" with self.mock_config_foomuuri( section='iplist', content=""" @foo https://foob.ar/ @foo + https://rabo.of/ @bar + https://foo.bar/ @baz + """, ): iplists = parse_config_iplist(minimal_config()) self.assertEqual( iplists['@foo'].sources, ['https://foob.ar/', 'https://rabo.of/'], ) self.assertEqual(iplists['@bar'].sources, ['https://foo.bar/']) self.assertEqual(iplists['@baz'].sources, []) def test_intermixing_iplist_sources_options(self, *_): """Test itermixing of iplist sources and options.""" with self.mock_config_foomuuri( section='iplist', content=""" @foo start=no https://foob.ar/ merge=no https://rabo.of/ """, ): iplists = parse_config_iplist(minimal_config()) default_options = IPListOptions() # Asserting applied options differ from default values self.assertNotEqual( iplists['@foo'].options.start, default_options.start ) self.assertNotEqual( iplists['@foo'].options.merge, default_options.merge ) # Asserting appled options values are exactly as set self.assertFalse(iplists['@foo'].options.start) self.assertFalse(iplists['@foo'].options.merge) self.assertEqual( iplists['@foo'].sources, ['https://foob.ar/', 'https://rabo.of/'], ) def test_last_option_priority_wins(self, *_): """Test last option defined for same ipset definition wins.""" with self.mock_config_foomuuri( section='iplist', content=""" @foo url_timeout=42d @foo url_timeout=69d """, ): iplists = parse_config_iplist(minimal_config()) self.assertEqual( iplists['@foo'].options.url_timeout, parse_duration('69d', 0) ) @unittest.mock.patch('foomuuri.verbose') def test_iplist_name_and_sources_vebose_output(self, verbose, CONFIG, *_): """Test output of iplists name and sources when verbose >= 2.""" with self.mock_config_foomuuri( section='iplist', content=""" @foo https://foob.ar/ @bar https://foo.bar/ """, ): CONFIG.verbose = 2 _ = parse_config_iplist(minimal_config()) verbose.assert_has_calls( [ unittest.mock.call('@foo https://foob.ar/'), unittest.mock.call('@bar https://foo.bar/'), ] ) def test_iplist_dynamic(self, *_): """Test dynamic flag in iplist.""" with self.mock_config_foomuuri( section='iplist', content=""" @foo dynamic=yes @bar dynamic=yes element_timeout=1m30s """, ): iplists = parse_config_iplist(minimal_config()) self.assertTrue(iplists['@foo'].options.dynamic) self.assertTrue(iplists['@bar'].options.dynamic) self.assertEqual( iplists['@foo'].options.element_timeout, parse_duration('0s', -1), ) self.assertEqual( iplists['@bar'].options.element_timeout, parse_duration('1m30s', -1), ) foomuuri-0.34/src/tests/test_parse_rule_line.py000066400000000000000000000022051526436212200220210ustar00rootroot00000000000000"""Test parse_rule_line().""" # pylint: disable=import-error import unittest import unittest.mock from foomuuri import parse_rule_line class TestParseRuleLine(unittest.TestCase): """Test parse_rule_line().""" @unittest.mock.patch('foomuuri.fail', side_effect=SystemExit) def test_incomplete_helper(self, fail): """Reject incomplete helper rule.""" for line, error in ( (['helper', 'ftp-21'], '"helper" without "protocol" is not valid'), ( ['tcp', 'helper', 'ftp-21'], '"helper" without "dport" is not valid', ), ): with self.subTest(line=line): with self.assertRaises(SystemExit): parse_rule_line(('File line: ', line)) fail.assert_called_with(f'File line: {error}') def test_valid_helper(self): """Accept valid helper rule.""" rule = parse_rule_line( ('File line: ', ['tcp', '21', 'helper', 'ftp-21']) ) self.assertEqual(rule.protocol, 'tcp') self.assertEqual(rule.dport, '21') self.assertEqual(rule.helper, 'ftp-21') foomuuri-0.34/src/tests/test_print_table.py000066400000000000000000000065061526436212200211640ustar00rootroot00000000000000"""Basic unit tests of print_table().""" # pylint: disable=invalid-name,import-error import io import unittest import unittest.mock from foomuuri import print_table @unittest.mock.patch('sys.stdout', new_callable=io.StringIO) class TestPrintTable(unittest.TestCase): """Test print_table().""" def setUp(self): """Fill in common fixture values.""" self.data_multiple_rows = [ ( 'row 1 column 1', 'row 1 colum 2', 'row 1 colu 3', ), ( 'row 2 colu 1', 'row 2 colum 2', 'row 2 column 3', ), ] self.data_no_rows = [] self.header = ( 'Header', 'Long Elaborate Header', 'Medium Header', ) def test_invalid_align_values(self, _): """Test invalid align values.""" invalid_align = [ '', # no alignment 'x', # invalid align value '<<', # too few values for 3 columns '<<<<', # too much values for 3 columns ] for align in invalid_align: with self.subTest(align): self.assertRaises( ValueError, print_table, self.data_multiple_rows, align=align, ) def test_print_table_all_args(self, stdout: io.StringIO): """Test table with all supported arguments.""" print_table( data=[self.header] + self.data_multiple_rows, header=True, align='<>^', sep=' ', ) self.assertEqual( stdout.getvalue(), """Header Long Elaborate Header Medium Header -------------- --------------------- -------------- row 1 column 1 row 1 colum 2 row 1 colu 3 row 2 colu 1 row 2 colum 2 row 2 column 3 """, ) def test_print_table_with_rows_no_headers(self, stdout: io.StringIO): """Test table without headers, rows only.""" print_table(data=self.data_multiple_rows) self.assertEqual( stdout.getvalue(), """row 1 column 1 row 1 colum 2 row 1 colu 3 row 2 colu 1 row 2 colum 2 row 2 column 3 """, ) def test_print_table_no_rows(self, stdout: io.StringIO): """Test table without headers and rows.""" print_table(data=self.data_no_rows) self.assertEqual( stdout.getvalue(), '', ) def test_print_table_with_headers_no_rows(self, stdout: io.StringIO): """Test table with headers, without rows.""" print_table(data=[self.header] + self.data_no_rows, header=True) self.assertEqual( stdout.getvalue(), '', ) def test_print_table_default_align(self, stdout: io.StringIO): """Test table with headers, and default (left) align.""" print_table(data=[self.header] + self.data_multiple_rows, header=True) self.assertEqual( stdout.getvalue(), """Header Long Elaborate Header Medium Header -------------- --------------------- -------------- row 1 column 1 row 1 colum 2 row 1 colu 3 row 2 colu 1 row 2 colum 2 row 2 column 3 """, ) foomuuri-0.34/src/tests/test_read_config.py000066400000000000000000000411711526436212200211160ustar00rootroot00000000000000"""Basic unit tests of read_config().""" # pylint: disable=invalid-name,import-error,too-many-public-methods # ruff: file-ignore[too-many-public-methods] import contextlib import unittest import unittest.mock from foomuuri import read_config class TestReadConfig(unittest.TestCase): """Basic unit tests of read_config().""" @staticmethod @contextlib.contextmanager def mock_config(config_text: str): """Return context manager of foomuuri.find_config_files() patch. See test_config_foomuuri.py for detailed explanation. """ config_file = unittest.mock.MagicMock() config_file.read_text.return_value = config_text with unittest.mock.patch( 'foomuuri.find_config_files', side_effect=[[], [config_file]] ): yield config_file def test_empty_section(self): """Test section without content.""" with self.mock_config(""" iplist { } foomuuri { } """) as config_file: config = read_config(require_etc_config=False) self.assertDictEqual( config, { '_section_line': { 'foomuuri': f'File {config_file} line 4: ', 'iplist': f'File {config_file} line 2: ', }, 'foomuuri': [], 'iplist': [], }, ) def test_non_empty_section(self): """Test section with content.""" with self.mock_config(""" iplist { url_timeout 1d } foomuuri { priority_offset 42 } """) as config_file: config = read_config(require_etc_config=False) self.assertDictEqual( config, { '_section_line': { 'foomuuri': f'File {config_file} line 5: ', 'iplist': f'File {config_file} line 2: ', }, 'iplist': [ (f'File {config_file} line 3: ', ['url_timeout', '1d']) ], 'foomuuri': [ ( f'File {config_file} line 5: ', ['priority_offset', '42'], ) ], }, ) def test_section_line_continuation(self): """Test line continuation within section.""" with self.mock_config(""" iplist { url_timeout \\ 1d } foomuuri { priority_offset \\ 42 } """) as config_file: config = read_config(require_etc_config=False) self.assertDictEqual( config, { '_section_line': { 'foomuuri': f'File {config_file} line 7: ', 'iplist': f'File {config_file} line 2: ', }, 'foomuuri': [ ( f'File {config_file} line 7: ', ['priority_offset', '42'], ) ], 'iplist': [ (f'File {config_file} line 4: ', ['url_timeout', '1d']) ], }, ) def test_last_line_continuation(self): """Test line continuation at config last line.""" with ( self.mock_config(""" iplist { } \\""") as config_file, unittest.mock.patch( 'foomuuri.fail', side_effect=SystemExit ) as fail, ): self.assertRaises( SystemExit, read_config, require_etc_config=False ) fail.assert_called_once_with( f'File {config_file}: Continuation "\\" at end of file' ) def test_content_outside_section(self): """Test content outside section.""" with ( self.mock_config(""" iplist { } url_timeout 1d """) as config_file, unittest.mock.patch( 'foomuuri.fail', side_effect=SystemExit ) as fail, ): self.assertRaises( SystemExit, read_config, require_etc_config=False ) fail.assert_called_once_with( f'File {config_file} line 4: Unknown line: url_timeout 1d' ) def test_content_outside_inline_section(self): """Test content outside inline section.""" with ( self.mock_config(""" iplist { } url_timeout 1d """) as config_file, unittest.mock.patch( 'foomuuri.fail', side_effect=SystemExit ) as fail, ): self.assertRaises( SystemExit, read_config, require_etc_config=False ) fail.assert_called_once_with( f'File {config_file} line 2: ' 'Closing brace must be last: iplist { } url_timeout 1d' ) def test_section_with_parameters(self): """Test section with parameters.""" with self.mock_config(""" prerouting filter mangle - 10 { } template template_name { } """) as config_file: config = read_config(require_etc_config=False) self.assertDictEqual( config, { '_section_line': { 'prerouting filter mangle - 10': f'File {config_file} ' 'line 2: ', 'template template_name': f'File {config_file} ' 'line 4: ', }, 'prerouting filter mangle - 10': [], 'template template_name': [], }, ) def test_section_with_single_word_parameter(self): """Test section accepting single word parameter only.""" with ( self.mock_config(""" template param1 param2 { } """) as config_file, unittest.mock.patch( 'foomuuri.fail', side_effect=SystemExit ) as fail, ): self.assertRaises( SystemExit, read_config, require_etc_config=False ) fail.assert_called_once_with( f'File {config_file} line 2: Section "template" ' 'must have single word name: template param1 param2 {' ) def test_parameter_for_parameterless_section(self): """Test passing parameter to section not supposed to have one.""" with ( self.mock_config(""" iplist parameter { } """) as config_file, unittest.mock.patch( 'foomuuri.fail', side_effect=SystemExit ) as fail, ): self.assertRaises( SystemExit, read_config, require_etc_config=False ) fail.assert_called_once_with( f'File {config_file} line 2: Section "iplist" does not ' 'take parameters: iplist parameter {' ) def test_section_missing_closing_brace(self): """Test section without closing brace.""" with ( self.mock_config(""" iplist { url_timeout 1d """) as config_file, unittest.mock.patch( 'foomuuri.fail', side_effect=SystemExit ) as fail, ): self.assertRaises( SystemExit, read_config, require_etc_config=False ) fail.assert_called_once_with( f'File {config_file}: Section "iplist" is missing ' '"}" at end of file' ) def test_inline_section_missing_closing_brace(self): """Test inline_section without closing brace.""" with ( self.mock_config(""" iplist { url_timeout 1d """) as config_file, unittest.mock.patch( 'foomuuri.fail', side_effect=SystemExit ) as fail, ): self.assertRaises( SystemExit, read_config, require_etc_config=False ) fail.assert_called_once_with( f'File {config_file} line 2: ' 'Closing brace missing: iplist { url_timeout 1d' ) def test_stray_closing_brace(self): """Test stray closing brace.""" with ( self.mock_config(""" iplist { } } """) as config_file, unittest.mock.patch( 'foomuuri.fail', side_effect=SystemExit ) as fail, ): self.assertRaises( SystemExit, read_config, require_etc_config=False ) fail.assert_called_once_with( f'File {config_file} line 4: Extra "}}"' ) def test_inline_section_extra_closing_brace(self): """Test inline section with extra closing brace.""" with ( self.mock_config(""" foomuuri { bar } } """) as config_file, unittest.mock.patch( 'foomuuri.fail', side_effect=SystemExit ) as fail, ): self.assertRaises( SystemExit, read_config, require_etc_config=False ) fail.assert_called_once_with( f'File {config_file} line 2: ' 'One closing brace allowed: foomuuri { bar } }' ) def test_inline_section_extra_opening_braces(self): """Test extra opening braces for inline section.""" with ( self.mock_config(""" foomuuri { bar } iplist { """) as config_file, unittest.mock.patch( 'foomuuri.fail', side_effect=SystemExit ) as fail, ): self.assertRaises( SystemExit, read_config, require_etc_config=False ) fail.assert_called_once_with( f'File {config_file} line 2: ' 'One opening brace allowed: foomuuri { bar } iplist {' ) def test_section_nesting(self): """Test section nesting.""" with ( self.mock_config(""" iplist { template { } } """) as config_file, unittest.mock.patch( 'foomuuri.fail', side_effect=SystemExit ) as fail, ): self.assertRaises( SystemExit, read_config, require_etc_config=False ) fail.assert_called_once_with( f'File {config_file} line 3: New "template {{" ' 'while section "iplist" is still open' ) def test_section_missing_opening_brace(self): """Test section without opening brace.""" with ( self.mock_config(""" iplist url_timeout 1d } """) as config_file, unittest.mock.patch( 'foomuuri.fail', side_effect=SystemExit ) as fail, ): self.assertRaises( SystemExit, read_config, require_etc_config=False ) fail.assert_called_once_with( f'File {config_file} line 2: Unknown line: iplist' ) def test_comments(self): """Test comments inside and outside section.""" with self.mock_config(""" # comment outside iplist { # comment on section start # comment inside section url_timeout 1d # comment section content } # comment on section close foomuuri { priority_offset 42 } # comment inline section """) as config_file: config = read_config(require_etc_config=False) self.assertDictEqual( config, { '_section_line': { 'foomuuri': f'File {config_file} line 8: ', 'iplist': f'File {config_file} line 3: ', }, 'foomuuri': [ ( f'File {config_file} line 8: ', ['priority_offset', '42'], ) ], 'iplist': [ (f'File {config_file} line 5: ', ['url_timeout', '1d']) ], }, ) def test_protected_section(self): """Test that section starting with _ is rejected.""" with ( self.mock_config(""" _section { } """) as config_file, unittest.mock.patch( 'foomuuri.fail', side_effect=SystemExit ) as fail, ): self.assertRaises( SystemExit, read_config, require_etc_config=False ) fail.assert_called_once_with( f'File {config_file} line 2: Unknown section: _section' ) def test_empty_config(self): """Test empty config file.""" with self.mock_config(''): config = read_config(require_etc_config=False) self.assertDictEqual( config, {'_section_line': {}}, ) def test_config_reading_stop_word(self): """Test that config reading stops on '# foomuuri: not-conf' marker.""" with self.mock_config(""" iplist { url_timeout 1d } # foomuuri: not-conf foomuuri { priority_offset 42 } """) as config_file: config = read_config(require_etc_config=False) self.assertDictEqual( config, { '_section_line': { 'iplist': f'File {config_file} line 2: ' }, 'iplist': [ (f'File {config_file} line 3: ', ['url_timeout', '1d']) ], }, ) def test_tokenizer_delimiters_and_glued_comments(self): """Test all supported tokenizer delimiters and glued comments.""" with self.mock_config(""" macro{#comment macro1 http;https;;smtp#comment macro2 ftp log "message ; 1"#comment macro3 ntp log 'message { 2 }'#comment }#comment foomuuri{priority_offset 42}#comment """) as config_file: config = read_config(require_etc_config=False) self.assertDictEqual( config, { '_section_line': { 'macro': f'File {config_file} line 2: ', 'foomuuri': f'File {config_file} line 7: ', }, 'macro': [ ( f'File {config_file} line 3: ', ['macro1', 'http', ';', 'https', ';', ';', 'smtp'], ), ( f'File {config_file} line 4: ', ['macro2', 'ftp', 'log', '"message ; 1"'], ), ( f'File {config_file} line 5: ', ['macro3', 'ntp', 'log', "'message { 2 }'"], ), ], 'foomuuri': [ ( f'File {config_file} line 7: ', ['priority_offset', '42'], ) ], }, ) def test_tokenizer_unbalanced_quotes_fail(self): """Test tokenizer failure on unbalanced quotes.""" for style, config in [ ('double', 'section { "content }'), ('single', "section { 'content }"), ]: with ( self.subTest(quote_style=style), self.mock_config(config) as config_file, unittest.mock.patch( 'foomuuri.fail', side_effect=SystemExit ) as fail, ): self.assertRaises( SystemExit, read_config, require_etc_config=False ) fail.assert_called_once_with( f"File {config_file} line 1: Can't parse line: " 'No closing quotation' ) foomuuri-0.34/src/tests/test_typed_config.py000066400000000000000000000241511526436212200213270ustar00rootroot00000000000000"""Basic unit tests of TypedConfig.""" # pylint: disable=import-error import pathlib import typing import unittest from dataclasses import dataclass, field from foomuuri import TypedConfig class TestTypedConfig(unittest.TestCase): """Setters / getters / type conversion tests.""" def set(self, name, value): """Set attribute and return value helper.""" self.config[name] = value return self.config[name] def set_from_str(self, name, value): """Set attribute from string and return value helper.""" self.config.set_from_str(name, value) return self.config[name] def append_from_str(self, name, value): """Append attribute value from string and return value helper.""" self.config.append_from_str(name, value) return self.config[name] def setUp(self): """Set up test data.""" @dataclass class TestConfig(TypedConfig): """Test class.""" # pylint: disable=too-many-instance-attributes initialized_str: str = 'init_value1' union_str_or_none: typing.Union[str, None] = None uninitialized_str: str = field(init=False) initialized_untyped = 'init_value2' # ruff: ignore[implicit-class-var-in-dataclass] type_conversion_float: float = 0.0 type_conversion_int: int = 0 type_conversion_list: list = field(default_factory=list) type_conversion_posixpath: pathlib.PosixPath = field(init=False) type_conversion_set: set = field(default_factory=set) parametrized_list: list[str] = field(default_factory=list) parametrized_dict: dict[str, int] = field(default_factory=dict) parametrized_union: typing.Union[list[str], int] = field( default_factory=list ) validation: str = field( init=False, metadata={'validate': lambda v: v} ) conversion: list = field( init=False, metadata={'convert_str': lambda v: v.split()} ) self.config = TestConfig() def test_accessor_style(self): """Test index and dot access and assignment.""" self.config.initialized_str = 'new_value1' self.assertEqual(self.config.initialized_str, 'new_value1') self.assertRaises( AttributeError, lambda: self.config.unknown_attribute ) self.config['initialized_str'] = 'new_value2' self.assertEqual(self.config['initialized_str'], 'new_value2') self.assertRaises( AttributeError, lambda: self.config['unknown_attribute'] ) self.assertRaises(AttributeError, self.set, 'unknown_attribute', '123') def test_parameterized_types(self): """Test assigning parametrized types attributes.""" self.assertListEqual( self.set('parametrized_list', ['value']), ['value'] ) self.assertDictEqual( self.set('parametrized_dict', {'key': 1}), {'key': 1} ) with self.assertRaises(TypeError): self.set('parametrized_list', {}) with self.assertRaises(TypeError): self.set('parametrized_dict', []) def test_parameterized_union_types(self): """Test assigning to Union attributes with parametrized args.""" self.assertEqual(self.set('parametrized_union', ['value']), ['value']) self.assertEqual(self.set('parametrized_union', 1), 1) with self.assertRaises(TypeError): self.set('parametrized_union', {'key': 1}) def test_initialized_typed(self): """Test assignment to initialized typed attributes.""" self.assertRaises(TypeError, self.set, 'initialized_str', 123) self.assertEqual(self.set('initialized_str', 'value'), 'value') def test_uninitialized_typed(self): """Test assignment to uninitialized typed attributes.""" self.assertRaises( AttributeError, lambda: self.config.uninitialized_str ) self.assertRaises(TypeError, self.set, 'uninitialized_str', 123) self.assertEqual(self.set('uninitialized_str', 'value'), 'value') def test_initialized_untyped(self): """Test access to initialized untyped attribute. Untyped attributes can be read, but cannot be written to. Gating attribute reads via dataclass fields membership is expensive and is not needed anyway. """ self.assertEqual(self.config.initialized_untyped, 'init_value2') with self.assertRaises(AttributeError): self.config.initialized_untyped = 'new_value' def test_union_type(self): """Test assignment to Union attributes.""" self.config.union_str_or_none = 'value' self.assertEqual(self.config.union_str_or_none, 'value') self.config.union_str_or_none = None self.assertIsNone(self.config.union_str_or_none) with self.assertRaises(TypeError): self.config.union_str_or_none = 1 # ty: ignore[invalid-assignment] def test_set_from_str(self): """Test attribute type conversion from strings.""" # Invalid value type: only str supported self.assertRaises( TypeError, self.config.set_from_str, 'type_conversion_int', 123 ) # Supported conversion, from str to int self.assertEqual(self.set_from_str('type_conversion_int', '123'), 123) # Supported conversion, from str to pathlib.PosixPath self.assertEqual( self.set_from_str('type_conversion_posixpath', '/path/'), pathlib.PosixPath('/path/'), ) # Supported conversion, from str to list self.assertEqual( self.set_from_str('type_conversion_list', '1 2 2 3 4'), ['1', '2', '2', '3', '4'], ) # Supported conversion, from str to set self.assertEqual( self.set_from_str('type_conversion_set', '1 2 2 3 4'), {'1', '2', '3', '4'}, ) # Supported conversion, from str to parametrized list self.assertEqual( self.set_from_str('parametrized_list', '1 2'), ['1', '2'], ) # Unsupported conversion, from str to parametrized dict self.assertRaises( TypeError, self.config.set_from_str, 'parametrized_dict', 'key' ) # Assignment, str to str self.assertEqual(self.set_from_str('initialized_str', '123'), '123') # Assignment, list converted from str self.assertEqual(self.set_from_str('conversion', '1'), ['1']) # Unsupported conversion, from str to float self.assertRaises( TypeError, self.config.set_from_str, 'type_conversion_float', '42.0', ) # Assign non-existing attribute self.assertRaises( AttributeError, self.config.set_from_str, 'unknownattribute', '123' ) def test_append_from_str(self): """Test attribute type conversion and appending from strings.""" # Invalid value type: only str supported self.assertRaises( TypeError, self.config.append_from_str, 'initialized_str', 123 ) # Appending, str to str self.assertEqual(self.set_from_str('initialized_str', '1'), '1') self.assertEqual(self.append_from_str('initialized_str', '2'), '1 2') # Appending, list converted from str to list self.assertEqual(self.set_from_str('conversion', '1'), ['1']) self.assertEqual(self.append_from_str('conversion', '2'), ['1', '2']) # Appending, str to list self.assertEqual( self.set_from_str('type_conversion_list', '1 2'), ['1', '2'], ) self.assertEqual( self.append_from_str('type_conversion_list', '2 3'), ['1', '2', '2', '3'], ) # Appending, str to set self.assertEqual( self.set_from_str('type_conversion_set', '1 2'), {'1', '2'}, ) self.assertEqual( self.append_from_str('type_conversion_set', '2 3'), {'1', '2', '3'}, ) # Appending, str to parametrized list self.assertEqual( self.set_from_str('parametrized_list', '1 2'), ['1', '2'], ) self.assertEqual( self.append_from_str('parametrized_list', '2 3'), ['1', '2', '2', '3'], ) # Unsupported append, from str to parametrized dict self.assertRaises( TypeError, self.config.append_from_str, 'parametrized_dict', 'key', ) # Unsupported append, from str to int self.assertRaises( TypeError, self.append_from_str, 'type_conversion_int', '1' ) # Append non-existing attribute self.assertRaises( AttributeError, self.append_from_str, 'unknownattribute', '123' ) def test_validation(self): """Test attribute metadata validation.""" self.assertRaises(ValueError, self.set, 'validation', '') self.assertEqual(self.set('validation', 'test'), 'test') def test_iter(self): """Test dataclass field names iteration.""" self.assertEqual( sorted(self.config), [ 'conversion', 'initialized_str', 'parametrized_dict', 'parametrized_list', 'parametrized_union', 'type_conversion_float', 'type_conversion_int', 'type_conversion_list', 'type_conversion_posixpath', 'type_conversion_set', 'uninitialized_str', 'union_str_or_none', 'validation', ], ) def test_contains(self): """Test dataclass field name membership.""" self.assertIn('initialized_str', self.config) self.assertNotIn('unknown_attribute', self.config) def test_get(self): """Test get() attribute lookup.""" self.assertEqual(self.config.get('initialized_str'), 'init_value1') self.assertEqual(self.config.get('unknown_attribute'), None) foomuuri-0.34/src/tests/test_validators.py000066400000000000000000000047101526436212200210240ustar00rootroot00000000000000"""Basic unit tests of Validators.""" # pylint: disable=import-error import unittest from foomuuri import Validators class TestValidators(unittest.TestCase): """Test assert helpers.""" def assert_valid_invalid(self, test, valid_cases, invalid_cases): """Run test against valid and invalid cases.""" for case in valid_cases: with self.subTest(case): self.assertTrue(test(case), f'Expected {case!r} to be valid') for case in invalid_cases: with self.subTest(case): self.assertFalse( test(case), f'Expected {case!r} to be invalid' ) def test_str_zone_name(self): """Test for str is zone name.""" invalid = ['', 'word word', '3word', 'w#ord', '_./_'] valid = ['wo_123_rd', 'w./_'] self.assert_valid_invalid(Validators.str_zone_name, valid, invalid) def test_str_iplist_name(self): """Test for str is iplist name.""" invalid = ['', '@', '@w w', '@kääk', '@3word'] valid = ['@word', '@_', '@_./-'] self.assert_valid_invalid(Validators.str_iplist_name, valid, invalid) def test_str_interface_name(self): """Test for str is linux interface name.""" invalid = [ '', 'word word', 'word\nword', 'word/word', 'word"word', 'word\\word', 'wordwordwordword', ] valid = ['eth0', ':', '@', 'kääk'] self.assert_valid_invalid( Validators.str_interface_name, valid, invalid ) def test_has_elements(self): """Test for list or set is not empty.""" invalid = [[], {}] valid = [['a'], ['a', 'b'], {'a'}, {'a', 'b'}] self.assert_valid_invalid(Validators.has_elements, valid, invalid) def test_int_positive_or_zero(self): """Test for int is not negative.""" invalid = [-1] valid = [0, 1] self.assert_valid_invalid( Validators.int_positive_or_zero, valid, invalid ) def test_int_positive(self): """Test for int is positive.""" invalid = [-1, 0] valid = [1] self.assert_valid_invalid(Validators.int_positive, valid, invalid) def test_str_yes_no(self): """Test for str is 'yes' or 'no'.""" invalid = ['', 'maybe', 'YES'] valid = ['yes', 'no'] self.assert_valid_invalid(Validators.str_yes_no, valid, invalid) foomuuri-0.34/systemd/000077500000000000000000000000001526436212200150005ustar00rootroot00000000000000foomuuri-0.34/systemd/fi.foobar.Foomuuri1.conf000066400000000000000000000012721526436212200214030ustar00rootroot00000000000000 foomuuri-0.34/systemd/fi.foobar.Foomuuri1.policy000066400000000000000000000020001526436212200217430ustar00rootroot00000000000000 Foomuuri https://github.com/FoobarOy/foomuuri Foomuuri information System policy prevents getting Foomuuri information yes yes yes Foomuuri configuration System policy prevents changing Foomuuri configuration auth_admin_keep auth_admin_keep auth_admin_keep foomuuri-0.34/systemd/foomuuri-boot.service000066400000000000000000000005571526436212200211770ustar00rootroot00000000000000[Unit] Description=Multizone bidirectional nftables firewall - Early boot Documentation=https://foomuuri.foobar.fi/latest/ After=local-fs.target ConditionPathExists=/var/lib/foomuuri/good.fw [Service] Type=oneshot RemainAfterExit=yes ExecStart=/bin/sh -c 'cat /var/lib/foomuuri/good.fw | nft -f -' # Above "sh 'cat | nft'" is a SELinux workaround ProtectSystem=full foomuuri-0.34/systemd/foomuuri-dbus.service000066400000000000000000000006001526436212200211560ustar00rootroot00000000000000[Unit] Description=Multizone bidirectional nftables firewall - D-Bus handler Documentation=https://foomuuri.foobar.fi/latest/ After=dbus.service After=polkit.service After=foomuuri.service Requires=foomuuri.service PartOf=foomuuri.service Conflicts=firewalld.service BindsTo=dbus.service [Service] Type=notify ExecStart=foomuuri dbus ExecReload=kill -HUP $MAINPID ProtectSystem=full foomuuri-0.34/systemd/foomuuri-iplist.service000066400000000000000000000003551526436212200215340ustar00rootroot00000000000000[Unit] Description=Multizone bidirectional nftables firewall - Refresh iplist entries Documentation=https://foomuuri.foobar.fi/latest/ [Service] Type=notify ExecStart=foomuuri --soft iplist refresh TimeoutStartSec=2m ProtectSystem=full foomuuri-0.34/systemd/foomuuri-iplist.timer000066400000000000000000000003471526436212200212150ustar00rootroot00000000000000[Unit] Description=Multizone bidirectional nftables firewall - Refresh iplist entries Documentation=https://foomuuri.foobar.fi/latest/ Requires=foomuuri.service PartOf=foomuuri.service [Timer] OnActiveSec=3m OnUnitInactiveSec=15m foomuuri-0.34/systemd/foomuuri-monitor.service000066400000000000000000000004631526436212200217170ustar00rootroot00000000000000[Unit] Description=Multizone bidirectional nftables firewall - Connectivity monitor Documentation=https://foomuuri.foobar.fi/latest/ After=network-online.target After=foomuuri.service PartOf=foomuuri.service [Service] Type=notify ExecStart=foomuuri monitor ExecReload=kill -HUP $MAINPID ProtectSystem=full foomuuri-0.34/systemd/foomuuri.service000066400000000000000000000007661526436212200202400ustar00rootroot00000000000000[Unit] Description=Multizone bidirectional nftables firewall Documentation=https://foomuuri.foobar.fi/latest/ After=local-fs.target After=foomuuri-boot.service Before=network-pre.target Wants=network-pre.target Wants=foomuuri-boot.service Wants=foomuuri-dbus.service Wants=foomuuri-iplist.timer Wants=foomuuri-monitor.service [Service] Type=oneshot RemainAfterExit=yes ExecStart=foomuuri start ExecReload=foomuuri reload ExecStop=foomuuri stop ProtectSystem=full [Install] WantedBy=multi-user.target foomuuri-0.34/systemd/foomuuri.tmpfilesd000066400000000000000000000000411526436212200205510ustar00rootroot00000000000000d /run/foomuuri 0755 root root - foomuuri-0.34/test/000077500000000000000000000000001526436212200142675ustar00rootroot00000000000000foomuuri-0.34/test/10-host-multizone/000077500000000000000000000000001526436212200175065ustar00rootroot00000000000000foomuuri-0.34/test/10-host-multizone/foomuuri.conf000066400000000000000000000013041526436212200222200ustar00rootroot00000000000000zone { localhost public home } public-localhost { # Incoming traffic in a cafe dhcp-client dhcpv6-client ping saddr_rate "5/second burst 20" ssh saddr_rate "5/minute burst 5" drop log } home-localhost { # Incoming traffic in safe location dhcp-client dhcpv6-client lsdp mdns ping ssdp ssh drop log } template outgoing_services { # Common outgoing traffic dhcp-server dhcpv6-server domain http https imap ntp ping smtp ssh } localhost-public { # Outgoing traffic in a cafe template outgoing_services reject log } localhost-home { # Outgoing traffic in safe location template outgoing_services googlemeet ipp mdns ssdp reject log } foomuuri-0.34/test/10-host-multizone/golden.txt000066400000000000000000000344631526436212200215310ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } set _rate_set_1_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_1_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_2_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_2_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_3_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_3_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { icmp type echo-request update @_rate_set_1_4 { ip saddr limit rate 5/second burst 20 packets } accept icmp type echo-request drop jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type broadcast udp dport 68 accept ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop udp dport 68 accept tcp dport 22 update @_rate_set_3_4 { ip saddr limit rate 5/minute burst 5 packets } accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { icmpv6 type echo-request update @_rate_set_2_6 { ip6 saddr limit rate 5/second burst 20 packets } accept icmpv6 type echo-request drop jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop ip6 daddr fe80::/10 udp sport 547 udp dport 546 accept tcp dport 22 update @_rate_set_3_6 { ip6 saddr limit rate 5/minute burst 5 packets } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain home-localhost { meta nfproto vmap { ipv4 : goto home-localhost_4, ipv6 : goto home-localhost_6 } } chain home-localhost_4 { icmp type echo-request accept jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type multicast ip daddr 224.0.0.251 udp dport 5353 accept fib daddr type multicast ip daddr 224.0.0.251 ip protocol igmp accept fib daddr type multicast ip daddr 239.255.255.250 udp dport 1900 accept fib daddr type multicast ip daddr 239.255.255.250 ip protocol igmp accept fib daddr type broadcast udp dport { 11430, 68 } accept fib daddr type { broadcast, multicast } drop udp dport 68 accept udp sport { 1900, 5353 } accept tcp dport 22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "home-localhost DROP " level info flags skuid drop } chain home-localhost_6 { icmpv6 type echo-request accept jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast ip6 daddr ff02::fb udp dport 5353 accept fib daddr type multicast ip6 daddr ff02::c udp dport 1900 accept fib daddr type multicast drop udp sport { 1900, 5353 } accept tcp dport 22 accept ip6 daddr fe80::/10 udp sport 547 udp dport 546 accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "home-localhost DROP " level info flags skuid drop } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { icmp type echo-request accept jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } udp dport { 123, 443, 53, 67 } accept tcp dport { 143, 22, 25, 443, 53, 80 } accept ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { icmpv6 type echo-request accept jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } udp dport { 123, 443, 53 } accept tcp dport { 143, 22, 25, 443, 53, 80 } accept ip6 daddr ff02::1:2 udp sport 546 udp dport 547 accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-home { meta nfproto vmap { ipv4 : goto localhost-home_4, ipv6 : goto localhost-home_6 } } chain localhost-home_4 { icmp type echo-request accept jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } udp dport { 123, 19302-19309, 3478, 443, 53, 67 } accept udp sport { 1900, 5353 } accept tcp dport { 143, 22, 25, 443, 53, 631, 80 } accept ip protocol igmp ip daddr 224.0.0.22 accept ip daddr 224.0.0.251 udp dport 5353 accept ip daddr 224.0.0.251 ip protocol igmp accept ip daddr 239.255.255.250 udp dport 1900 accept ip daddr 239.255.255.250 ip protocol igmp accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-home REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-home_6 { icmpv6 type echo-request accept jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } udp dport { 123, 19302-19309, 3478, 443, 53 } accept udp sport { 1900, 5353 } accept tcp dport { 143, 22, 25, 443, 53, 631, 80 } accept ip6 daddr ff02::1:2 udp sport 546 udp dport 547 accept ip6 daddr ff02::fb udp dport 5353 accept ip6 daddr ff02::c udp dport 1900 accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-home REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-home { meta nfproto vmap { ipv4 : goto public-home_4, ipv6 : goto public-home_6 } } chain public-home_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-home DROP " level info flags skuid drop } chain public-home_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-home DROP " level info flags skuid drop } chain home-public { meta nfproto vmap { ipv4 : goto home-public_4, ipv6 : goto home-public_6 } } chain home-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "home-public DROP " level info flags skuid drop } chain home-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "home-public DROP " level info flags skuid drop } chain home-home { meta nfproto vmap { ipv4 : goto home-home_4, ipv6 : goto home-home_6 } } chain home-home_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "home-home DROP " level info flags skuid drop } chain home-home_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "home-home DROP " level info flags skuid drop } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/10-host/000077500000000000000000000000001526436212200154625ustar00rootroot00000000000000foomuuri-0.34/test/10-host/foomuuri.conf000066400000000000000000000004001526436212200201700ustar00rootroot00000000000000zone { localhost public * # All network interfaces belong to zone "public" } public-localhost { # Allow specified incoming traffic dhcp-client dhcpv6-client ping ssh drop log } localhost-public { # Allow all outgoing traffic accept } foomuuri-0.34/test/10-host/golden.txt000066400000000000000000000167401526436212200175030ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones jump public-localhost update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones jump localhost-public ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones iifname vmap @forward_catchall_input_zones oifname vmap @forward_catchall_output_zones jump public-public update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } map forward_catchall_input_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_catchall_output_zones { type ifname : verdict elements = { "lo" : accept, } } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { icmp type echo-request accept jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type broadcast udp dport 68 accept ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop udp dport 68 accept tcp dport 22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { icmpv6 type echo-request accept jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop tcp dport 22 accept ip6 daddr fe80::/10 udp sport 547 udp dport 546 accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept accept } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/10-multi-isp/000077500000000000000000000000001526436212200164305ustar00rootroot00000000000000foomuuri-0.34/test/10-multi-isp/foomuuri.conf000066400000000000000000000062141526436212200211470ustar00rootroot00000000000000# Basic configuration: zone { # Define zones with interfaces localhost public enp1s0 enp2s0 internal enp8s0 } foomuuri { # Reverse path filtering must be disabled for public interfaces rpfilter -enp1s0 -enp2s0 } snat { # Masquerade outgoing traffic from internal to public. Both ISPs must be # masqueraded separately. saddr 10.0.0.0/8 oifname enp1s0 masquerade saddr 10.0.0.0/8 oifname enp2s0 masquerade } # Multi-ISP magic is here, using marks to select which ISP to use. Order # of the rules is important. Specific rules should be first, generic last. prerouting { # Accept if mark is already set (not zero). Existing mark will be used. mark_match -0x0000/0xff00 # == Incoming traffic == # Mark traffic from enp1s0 as 0x100 (ISP1) and enp2s0 as 0x200 (ISP2). # This is needed for correctly routing reply packets. iifname enp1s0 mark_set 0x100/0xff00 iifname enp2s0 mark_set 0x200/0xff00 # == Outgoing traffic == # Specific rules should be added first. For example, uncomment next line to # route all SSH traffic from internal to public via ISP2. #iifname enp8s0 ssh mark_set 0x200/0xff00 # Similarly, some source IPs can always be routed via ISP1. #saddr 10.0.1.0/24 mark_set 0x100/0xff00 # For active-active configuration use following line. It uses random number # generator to mark traffic with 0x100 or 0x200. This routes 60% (0-5) # of outgoing traffic to ISP1 and 40% (6-9) to ISP2. nft "meta mark set numgen random mod 10 map { 0-5: 0x100, 6-9: 0x200 } ct mark set meta mark accept" # For active-passive configuration uncomment next line and add comment to # above nft-line. It simply assigns mark 0x100 (ISP1) to all traffic and # uses ISP2 only as fallback. #mark_set 0x100/0xff00 } hook { # Setup "ip rule" and "ip route" rules when Foomuuri starts or stops. # See below for example "multi-isp" file. post_start /etc/foomuuri/multi-isp start post_stop /etc/foomuuri/multi-isp stop } # foomuuri-monitor config: target isp1 { # Monitor ISP1 connectivity by pinging 8.8.4.4. Ideally this would be # some ISP1's router's IP address. command fping --iface enp1s0 8.8.4.4 command_up /etc/foomuuri/multi-isp up 1 command_down /etc/foomuuri/multi-isp down 1 } target isp2 { # Monitor ISP2 connectivity by pinging their router 172.25.31.149. command fping --iface enp2s0 172.25.31.149 command_up /etc/foomuuri/multi-isp up 2 command_down /etc/foomuuri/multi-isp down 2 } # Normal zone-zone rules, copied from router firewall example configuration: public-localhost { ping saddr_rate "5/second burst 20" ssh saddr_rate "5/minute burst 5" drop log } internal-localhost { dhcp-server dhcpv6-server domain domain-s ntp ping ssh reject log } template outgoing_services { # Shared list of services for localhost-public and internal-public. domain domain-s http https ntp ping smtp ssh } localhost-public { template outgoing_services reject log } internal-public { template outgoing_services googlemeet imap reject log } public-internal { drop log } localhost-internal { dhcp-client dhcpv6-client ping ssh reject log } foomuuri-0.34/test/10-multi-isp/golden.txt000066400000000000000000000362761526436212200204570ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } set _rate_set_1_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_1_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_2_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_2_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_3_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_3_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } map input_zones { type ifname : verdict elements = { "lo" : accept, "enp1s0" : jump public-localhost, "enp2s0" : jump public-localhost, "enp8s0" : jump internal-localhost, } } map output_zones { type ifname : verdict elements = { "lo" : accept, "enp1s0" : jump localhost-public, "enp2s0" : jump localhost-public, "enp8s0" : jump localhost-internal, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, "enp1s0" . "enp1s0" : jump public-public, "enp1s0" . "enp2s0" : jump public-public, "enp2s0" . "enp1s0" : jump public-public, "enp2s0" . "enp2s0" : jump public-public, "enp1s0" . "enp8s0" : jump public-internal, "enp2s0" . "enp8s0" : jump public-internal, "enp8s0" . "enp1s0" : jump internal-public, "enp8s0" . "enp2s0" : jump internal-public, "enp8s0" . "enp8s0" : jump internal-internal, } } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { icmp type echo-request update @_rate_set_1_4 { ip saddr limit rate 5/second burst 20 packets } accept icmp type echo-request drop jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop tcp dport 22 update @_rate_set_3_4 { ip saddr limit rate 5/minute burst 5 packets } accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { icmpv6 type echo-request update @_rate_set_2_6 { ip6 saddr limit rate 5/second burst 20 packets } accept icmpv6 type echo-request drop jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop tcp dport 22 update @_rate_set_3_6 { ip6 saddr limit rate 5/minute burst 5 packets } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain internal-localhost { meta nfproto vmap { ipv4 : goto internal-localhost_4, ipv6 : goto internal-localhost_6 } } chain internal-localhost_4 { icmp type echo-request accept jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type broadcast udp dport 67 accept ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop udp dport { 123, 53, 67, 853 } accept tcp dport { 22, 53, 853 } accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "internal-localhost REJECT " level info flags skuid reject with icmpx admin-prohibited } chain internal-localhost_6 { icmpv6 type echo-request accept jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast ip6 daddr ff02::1:2 udp sport 546 udp dport 547 accept fib daddr type multicast drop udp dport { 123, 53, 853 } accept tcp dport { 22, 53, 853 } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "internal-localhost REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { icmp type echo-request accept jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } udp dport { 123, 443, 53, 853 } accept tcp dport { 22, 25, 443, 53, 80, 853 } accept ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { icmpv6 type echo-request accept jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } udp dport { 123, 443, 53, 853 } accept tcp dport { 22, 25, 443, 53, 80, 853 } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain internal-public { meta nfproto vmap { ipv4 : goto internal-public_4, ipv6 : goto internal-public_6 } } chain internal-public_4 { icmp type echo-request accept jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop udp dport { 123, 19302-19309, 3478, 443, 53, 853 } accept tcp dport { 143, 22, 25, 443, 53, 80, 853 } accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "internal-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain internal-public_6 { icmpv6 type echo-request accept jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop udp dport { 123, 19302-19309, 3478, 443, 53, 853 } accept tcp dport { 143, 22, 25, 443, 53, 80, 853 } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "internal-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain public-internal { meta nfproto vmap { ipv4 : goto public-internal_4, ipv6 : goto public-internal_6 } } chain public-internal_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-internal DROP " level info flags skuid drop } chain public-internal_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-internal DROP " level info flags skuid drop } chain localhost-internal { meta nfproto vmap { ipv4 : goto localhost-internal_4, ipv6 : goto localhost-internal_6 } } chain localhost-internal_4 { icmp type echo-request accept jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } udp dport 68 accept tcp dport 22 accept ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-internal REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-internal_6 { icmpv6 type echo-request accept jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } tcp dport 22 accept ip6 daddr fe80::/10 udp sport 547 udp dport 546 accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-internal REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain internal-internal { meta nfproto vmap { ipv4 : goto internal-internal_4, ipv6 : goto internal-internal_6 } } chain internal-internal_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "internal-internal DROP " level info flags skuid drop } chain internal-internal_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "internal-internal DROP " level info flags skuid drop } chain nat_postrouting_srcnat { type nat hook postrouting priority srcnat + 5 oifname "enp1s0" ip saddr 10.0.0.0/8 masquerade oifname "enp2s0" ip saddr 10.0.0.0/8 masquerade } chain filter_prerouting_mangle { type filter hook prerouting priority mangle + 5 meta mark set ct mark meta mark & 0xff00 != 0x0000 accept iifname "enp1s0" meta mark set meta mark & 0xffff00ff | 0x100 ct mark set meta mark accept iifname "enp2s0" meta mark set meta mark & 0xffff00ff | 0x200 ct mark set meta mark accept meta mark set numgen random mod 10 map { 0-5: 0x100, 6-9: 0x200 } ct mark set meta mark accept } chain route_output_mangle { type route hook output priority mangle + 5 meta mark set ct mark } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 iifname != { "enp1s0", "enp2s0" } fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/20-router/000077500000000000000000000000001526436212200160265ustar00rootroot00000000000000foomuuri-0.34/test/20-router/foomuuri.conf000066400000000000000000000062741526436212200205530ustar00rootroot00000000000000zone { localhost public eth0 internal eth1 dmz eth2 } snat { # Masquerade traffic from internal to public. saddr 10.0.0.0/8 oifname eth0 masquerade } dnat { # DNAT incoming SMTP and HTTPS traffic from public to dmz server. This # section is needed only if dmz server doesn't have public IP address. iifname eth0 smtp dnat 10.1.0.2 # public -> dmz iifname eth0 http dnat 10.1.0.2 # public -> dmz iifname eth0 https dnat 10.1.0.2 # public -> dmz iifname eth0 tcp 111 112 113 dnat 10.1.0.2 # set of ports iifname eth1 daddr 192.0.2.32 smtp dnat 10.1.0.2 # internal -> dmz iifname eth1 daddr 192.0.2.32 http dnat 10.1.0.2 # internal -> dmz iifname eth1 daddr 192.0.2.32 https dnat 10.1.0.2 # internal -> dmz } macro { # Define rate limits as macros as same limits are used in public-localhost # and in public-dmz. http_rate saddr_rate "100/second burst 400" saddr_rate_name http_limit mail_rate saddr_rate "1/second burst 10" ping_rate saddr_rate "5/second burst 20" ssh_rate saddr_rate "5/minute burst 5" } template localhost_services { # Shared list of services running on localhost. It runs DHCP server and DNS # resolver for internal and dmz networks, plus basic SSH etc. rules. dhcp-server dhcpv6-server domain domain-s ntp ping ssh } public-localhost { # Allow only ping and SSH from internet to localhost. ping ping_rate ssh ssh_rate drop log } internal-localhost { # Servers on internal network can access localhost's basic services. template localhost_services reject log } dmz-localhost { # Servers on dmz can access localhost's basic services, similar to # internal-localhost. template localhost_services reject log } template public_services { # Shared list of services that run on internet. domain domain-s http https ntp ping ssh } localhost-public { # Basic services from localhost to internet: DNS queries, HTTPS, SSH, etc. template public_services reject log } internal-public { # Basic services from internal to internet: DNS queries, HTTPS, SSH, etc. template public_services googlemeet reject log } dmz-public { # Basic services from dmz to internet, plus SMTP for email transfer.. template public_services smtp reject log } public-internal { # No traffic is allowed from internet to internal network. drop log } localhost-internal { # DHCP server reply packets dhcp-client dhcpv6-client # Very limited access from localhost to internal network. ping ssh reject log } dmz-internal { # Servers on dmz don't need any access to internal network. reject log } template dmz_services { # Shared list of services that run on dmz. http https ping smtp ssh } localhost-dmz { # DHCP server reply packets dhcp-client dhcpv6-client # localhost can access dmz server services. template dmz_services reject log } public-dmz { # Allow traffic from internet to dmz server with rate limits. http http_rate https http_rate smtp mail_rate ping ping_rate ssh ssh_rate drop log } internal-dmz { # Laptops and workstations in internal network can access dmz server # services, plus IMAP for reading email. template dmz_services imap reject log } foomuuri-0.34/test/20-router/golden.txt000066400000000000000000000606541526436212200200520ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } set _rate_set_1_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_1_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_2_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_2_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_3_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_3_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } set http_limit_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set http_limit_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_4_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_4_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_5_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_5_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_6_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_6_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_7_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_7_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } map input_zones { type ifname : verdict elements = { "lo" : accept, "eth0" : jump public-localhost, "eth1" : jump internal-localhost, "eth2" : jump dmz-localhost, } } map output_zones { type ifname : verdict elements = { "lo" : accept, "eth0" : jump localhost-public, "eth1" : jump localhost-internal, "eth2" : jump localhost-dmz, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, "eth0" . "eth0" : jump public-public, "eth0" . "eth1" : jump public-internal, "eth0" . "eth2" : jump public-dmz, "eth1" . "eth0" : jump internal-public, "eth1" . "eth1" : jump internal-internal, "eth1" . "eth2" : jump internal-dmz, "eth2" . "eth0" : jump dmz-public, "eth2" . "eth1" : jump dmz-internal, "eth2" . "eth2" : jump dmz-dmz, } } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { icmp type echo-request update @_rate_set_1_4 { ip saddr limit rate 5/second burst 20 packets } accept icmp type echo-request drop jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop tcp dport 22 update @_rate_set_3_4 { ip saddr limit rate 5/minute burst 5 packets } accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { icmpv6 type echo-request update @_rate_set_2_6 { ip6 saddr limit rate 5/second burst 20 packets } accept icmpv6 type echo-request drop jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop tcp dport 22 update @_rate_set_3_6 { ip6 saddr limit rate 5/minute burst 5 packets } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain internal-localhost { meta nfproto vmap { ipv4 : goto internal-localhost_4, ipv6 : goto internal-localhost_6 } } chain internal-localhost_4 { icmp type echo-request accept jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type broadcast udp dport 67 accept ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop udp dport { 123, 53, 67, 853 } accept tcp dport { 22, 53, 853 } accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "internal-localhost REJECT " level info flags skuid reject with icmpx admin-prohibited } chain internal-localhost_6 { icmpv6 type echo-request accept jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast ip6 daddr ff02::1:2 udp sport 546 udp dport 547 accept fib daddr type multicast drop udp dport { 123, 53, 853 } accept tcp dport { 22, 53, 853 } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "internal-localhost REJECT " level info flags skuid reject with icmpx admin-prohibited } chain dmz-localhost { meta nfproto vmap { ipv4 : goto dmz-localhost_4, ipv6 : goto dmz-localhost_6 } } chain dmz-localhost_4 { icmp type echo-request accept jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type broadcast udp dport 67 accept ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop udp dport { 123, 53, 67, 853 } accept tcp dport { 22, 53, 853 } accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "dmz-localhost REJECT " level info flags skuid reject with icmpx admin-prohibited } chain dmz-localhost_6 { icmpv6 type echo-request accept jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast ip6 daddr ff02::1:2 udp sport 546 udp dport 547 accept fib daddr type multicast drop udp dport { 123, 53, 853 } accept tcp dport { 22, 53, 853 } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "dmz-localhost REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { icmp type echo-request accept jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } udp dport { 123, 443, 53, 853 } accept tcp dport { 22, 443, 53, 80, 853 } accept ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { icmpv6 type echo-request accept jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } udp dport { 123, 443, 53, 853 } accept tcp dport { 22, 443, 53, 80, 853 } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain internal-public { meta nfproto vmap { ipv4 : goto internal-public_4, ipv6 : goto internal-public_6 } } chain internal-public_4 { icmp type echo-request accept jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop udp dport { 123, 19302-19309, 3478, 443, 53, 853 } accept tcp dport { 22, 443, 53, 80, 853 } accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "internal-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain internal-public_6 { icmpv6 type echo-request accept jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop udp dport { 123, 19302-19309, 3478, 443, 53, 853 } accept tcp dport { 22, 443, 53, 80, 853 } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "internal-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain dmz-public { meta nfproto vmap { ipv4 : goto dmz-public_4, ipv6 : goto dmz-public_6 } } chain dmz-public_4 { icmp type echo-request accept jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop udp dport { 123, 443, 53, 853 } accept tcp dport { 22, 25, 443, 53, 80, 853 } accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "dmz-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain dmz-public_6 { icmpv6 type echo-request accept jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop udp dport { 123, 443, 53, 853 } accept tcp dport { 22, 25, 443, 53, 80, 853 } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "dmz-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain public-internal { meta nfproto vmap { ipv4 : goto public-internal_4, ipv6 : goto public-internal_6 } } chain public-internal_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-internal DROP " level info flags skuid drop } chain public-internal_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-internal DROP " level info flags skuid drop } chain localhost-internal { meta nfproto vmap { ipv4 : goto localhost-internal_4, ipv6 : goto localhost-internal_6 } } chain localhost-internal_4 { icmp type echo-request accept jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } udp dport 68 accept tcp dport 22 accept ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-internal REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-internal_6 { icmpv6 type echo-request accept jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } tcp dport 22 accept ip6 daddr fe80::/10 udp sport 547 udp dport 546 accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-internal REJECT " level info flags skuid reject with icmpx admin-prohibited } chain dmz-internal { meta nfproto vmap { ipv4 : goto dmz-internal_4, ipv6 : goto dmz-internal_6 } } chain dmz-internal_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "dmz-internal REJECT " level info flags skuid reject with icmpx admin-prohibited } chain dmz-internal_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "dmz-internal REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-dmz { meta nfproto vmap { ipv4 : goto localhost-dmz_4, ipv6 : goto localhost-dmz_6 } } chain localhost-dmz_4 { icmp type echo-request accept jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } udp dport { 443, 68 } accept tcp dport { 22, 25, 443, 80 } accept ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-dmz REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-dmz_6 { icmpv6 type echo-request accept jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } udp dport 443 accept tcp dport { 22, 25, 443, 80 } accept ip6 daddr fe80::/10 udp sport 547 udp dport 546 accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-dmz REJECT " level info flags skuid reject with icmpx admin-prohibited } chain public-dmz { meta nfproto vmap { ipv4 : goto public-dmz_4, ipv6 : goto public-dmz_6 } } chain public-dmz_4 { icmp type echo-request update @_rate_set_5_4 { ip saddr limit rate 5/second burst 20 packets } accept icmp type echo-request drop jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop tcp dport 80 update @http_limit_4 { ip saddr limit rate 100/second burst 400 packets } accept tcp dport 443 update @http_limit_4 { ip saddr limit rate 100/second burst 400 packets } accept udp dport 443 update @http_limit_4 { ip saddr limit rate 100/second burst 400 packets } accept tcp dport 25 update @_rate_set_4_4 { ip saddr limit rate 1/second burst 10 packets } accept tcp dport 22 update @_rate_set_7_4 { ip saddr limit rate 5/minute burst 5 packets } accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-dmz DROP " level info flags skuid drop } chain public-dmz_6 { icmpv6 type echo-request update @_rate_set_6_6 { ip6 saddr limit rate 5/second burst 20 packets } accept icmpv6 type echo-request drop jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop tcp dport 80 update @http_limit_6 { ip6 saddr limit rate 100/second burst 400 packets } accept tcp dport 443 update @http_limit_6 { ip6 saddr limit rate 100/second burst 400 packets } accept udp dport 443 update @http_limit_6 { ip6 saddr limit rate 100/second burst 400 packets } accept tcp dport 25 update @_rate_set_4_6 { ip6 saddr limit rate 1/second burst 10 packets } accept tcp dport 22 update @_rate_set_7_6 { ip6 saddr limit rate 5/minute burst 5 packets } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-dmz DROP " level info flags skuid drop } chain internal-dmz { meta nfproto vmap { ipv4 : goto internal-dmz_4, ipv6 : goto internal-dmz_6 } } chain internal-dmz_4 { icmp type echo-request accept jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop udp dport 443 accept tcp dport { 143, 22, 25, 443, 80 } accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "internal-dmz REJECT " level info flags skuid reject with icmpx admin-prohibited } chain internal-dmz_6 { icmpv6 type echo-request accept jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop udp dport 443 accept tcp dport { 143, 22, 25, 443, 80 } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "internal-dmz REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain internal-internal { meta nfproto vmap { ipv4 : goto internal-internal_4, ipv6 : goto internal-internal_6 } } chain internal-internal_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "internal-internal DROP " level info flags skuid drop } chain internal-internal_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "internal-internal DROP " level info flags skuid drop } chain dmz-dmz { meta nfproto vmap { ipv4 : goto dmz-dmz_4, ipv6 : goto dmz-dmz_6 } } chain dmz-dmz_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "dmz-dmz DROP " level info flags skuid drop } chain dmz-dmz_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "dmz-dmz DROP " level info flags skuid drop } chain nat_postrouting_srcnat { type nat hook postrouting priority srcnat + 5 oifname "eth0" ip saddr 10.0.0.0/8 masquerade } chain nat_prerouting_dstnat { type nat hook prerouting priority dstnat + 5 iifname "eth0" tcp dport 25 dnat ip to 10.1.0.2 iifname "eth0" tcp dport 80 dnat ip to 10.1.0.2 iifname "eth0" tcp dport 443 dnat ip to 10.1.0.2 iifname "eth0" udp dport 443 dnat ip to 10.1.0.2 iifname "eth0" tcp dport { 111, 112, 113 } dnat ip to 10.1.0.2 iifname "eth1" ip daddr 192.0.2.32 tcp dport 25 dnat ip to 10.1.0.2 iifname "eth1" ip daddr 192.0.2.32 tcp dport 80 dnat ip to 10.1.0.2 iifname "eth1" ip daddr 192.0.2.32 tcp dport 443 dnat ip to 10.1.0.2 iifname "eth1" ip daddr 192.0.2.32 udp dport 443 dnat ip to 10.1.0.2 } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/50-accept/000077500000000000000000000000001526436212200157505ustar00rootroot00000000000000foomuuri-0.34/test/50-accept/foomuuri.conf000066400000000000000000000003251526436212200204640ustar00rootroot00000000000000zone { localhost a b c } a-b { # not added drop log } a-c { # both added accept } b-a { # multicast dropped, broadcast accepted multicast drop accept } b-c { # empty chain, not added } foomuuri-0.34/test/50-accept/golden.txt000066400000000000000000000411001526436212200177550ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain a-b { meta nfproto vmap { ipv4 : goto a-b_4, ipv6 : goto a-b_6 } } chain a-b_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "a-b DROP " level info flags skuid drop } chain a-b_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "a-b DROP " level info flags skuid drop } chain a-c { meta nfproto vmap { ipv4 : goto a-c_4, ipv6 : goto a-c_6 } } chain a-c_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type multicast accept fib daddr type broadcast accept accept } chain a-c_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast accept fib daddr type multicast drop accept } chain b-a { meta nfproto vmap { ipv4 : goto b-a_4, ipv6 : goto b-a_6 } } chain b-a_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type multicast drop fib daddr type multicast accept fib daddr type broadcast accept accept } chain b-a_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop fib daddr type multicast accept fib daddr type multicast drop accept } chain b-c { meta nfproto vmap { ipv4 : goto b-c_4, ipv6 : goto b-c_6 } } chain b-c_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "b-c DROP " level info flags skuid drop } chain b-c_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "b-c DROP " level info flags skuid drop } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain localhost-a { meta nfproto vmap { ipv4 : goto localhost-a_4, ipv6 : goto localhost-a_6 } } chain localhost-a_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-a REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-a_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-a REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-b { meta nfproto vmap { ipv4 : goto localhost-b_4, ipv6 : goto localhost-b_6 } } chain localhost-b_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-b REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-b_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-b REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-c { meta nfproto vmap { ipv4 : goto localhost-c_4, ipv6 : goto localhost-c_6 } } chain localhost-c_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-c REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-c_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-c REJECT " level info flags skuid reject with icmpx admin-prohibited } chain a-localhost { meta nfproto vmap { ipv4 : goto a-localhost_4, ipv6 : goto a-localhost_6 } } chain a-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "a-localhost DROP " level info flags skuid drop } chain a-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "a-localhost DROP " level info flags skuid drop } chain a-a { meta nfproto vmap { ipv4 : goto a-a_4, ipv6 : goto a-a_6 } } chain a-a_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "a-a DROP " level info flags skuid drop } chain a-a_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "a-a DROP " level info flags skuid drop } chain b-localhost { meta nfproto vmap { ipv4 : goto b-localhost_4, ipv6 : goto b-localhost_6 } } chain b-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "b-localhost DROP " level info flags skuid drop } chain b-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "b-localhost DROP " level info flags skuid drop } chain b-b { meta nfproto vmap { ipv4 : goto b-b_4, ipv6 : goto b-b_6 } } chain b-b_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "b-b DROP " level info flags skuid drop } chain b-b_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "b-b DROP " level info flags skuid drop } chain c-localhost { meta nfproto vmap { ipv4 : goto c-localhost_4, ipv6 : goto c-localhost_6 } } chain c-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "c-localhost DROP " level info flags skuid drop } chain c-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "c-localhost DROP " level info flags skuid drop } chain c-a { meta nfproto vmap { ipv4 : goto c-a_4, ipv6 : goto c-a_6 } } chain c-a_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "c-a DROP " level info flags skuid drop } chain c-a_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "c-a DROP " level info flags skuid drop } chain c-b { meta nfproto vmap { ipv4 : goto c-b_4, ipv6 : goto c-b_6 } } chain c-b_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "c-b DROP " level info flags skuid drop } chain c-b_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "c-b DROP " level info flags skuid drop } chain c-c { meta nfproto vmap { ipv4 : goto c-c_4, ipv6 : goto c-c_6 } } chain c-c_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "c-c DROP " level info flags skuid drop } chain c-c_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "c-c DROP " level info flags skuid drop } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/50-any/000077500000000000000000000000001526436212200153005ustar00rootroot00000000000000foomuuri-0.34/test/50-any/foomuuri.conf000066400000000000000000000004711526436212200200160ustar00rootroot00000000000000zone { localhost public foo bar } any-public { tcp 1000 tcp 1001 szone foo tcp 1002 szone -foo tcp 1003 szone -foo -bar } public-any { tcp 2000 tcp 2001 dzone foo tcp 2002 dzone -foo tcp 2003 dzone -foo -bar } any-any { tcp 3000 tcp 3001 dzone foo tcp 3002 dzone foo szone -bar } foomuuri-0.34/test/50-any/golden.txt000066400000000000000000000447101526436212200173170ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop tcp dport { 2000, 2002, 2003, 3000 } accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop tcp dport { 2000, 2002, 2003, 3000 } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop tcp dport { 1000, 1002, 1003, 2000, 2002, 2003, 3000 } accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop tcp dport { 1000, 1002, 1003, 2000, 2002, 2003, 3000 } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-foo { meta nfproto vmap { ipv4 : goto public-foo_4, ipv6 : goto public-foo_6 } } chain public-foo_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop tcp dport { 2000, 2001, 3000, 3001, 3002 } accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-foo DROP " level info flags skuid drop } chain public-foo_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop tcp dport { 2000, 2001, 3000, 3001, 3002 } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-foo DROP " level info flags skuid drop } chain public-bar { meta nfproto vmap { ipv4 : goto public-bar_4, ipv6 : goto public-bar_6 } } chain public-bar_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop tcp dport { 2000, 2002, 3000 } accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-bar DROP " level info flags skuid drop } chain public-bar_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop tcp dport { 2000, 2002, 3000 } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-bar DROP " level info flags skuid drop } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } tcp dport { 1000, 1002, 1003, 3000 } accept ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } tcp dport { 1000, 1002, 1003, 3000 } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain foo-public { meta nfproto vmap { ipv4 : goto foo-public_4, ipv6 : goto foo-public_6 } } chain foo-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop tcp dport { 1000, 1001, 3000 } accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "foo-public DROP " level info flags skuid drop } chain foo-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop tcp dport { 1000, 1001, 3000 } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "foo-public DROP " level info flags skuid drop } chain bar-public { meta nfproto vmap { ipv4 : goto bar-public_4, ipv6 : goto bar-public_6 } } chain bar-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop tcp dport { 1000, 1002, 3000 } accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "bar-public DROP " level info flags skuid drop } chain bar-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop tcp dport { 1000, 1002, 3000 } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "bar-public DROP " level info flags skuid drop } chain localhost-foo { meta nfproto vmap { ipv4 : goto localhost-foo_4, ipv6 : goto localhost-foo_6 } } chain localhost-foo_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } tcp dport { 3000, 3001, 3002 } accept ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-foo REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-foo_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } tcp dport { 3000, 3001, 3002 } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-foo REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-bar { meta nfproto vmap { ipv4 : goto localhost-bar_4, ipv6 : goto localhost-bar_6 } } chain localhost-bar_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } tcp dport 3000 accept ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-bar REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-bar_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } tcp dport 3000 accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-bar REJECT " level info flags skuid reject with icmpx admin-prohibited } chain foo-localhost { meta nfproto vmap { ipv4 : goto foo-localhost_4, ipv6 : goto foo-localhost_6 } } chain foo-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop tcp dport 3000 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "foo-localhost DROP " level info flags skuid drop } chain foo-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop tcp dport 3000 accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "foo-localhost DROP " level info flags skuid drop } chain foo-foo { meta nfproto vmap { ipv4 : goto foo-foo_4, ipv6 : goto foo-foo_6 } } chain foo-foo_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop tcp dport { 3000, 3001, 3002 } accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "foo-foo DROP " level info flags skuid drop } chain foo-foo_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop tcp dport { 3000, 3001, 3002 } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "foo-foo DROP " level info flags skuid drop } chain foo-bar { meta nfproto vmap { ipv4 : goto foo-bar_4, ipv6 : goto foo-bar_6 } } chain foo-bar_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop tcp dport 3000 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "foo-bar DROP " level info flags skuid drop } chain foo-bar_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop tcp dport 3000 accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "foo-bar DROP " level info flags skuid drop } chain bar-localhost { meta nfproto vmap { ipv4 : goto bar-localhost_4, ipv6 : goto bar-localhost_6 } } chain bar-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop tcp dport 3000 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "bar-localhost DROP " level info flags skuid drop } chain bar-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop tcp dport 3000 accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "bar-localhost DROP " level info flags skuid drop } chain bar-foo { meta nfproto vmap { ipv4 : goto bar-foo_4, ipv6 : goto bar-foo_6 } } chain bar-foo_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop tcp dport { 3000, 3001 } accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "bar-foo DROP " level info flags skuid drop } chain bar-foo_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop tcp dport { 3000, 3001 } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "bar-foo DROP " level info flags skuid drop } chain bar-bar { meta nfproto vmap { ipv4 : goto bar-bar_4, ipv6 : goto bar-bar_6 } } chain bar-bar_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop tcp dport 3000 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "bar-bar DROP " level info flags skuid drop } chain bar-bar_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop tcp dport 3000 accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "bar-bar DROP " level info flags skuid drop } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/50-compare/000077500000000000000000000000001526436212200161375ustar00rootroot00000000000000foomuuri-0.34/test/50-compare/foomuuri.conf000066400000000000000000000005711526436212200206560ustar00rootroot00000000000000zone { localhost public } localhost-public { tcp 1000 tcp 1001 1002 tcp == 1003 1004 tcp dport 1005 1006 tcp dport == 1007 1008 tcp -2000 tcp -2001 -2002 tcp != 2003 2004 tcp dport -2005 -2006 tcp dport != 2007 2008 tcp < 3000 tcp > 3001 tcp <= 3002 tcp >= 3003 tcp dport < 3004 tcp dport > 3005 tcp dport <= 3006 tcp dport >= 3007 } foomuuri-0.34/test/50-compare/golden.txt000066400000000000000000000201561526436212200201540ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } tcp dport { 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008 } accept ip protocol igmp ip daddr 224.0.0.22 accept tcp dport != 2000 accept tcp dport != { 2001, 2002 } accept tcp dport != { 2003, 2004 } accept tcp dport != { 2005, 2006 } accept tcp dport != { 2007, 2008 } accept tcp dport < 3000 accept tcp dport > 3001 accept tcp dport <= 3002 accept tcp dport >= 3003 accept tcp dport < 3004 accept tcp dport > 3005 accept tcp dport <= 3006 accept tcp dport >= 3007 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } tcp dport { 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008 } accept tcp dport != 2000 accept tcp dport != { 2001, 2002 } accept tcp dport != { 2003, 2004 } accept tcp dport != { 2005, 2006 } accept tcp dport != { 2007, 2008 } accept tcp dport < 3000 accept tcp dport > 3001 accept tcp dport <= 3002 accept tcp dport >= 3003 accept tcp dport < 3004 accept tcp dport > 3005 accept tcp dport <= 3006 accept tcp dport >= 3007 accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/50-parser/000077500000000000000000000000001526436212200160055ustar00rootroot00000000000000foomuuri-0.34/test/50-parser/foomuuri.conf000066400000000000000000000003441526436212200205220ustar00rootroot00000000000000zone { localhost public } macro { testmacro \ 1000 \ 1001 } localhost-public { tcp \ testmacro \ drop tcp $(shell cat 50-parser/portlist.txt | tr '\n' ' ') reject $(shell cat 50-parser/rules.txt) } foomuuri-0.34/test/50-parser/golden.txt000066400000000000000000000170341526436212200200230ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept tcp dport { 1000, 1001 } drop tcp dport { 1002, 1003, 1004 } reject with icmpx admin-prohibited tcp dport 2001 accept tcp dport 2002 reject with icmpx admin-prohibited tcp dport 22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public DROP " level info flags skuid drop } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } tcp dport { 1000, 1001 } drop tcp dport { 1002, 1003, 1004 } reject with icmpx admin-prohibited tcp dport 2001 accept tcp dport 2002 reject with icmpx admin-prohibited tcp dport 22 accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public DROP " level info flags skuid drop } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/50-parser/portlist.txt000066400000000000000000000000171526436212200204240ustar00rootroot000000000000001002 1003 1004 foomuuri-0.34/test/50-parser/rules.txt000066400000000000000000000000461526436212200177000ustar00rootroot00000000000000tcp 2001 tcp 2002 reject ssh drop log foomuuri-0.34/test/50-tcp/000077500000000000000000000000001526436212200152775ustar00rootroot00000000000000foomuuri-0.34/test/50-tcp/foomuuri.conf000066400000000000000000000004761526436212200200220ustar00rootroot00000000000000zone { localhost public } localhost-public { tcp # all tcp traffic reject log # final rule ssh # unreached rule, will not be added } public-localhost { udp # all udp traffic drop # final rule accept # unreached rule, will not be added ssh # unreached rule, will not be added } foomuuri-0.34/test/50-tcp/golden.txt000066400000000000000000000160511526436212200173130ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept ip protocol tcp accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } ip6 nexthdr tcp accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop ip protocol udp accept drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop ip6 nexthdr udp accept drop } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/60-cgroup/000077500000000000000000000000001526436212200160115ustar00rootroot00000000000000foomuuri-0.34/test/60-cgroup/foomuuri.conf000066400000000000000000000004541526436212200205300ustar00rootroot00000000000000foomuuri { nft_bin true # nft checks if cgroup exists, skip it } zone { localhost public } localhost-public { cgroup 10 cgroup 11-14 cgroup 15 16 20-30 cgroup -40 cgroup -41 -42 cgroup -43 -50-60 cgroup "user.slice" } public-localhost { cgroup "system.slice/sshd.service" } foomuuri-0.34/test/60-cgroup/golden.txt000066400000000000000000000174421526436212200200320ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept meta cgroup 10 accept meta cgroup 11-14 accept meta cgroup { 15, 16, 20-30 } accept meta cgroup != 40 accept meta cgroup != { 41, 42 } accept meta cgroup != { 43, 50-60 } accept socket cgroupv2 level 1 "user.slice" accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } meta cgroup 10 accept meta cgroup 11-14 accept meta cgroup { 15, 16, 20-30 } accept meta cgroup != 40 accept meta cgroup != { 41, 42 } accept meta cgroup != { 43, 50-60 } accept socket cgroupv2 level 1 "user.slice" accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop socket cgroupv2 level 2 "system.slice/sshd.service" accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop socket cgroupv2 level 2 "system.slice/sshd.service" accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/60-conntrack/000077500000000000000000000000001526436212200164745ustar00rootroot00000000000000foomuuri-0.34/test/60-conntrack/foomuuri.conf000066400000000000000000000021571526436212200212150ustar00rootroot00000000000000zone { localhost public } localhost-public { # pre-ct: log and count traffic, don't accept yet log "outgoing" # plain rule, implicit -conntrack and continue log "outgoing_2" continue -conntrack # same as above rule counter all_out # plain rule, implicit -conntrack and continue counter all_out_2 continue -conntrack # same as above rule tcp sport 22 counter ssh_out continue -conntrack # post-ct: normal rules ssh https counter new_https # post-ct, so counts only new, not established log "post-ct,pre-reject" continue conntrack reject } public-localhost { # pre-ct: log and count traffic, don't accept yet log "incoming" log "incoming_2" continue -conntrack counter all_in counter all_in_2 continue -conntrack tcp dport 22 counter ssh_in continue -conntrack # Generic rule to accept all DNAT'ed traffic ct_status dnat # post-ct: normal rules ssh drop } # Incoming traffic prerouting filter raw { domain notrack tcp sport 53 notrack udp sport 53 notrack } # Locally created traffic output filter raw { domain notrack tcp sport 53 notrack udp sport 53 notrack } foomuuri-0.34/test/60-conntrack/golden.txt000066400000000000000000000222151526436212200205070ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "outgoing " level info flags skuid update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "outgoing_2 " level info flags skuid counter name "all_out" continue counter name "all_out_2" continue tcp sport 22 counter name "ssh_out" continue jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } tcp dport 22 accept ip protocol igmp ip daddr 224.0.0.22 accept tcp dport 443 counter name "new_https" accept udp dport 443 counter name "new_https" accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "post-ct,pre-reject " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "outgoing " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "outgoing_2 " level info flags skuid counter name "all_out" continue counter name "all_out_2" continue tcp sport 22 counter name "ssh_out" continue jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } tcp dport 22 accept tcp dport 443 counter name "new_https" accept udp dport 443 counter name "new_https" accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "post-ct,pre-reject " level info flags skuid reject with icmpx admin-prohibited } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "incoming " level info flags skuid update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "incoming_2 " level info flags skuid counter name "all_in" continue counter name "all_in_2" continue tcp dport 22 counter name "ssh_in" continue jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop ct status dnat accept tcp dport 22 accept drop } chain public-localhost_6 { update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "incoming " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "incoming_2 " level info flags skuid counter name "all_in" continue counter name "all_in_2" continue tcp dport 22 counter name "ssh_in" continue jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop ct status dnat accept tcp dport 22 accept drop } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain filter_prerouting_raw { type filter hook prerouting priority raw tcp dport 53 notrack udp dport 53 notrack tcp sport 53 notrack udp sport 53 notrack } chain filter_output_raw { type filter hook output priority raw tcp dport 53 notrack udp dport 53 notrack tcp sport 53 notrack udp sport 53 notrack } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } counter all_in { } counter all_in_2 { } counter all_out { } counter all_out_2 { } counter new_https { } counter ssh_in { } counter ssh_out { } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/60-queue/000077500000000000000000000000001526436212200156365ustar00rootroot00000000000000foomuuri-0.34/test/60-queue/foomuuri.conf000066400000000000000000000004751526436212200203600ustar00rootroot00000000000000zone { localhost public } forward { # Log all packets using nflog group 3 log "Forward-IPS" log_level "group 3" # Count all packets counter # Forward matching packets only iifname eth0 oifname eth1 queue # Forward all packets to userspace for IPS inspection queue flags fanout,bypass to 3-5 } foomuuri-0.34/test/60-queue/golden.txt000066400000000000000000000166531526436212200176620ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain filter_forward_mangle { type filter hook forward priority mangle + 5 log prefix "Forward-IPS " group 3 continue counter continue iifname "eth0" oifname "eth1" queue queue flags fanout,bypass to 3-5 } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/60-special-chains/000077500000000000000000000000001526436212200173755ustar00rootroot00000000000000foomuuri-0.34/test/60-special-chains/foomuuri.conf000066400000000000000000000015241526436212200221130ustar00rootroot00000000000000zone { localhost public } snat { } snat { saddr 10.0.0.1 accept } snat nat srcnat + 10 { saddr 10.0.0.2 accept } postrouting nat srcnat + 20 { saddr 10.0.0.22 accept } prerouting { saddr 10.0.0.3 accept } prerouting filter raw { saddr 10.0.0.4 accept } prerouting filter -300 { saddr 10.0.0.5 accept } prerouting filter 200 { saddr 10.0.0.6 accept } prerouting filter raw + 20 { saddr 10.0.0.7 accept } prerouting filter raw - 10 { saddr 10.0.0.77 accept } output { saddr 10.0.0.8 accept } output filter raw { saddr 10.0.0.9 accept } rpfilter { saddr 10.0.0.10 accept } dnat { icmp daddr 192.168.1.1 dnat 10.1.1.1 tcp daddr 192.168.1.2 dnat 10.1.1.2 udp daddr 192.168.1.3 dnat 10.1.1.3 daddr 192.168.1.4 dnat 10.1.1.4 daddr 192.0.2.1 ping dnat 172.16.2.1 daddr 2001:db8::1 ping dnat fc00:db8::1 } foomuuri-0.34/test/60-special-chains/golden.txt000066400000000000000000000215331526436212200214120ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { ip saddr 10.0.0.10 accept udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain nat_postrouting_srcnat { type nat hook postrouting priority srcnat + 5 ip saddr 10.0.0.1 accept } chain nat_postrouting_srcnat_10 { type nat hook postrouting priority srcnat + 10 ip saddr 10.0.0.2 accept } chain nat_prerouting_dstnat { type nat hook prerouting priority dstnat + 5 ip daddr 192.168.1.1 ip protocol icmp dnat ip to 10.1.1.1 ip daddr 192.168.1.2 ip protocol tcp dnat ip to 10.1.1.2 ip daddr 192.168.1.3 ip protocol udp dnat ip to 10.1.1.3 ip daddr 192.168.1.4 dnat ip to 10.1.1.4 ip daddr 192.0.2.1 icmp type echo-request dnat ip to 172.16.2.1 ip6 daddr 2001:db8::1 icmpv6 type echo-request dnat ip6 to fc00:db8::1 } chain filter_prerouting_mangle { type filter hook prerouting priority mangle + 5 ip saddr 10.0.0.3 accept } chain filter_prerouting_raw { type filter hook prerouting priority raw ip saddr 10.0.0.4 accept } chain filter_prerouting_-300 { type filter hook prerouting priority -300 ip saddr 10.0.0.5 accept } chain filter_prerouting_200 { type filter hook prerouting priority 200 ip saddr 10.0.0.6 accept } chain filter_prerouting_raw_20 { type filter hook prerouting priority raw + 20 ip saddr 10.0.0.7 accept } chain filter_prerouting_raw_-_10 { type filter hook prerouting priority raw - 10 ip saddr 10.0.0.77 accept } chain nat_postrouting_srcnat_20 { type nat hook postrouting priority srcnat + 20 ip saddr 10.0.0.22 accept } chain route_output_mangle { type route hook output priority mangle + 5 ip saddr 10.0.0.8 accept } chain filter_output_raw { type filter hook output priority raw ip saddr 10.0.0.9 accept } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/60-zone-wilcard/000077500000000000000000000000001526436212200171105ustar00rootroot00000000000000foomuuri-0.34/test/60-zone-wilcard/foomuuri.conf000066400000000000000000000000431526436212200216210ustar00rootroot00000000000000zone { localhost public eth* } foomuuri-0.34/test/60-zone-wilcard/golden.txt000066400000000000000000000165621526436212200211330ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict flags interval elements = { "lo" : accept, "eth*" : jump public-localhost, } } map output_zones { type ifname : verdict flags interval elements = { "lo" : accept, "eth*" : jump localhost-public, } } map forward_zones { type ifname . ifname : verdict flags interval elements = { "lo" . "lo" : accept, "eth*" . "eth*" : jump public-public, } } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/70-ban-o-matic/000077500000000000000000000000001526436212200166025ustar00rootroot00000000000000foomuuri-0.34/test/70-ban-o-matic/foomuuri.conf000066400000000000000000000021471526436212200213220ustar00rootroot00000000000000zone { localhost public } iplist { # List of known good hosts, don't ban these @good 192.168.0.0/24 foobar.fi # List containing banned IP addresses. They will be banned for 5 minutes. @banned dynamic=yes element_timeout=5m } public-localhost { # Drop all new traffic if source IP address is in @banned iplist. # Update/reset ban expire timeout. Add this as first rule. saddr @banned iplist_update saddr @banned drop log + ":banned" # Allow SSH from known good hosts and others with rate limit. # If there are more than 5/minute connections add them to @banned iplist. ssh saddr @good ssh saddr_rate "5/minute burst 5" ssh iplist_update saddr @banned drop log + ":ban-ssh" # ...other normal rules, with or without similar rate limit banning... # Instead of normal final "drop log" rule: # - Allow 2/min dropped connections without banning. # - Somebody is port scanning us. Add IP address to @banned iplist. # - Make sure @good addresses are never banned. saddr @good drop log saddr_rate "2/minute burst 10" drop log iplist_update saddr @banned drop log + ":ban-portscan" } foomuuri-0.34/test/70-ban-o-matic/golden.txt000066400000000000000000000246711526436212200206250ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } set _rate_set_1_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_1_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_2_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_2_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop ip saddr @banned_4 update @banned_4 { ip saddr } jump lograte_1 ip saddr @good_4 tcp dport 22 accept tcp dport 22 update @_rate_set_1_4 { ip saddr limit rate 5/minute burst 5 packets } accept tcp dport 22 update @banned_4 { ip saddr } jump lograte_2 ip saddr @good_4 jump lograte_3 update @_rate_set_2_4 { ip saddr limit rate 2/minute burst 10 packets } jump lograte_4 update @banned_4 { ip saddr } jump lograte_5 update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop ip6 saddr @banned_6 update @banned_6 { ip6 saddr } jump lograte_7 ip6 saddr @good_6 tcp dport 22 accept tcp dport 22 update @_rate_set_1_6 { ip6 saddr limit rate 5/minute burst 5 packets } accept tcp dport 22 update @banned_6 { ip6 saddr } jump lograte_8 ip6 saddr @good_6 jump lograte_9 update @_rate_set_2_6 { ip6 saddr limit rate 2/minute burst 10 packets } jump lograte_10 update @banned_6 { ip6 saddr } jump lograte_11 update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain lograte_1 { update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP:banned " level info flags skuid drop } chain lograte_2 { update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP:ban-ssh " level info flags skuid drop } chain lograte_3 { update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain lograte_4 { update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain lograte_5 { update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP:ban-portscan " level info flags skuid drop } chain lograte_7 { update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP:banned " level info flags skuid drop } chain lograte_8 { update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP:ban-ssh " level info flags skuid drop } chain lograte_9 { update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain lograte_10 { update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain lograte_11 { update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP:ban-portscan " level info flags skuid drop } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } set banned_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 300s } set banned_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 300s } set good_4 { type ipv4_addr flags interval,timeout auto-merge } set good_6 { type ipv6_addr flags interval,timeout auto-merge } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } add element inet foomuuri good_4 { 192.168.0.0/24, } foomuuri-0.34/test/70-counter/000077500000000000000000000000001526436212200161725ustar00rootroot00000000000000foomuuri-0.34/test/70-counter/foomuuri.conf000066400000000000000000000005511526436212200207070ustar00rootroot00000000000000foomuuri { counter dmz-internal any-public } zone { localhost public internal dmz } localhost-public { tcp 1001 tcp 1002 nft "tcp dport 1003" # not added to nft rules } public-public { tcp 2001 tcp 2002 } dmz-internal { tcp 3001 tcp 3002 } internal-dmz { # not added here tcp 4010 } public-dmz { # not added here tcp 4010 } foomuuri-0.34/test/70-counter/golden.txt000066400000000000000000000451461526436212200202150ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept tcp dport 1001 counter accept tcp dport 1002 counter accept tcp dport 1003 counter jump lograte_1 } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } tcp dport 1001 counter accept tcp dport 1002 counter accept tcp dport 1003 counter jump lograte_2 } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop tcp dport 2001 counter accept tcp dport 2002 counter accept counter jump lograte_3 } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop tcp dport 2001 counter accept tcp dport 2002 counter accept counter jump lograte_4 } chain dmz-internal { meta nfproto vmap { ipv4 : goto dmz-internal_4, ipv6 : goto dmz-internal_6 } } chain dmz-internal_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop tcp dport 3001 counter accept tcp dport 3002 counter accept counter jump lograte_5 } chain dmz-internal_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop tcp dport 3001 counter accept tcp dport 3002 counter accept counter jump lograte_6 } chain internal-dmz { meta nfproto vmap { ipv4 : goto internal-dmz_4, ipv6 : goto internal-dmz_6 } } chain internal-dmz_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop tcp dport 4010 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "internal-dmz DROP " level info flags skuid drop } chain internal-dmz_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop tcp dport 4010 accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "internal-dmz DROP " level info flags skuid drop } chain public-dmz { meta nfproto vmap { ipv4 : goto public-dmz_4, ipv6 : goto public-dmz_6 } } chain public-dmz_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop tcp dport 4010 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-dmz DROP " level info flags skuid drop } chain public-dmz_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop tcp dport 4010 accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-dmz DROP " level info flags skuid drop } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain localhost-internal { meta nfproto vmap { ipv4 : goto localhost-internal_4, ipv6 : goto localhost-internal_6 } } chain localhost-internal_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-internal REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-internal_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-internal REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-dmz { meta nfproto vmap { ipv4 : goto localhost-dmz_4, ipv6 : goto localhost-dmz_6 } } chain localhost-dmz_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-dmz REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-dmz_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-dmz REJECT " level info flags skuid reject with icmpx admin-prohibited } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-internal { meta nfproto vmap { ipv4 : goto public-internal_4, ipv6 : goto public-internal_6 } } chain public-internal_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-internal DROP " level info flags skuid drop } chain public-internal_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-internal DROP " level info flags skuid drop } chain internal-localhost { meta nfproto vmap { ipv4 : goto internal-localhost_4, ipv6 : goto internal-localhost_6 } } chain internal-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "internal-localhost DROP " level info flags skuid drop } chain internal-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "internal-localhost DROP " level info flags skuid drop } chain internal-public { meta nfproto vmap { ipv4 : goto internal-public_4, ipv6 : goto internal-public_6 } } chain internal-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop counter jump lograte_21 } chain internal-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop counter jump lograte_22 } chain internal-internal { meta nfproto vmap { ipv4 : goto internal-internal_4, ipv6 : goto internal-internal_6 } } chain internal-internal_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "internal-internal DROP " level info flags skuid drop } chain internal-internal_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "internal-internal DROP " level info flags skuid drop } chain dmz-localhost { meta nfproto vmap { ipv4 : goto dmz-localhost_4, ipv6 : goto dmz-localhost_6 } } chain dmz-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "dmz-localhost DROP " level info flags skuid drop } chain dmz-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "dmz-localhost DROP " level info flags skuid drop } chain dmz-public { meta nfproto vmap { ipv4 : goto dmz-public_4, ipv6 : goto dmz-public_6 } } chain dmz-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop counter jump lograte_27 } chain dmz-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop counter jump lograte_28 } chain dmz-dmz { meta nfproto vmap { ipv4 : goto dmz-dmz_4, ipv6 : goto dmz-dmz_6 } } chain dmz-dmz_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "dmz-dmz DROP " level info flags skuid drop } chain dmz-dmz_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "dmz-dmz DROP " level info flags skuid drop } chain lograte_1 { update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain lograte_2 { update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain lograte_3 { update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain lograte_4 { update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain lograte_5 { update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "dmz-internal DROP " level info flags skuid drop } chain lograte_6 { update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "dmz-internal DROP " level info flags skuid drop } chain lograte_21 { update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "internal-public DROP " level info flags skuid drop } chain lograte_22 { update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "internal-public DROP " level info flags skuid drop } chain lograte_27 { update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "dmz-public DROP " level info flags skuid drop } chain lograte_28 { update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "dmz-public DROP " level info flags skuid drop } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/70-dscp/000077500000000000000000000000001526436212200154445ustar00rootroot00000000000000foomuuri-0.34/test/70-dscp/foomuuri.conf000066400000000000000000000004331526436212200201600ustar00rootroot00000000000000zone { localhost public } localhost-public { dscp 1 saddr 10.0.0.1 dscp 2 saddr ff00::2 dscp 3 dscp 0x38 drop dscp -0x20 log reject ssh dscp cs1 dscp -cs1 -cs2 dscp cs0 cs1 cs2 cs3 cs4 cs5 cs6 cs7 af11 af12 af13 af21 af22 af23 af31 af32 af33 af41 af42 af43 ef } foomuuri-0.34/test/70-dscp/golden.txt000066400000000000000000000203161526436212200174570ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept ip dscp 1 accept ip saddr 10.0.0.1 ip dscp 2 accept ip dscp 0x38 drop ip dscp != 0x20 jump lograte_1 tcp dport 22 ip dscp cs1 accept ip dscp != { cs1, cs2 } accept ip dscp { af11, af12, af13, af21, af22, af23, af31, af32, af33, af41, af42, af43, cs0, cs1, cs2, cs3, cs4, cs5, cs6, cs7, ef } accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } ip6 dscp 1 accept ip6 saddr ff00::2 ip6 dscp 3 accept ip6 dscp 0x38 drop ip6 dscp != 0x20 jump lograte_3 tcp dport 22 ip6 dscp cs1 accept ip6 dscp != { cs1, cs2 } accept ip6 dscp { af11, af12, af13, af21, af22, af23, af31, af32, af33, af41, af42, af43, cs0, cs1, cs2, cs3, cs4, cs5, cs6, cs7, ef } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain lograte_1 { update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain lograte_3 { update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/70-flowtable/000077500000000000000000000000001526436212200164725ustar00rootroot00000000000000foomuuri-0.34/test/70-flowtable/foomuuri.conf000066400000000000000000000001551526436212200212070ustar00rootroot00000000000000foomuuri { flowtable eth0 eth1 hw_offload=yes } zone { localhost internal eth0 public eth1 } foomuuri-0.34/test/70-flowtable/golden.txt000066400000000000000000000301511526436212200205030ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } flowtable fastpath { hook ingress priority filter + 5 devices = { "eth0", "eth1" } flags offload } chain forward { type filter hook forward priority filter + 5 flow add @fastpath iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, "eth0" : jump internal-localhost, "eth1" : jump public-localhost, } } map output_zones { type ifname : verdict elements = { "lo" : accept, "eth0" : jump localhost-internal, "eth1" : jump localhost-public, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, "eth0" . "eth0" : jump internal-internal, "eth0" . "eth1" : jump internal-public, "eth1" . "eth0" : jump public-internal, "eth1" . "eth1" : jump public-public, } } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain localhost-internal { meta nfproto vmap { ipv4 : goto localhost-internal_4, ipv6 : goto localhost-internal_6 } } chain localhost-internal_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-internal REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-internal_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-internal REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain internal-localhost { meta nfproto vmap { ipv4 : goto internal-localhost_4, ipv6 : goto internal-localhost_6 } } chain internal-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "internal-localhost DROP " level info flags skuid drop } chain internal-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "internal-localhost DROP " level info flags skuid drop } chain internal-internal { meta nfproto vmap { ipv4 : goto internal-internal_4, ipv6 : goto internal-internal_6 } } chain internal-internal_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "internal-internal DROP " level info flags skuid drop } chain internal-internal_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "internal-internal DROP " level info flags skuid drop } chain internal-public { meta nfproto vmap { ipv4 : goto internal-public_4, ipv6 : goto internal-public_6 } } chain internal-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "internal-public DROP " level info flags skuid drop } chain internal-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "internal-public DROP " level info flags skuid drop } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-internal { meta nfproto vmap { ipv4 : goto public-internal_4, ipv6 : goto public-internal_6 } } chain public-internal_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-internal DROP " level info flags skuid drop } chain public-internal_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-internal DROP " level info flags skuid drop } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/70-helper/000077500000000000000000000000001526436212200157725ustar00rootroot00000000000000foomuuri-0.34/test/70-helper/foomuuri.conf000066400000000000000000000001261526436212200205050ustar00rootroot00000000000000zone { localhost public } localhost-public { ftp } public-localhost { ftp } foomuuri-0.34/test/70-helper/golden.txt000066400000000000000000000170521526436212200200100ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } tcp dport 21 accept ip protocol igmp ip daddr 224.0.0.22 accept ct helper "ftp" accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } tcp dport 21 accept ct helper "ftp" accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop tcp dport 21 accept ct helper "ftp" accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop tcp dport 21 accept ct helper "ftp" accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } ct helper ftp-21 { type "ftp" protocol tcp } chain helper { type filter hook prerouting priority filter + 5 tcp dport 21 ct helper set "ftp-21" } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/70-ifname-lo/000077500000000000000000000000001526436212200163625ustar00rootroot00000000000000foomuuri-0.34/test/70-ifname-lo/foomuuri.conf000066400000000000000000000002361526436212200210770ustar00rootroot00000000000000zone { localhost public } prerouting { tcp 1000 iifname lo drop tcp 1001 oifname lo drop tcp 1002 iifname eth2 drop tcp 1003 oifname eth3 drop } foomuuri-0.34/test/70-ifname-lo/golden.txt000066400000000000000000000166711526436212200204060ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain filter_prerouting_mangle { type filter hook prerouting priority mangle + 5 iif 0 tcp dport 1000 drop fib daddr type local tcp dport 1001 drop iifname "eth2" tcp dport 1002 drop oifname "eth3" tcp dport 1003 drop } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/70-iplist/000077500000000000000000000000001526436212200160175ustar00rootroot00000000000000foomuuri-0.34/test/70-iplist/Makefile000066400000000000000000000010551526436212200174600ustar00rootroot00000000000000.PHONY: all all: rm -f iplist-cache.json $(RUN) ../../src/foomuuri --set=etc_dir=. --set=share_dir=../../etc --set=state_dir=. --set=run_dir=. --set=nft_bin=true iplist refresh : # Normalize timestamps : # Remove localhost's IPv6 as not all systems have it : # Remove tailing comma in localhost's IPv4 after removing IPv6 entry jq --sort-keys . iplist-cache.json | \ sed -E 's/[1-3][0-9]{9}/1800000000/; /"::1"/d; /"127.0.0.1"/ s/,//' > iplist-cache.json.2 mv iplist-cache.json.2 iplist-cache.json diff -u golden-cache-json.txt iplist-cache.json foomuuri-0.34/test/70-iplist/foomuuri.conf000066400000000000000000000010121526436212200205250ustar00rootroot00000000000000zone { localhost public } iplist { timeout 1h 2m # backward compabiliy (v0.27) url_refresh=1d2h3m @empty @empty_no_merge merge=no @one 10.0.0.1 @two 10.0.0.1 ff00::1 /non/existing/file dns_refresh=3h missing_ok=yes @three 10.0.0.4 10.0.0.5 ff00::1 /non/existing/file dns_refresh=3h merge=no missing_ok=yes @start 10.0.0.6 start=no @file ./iplist.txt url_timeout=1000d # More than 2 years is never @empty_file ./iplist_empty.txt missing_ok=yes @dns localhost dns_timeout=1000d } foomuuri-0.34/test/70-iplist/golden-cache-json.txt000066400000000000000000000006061526436212200220420ustar00rootroot00000000000000{ "./iplist.txt": { "ip": { "10.1.0.1": 4000000000, "10.1.0.2": 4000000000, "10.1.0.3": 4000000000, "ff00::10": 4000000000, "ff00::20": 4000000000 }, "refresh": 1800000000 }, "./iplist_empty.txt": { "ip": {}, "refresh": 1800000000 }, "localhost": { "ip": { "127.0.0.1": 4000000000 }, "refresh": 1800000000 } } foomuuri-0.34/test/70-iplist/golden.txt000066400000000000000000000216071526436212200200360ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } set dns_4 { type ipv4_addr flags interval,timeout auto-merge } set dns_6 { type ipv6_addr flags interval,timeout auto-merge } set empty_4 { type ipv4_addr flags interval,timeout auto-merge } set empty_6 { type ipv6_addr flags interval,timeout auto-merge } set empty_file_4 { type ipv4_addr flags interval,timeout auto-merge } set empty_file_6 { type ipv6_addr flags interval,timeout auto-merge } set empty_no_merge_4 { type ipv4_addr flags interval,timeout } set empty_no_merge_6 { type ipv6_addr flags interval,timeout } set file_4 { type ipv4_addr flags interval,timeout auto-merge } set file_6 { type ipv6_addr flags interval,timeout auto-merge } set one_4 { type ipv4_addr flags interval,timeout auto-merge } set one_6 { type ipv6_addr flags interval,timeout auto-merge } set start_4 { type ipv4_addr flags interval,timeout auto-merge } set start_6 { type ipv6_addr flags interval,timeout auto-merge } set three_4 { type ipv4_addr flags interval,timeout } set three_6 { type ipv6_addr flags interval,timeout } set two_4 { type ipv4_addr flags interval,timeout auto-merge } set two_6 { type ipv6_addr flags interval,timeout auto-merge } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } add element inet foomuuri dns_4 { 127.0.0.1, } add element inet foomuuri file_4 { 10.1.0.1, 10.1.0.2, 10.1.0.3, } add element inet foomuuri file_6 { ff00::10, ff00::20, } add element inet foomuuri one_4 { 10.0.0.1, } add element inet foomuuri three_4 { 10.0.0.4, 10.0.0.5, } add element inet foomuuri three_6 { ff00::1, } add element inet foomuuri two_4 { 10.0.0.1, } add element inet foomuuri two_6 { ff00::1, } foomuuri-0.34/test/70-iplist/iplist.txt000066400000000000000000000001031526436212200200560ustar00rootroot0000000000000010.1.0.1 10.1.0.2 10.1.0.3 # comment to ignore ff00::10 ff00::20 foomuuri-0.34/test/70-iplist/iplist_empty.txt000066400000000000000000000000001526436212200212700ustar00rootroot00000000000000foomuuri-0.34/test/70-ipsec/000077500000000000000000000000001526436212200156165ustar00rootroot00000000000000foomuuri-0.34/test/70-ipsec/foomuuri.conf000066400000000000000000000002241526436212200203300ustar00rootroot00000000000000zone { localhost public } localhost-public { tcp 1000 dipsec tcp 1001 -dipsec } public-localhost { tcp 2000 sipsec tcp 2001 -sipsec } foomuuri-0.34/test/70-ipsec/golden.txt000066400000000000000000000170341526436212200176340ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept rt ipsec exists tcp dport 1000 accept rt ipsec missing tcp dport 1001 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } rt ipsec exists tcp dport 1000 accept rt ipsec missing tcp dport 1001 accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop meta ipsec exists tcp dport 2000 accept meta ipsec missing tcp dport 2001 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop meta ipsec exists tcp dport 2000 accept meta ipsec missing tcp dport 2001 accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/70-length/000077500000000000000000000000001526436212200157745ustar00rootroot00000000000000foomuuri-0.34/test/70-length/foomuuri.conf000066400000000000000000000002221526436212200205040ustar00rootroot00000000000000zone { localhost public } localhost-public { ping length < 1000 } public-localhost { ping length < 100 saddr_rate "5/second burst 20" } foomuuri-0.34/test/70-length/golden.txt000066400000000000000000000200251526436212200200040ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } set _rate_set_1_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_1_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_2_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_2_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { icmp type echo-request ip length < 1000 accept icmp type echo-request drop jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { icmpv6 type echo-request ip6 length < 1000 accept icmpv6 type echo-request drop jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { icmp type echo-request ip length < 100 update @_rate_set_1_4 { ip saddr limit rate 5/second burst 20 packets } accept icmp type echo-request drop jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { icmpv6 type echo-request ip6 length < 100 update @_rate_set_2_6 { ip6 saddr limit rate 5/second burst 20 packets } accept icmpv6 type echo-request drop jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/70-log/000077500000000000000000000000001526436212200152745ustar00rootroot00000000000000foomuuri-0.34/test/70-log/foomuuri.conf000066400000000000000000000003531526436212200200110ustar00rootroot00000000000000zone { localhost public } localhost-public { tcp 1001 log tcp 1002 log "abc def" tcp 1003 log + ":tag" tcp 1004 log "$(szone) => $(dzone): $(statement)" tcp 1005 log "new level" log_level "level crit flags ip options" } foomuuri-0.34/test/70-log/golden.txt000066400000000000000000000221751526436212200173140ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept tcp dport 1001 jump lograte_1 tcp dport 1002 jump lograte_2 tcp dport 1003 jump lograte_3 tcp dport 1004 jump lograte_4 tcp dport 1005 jump lograte_5 update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } tcp dport 1001 jump lograte_7 tcp dport 1002 jump lograte_8 tcp dport 1003 jump lograte_9 tcp dport 1004 jump lograte_10 tcp dport 1005 jump lograte_11 update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain lograte_1 { update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public ACCEPT " level info flags skuid accept } chain lograte_2 { update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "abc def " level info flags skuid accept } chain lograte_3 { update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public ACCEPT:tag " level info flags skuid accept } chain lograte_4 { update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost => public: ACCEPT " level info flags skuid accept } chain lograte_5 { update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "new level " level crit flags ip options accept } chain lograte_7 { update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public ACCEPT " level info flags skuid accept } chain lograte_8 { update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "abc def " level info flags skuid accept } chain lograte_9 { update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public ACCEPT:tag " level info flags skuid accept } chain lograte_10 { update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost => public: ACCEPT " level info flags skuid accept } chain lograte_11 { update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "new level " level crit flags ip options accept } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/70-mac/000077500000000000000000000000001526436212200152535ustar00rootroot00000000000000foomuuri-0.34/test/70-mac/foomuuri.conf000066400000000000000000000002021526436212200177610ustar00rootroot00000000000000zone { localhost public } localhost-public { mac_saddr 11:22:33:44:55:66 mac_daddr de:ad:be:af:00:00 de:ad:be:af:00:01 } foomuuri-0.34/test/70-mac/golden.txt000066400000000000000000000166321526436212200172740ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept ether saddr 11:22:33:44:55:66 accept ether daddr { de:ad:be:af:00:00, de:ad:be:af:00:01 } accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } ether saddr 11:22:33:44:55:66 accept ether daddr { de:ad:be:af:00:00, de:ad:be:af:00:01 } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/70-macro/000077500000000000000000000000001526436212200156145ustar00rootroot00000000000000foomuuri-0.34/test/70-macro/foomuuri.conf000066400000000000000000000020121526436212200203230ustar00rootroot00000000000000zone { localhost public } macro { single 10.1.1.1 multiple 10.1.2.2 10.1.3.3 10.1.4.4 netmask 10.1.2.0 10.1.3.0 10.1.4.0 single6 ffe0::1 netmask6 ffe0:1:: ffe0:2:: overwrite foo overwrite 10.3.3.1 # will print warning append + 10.2.2.1 append + 10.2.2.2 append + 10.2.2.3 x_1 tcp; udp x_2 10001; 10002 x_3 x_1 x_2 drop x_4 x_3 uid 1; x_3 uid 2 y_1 tcp 10101; tcp 10102 y_2 uid root y_1 gid 0 y_3 tcp 10201 y_2 tcp 10202 z_1 x_1 z_2 10301 10302 z_3 z_1 z_2 } dnat { iifname eth4 tcp 123 dnat single:44 iifname eth6 tcp 123 dnat [single6]:66 } localhost-public { tcp 1000 saddr single tcp 1001 saddr -single tcp 1002 saddr multiple tcp 1003 saddr -multiple tcp 1004 saddr netmask/24 tcp 1005 saddr -netmask/24 tcp 1006 saddr single6 tcp 1007 saddr -single6 tcp 1008 saddr netmask6/64 tcp 1009 saddr overwrite tcp 1010 saddr append x_4 y_3 z_3 reject } foomuuri-0.34/test/70-macro/golden.txt000066400000000000000000000221611526436212200176270ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept ip saddr 10.1.1.1 tcp dport 1000 accept ip saddr != 10.1.1.1 tcp dport 1001 accept ip saddr { 10.1.2.2, 10.1.3.3, 10.1.4.4 } tcp dport 1002 accept ip saddr != { 10.1.2.2, 10.1.3.3, 10.1.4.4 } tcp dport 1003 accept ip saddr { 10.1.2.0/24, 10.1.3.0/24, 10.1.4.0/24 } tcp dport 1004 accept ip saddr != { 10.1.2.0/24, 10.1.3.0/24, 10.1.4.0/24 } tcp dport 1005 accept ip saddr 10.3.3.1 tcp dport 1009 accept ip saddr { 10.2.2.1, 10.2.2.2, 10.2.2.3 } tcp dport 1010 accept tcp dport 10001 meta skuid 1 drop tcp dport 10002 meta skuid 1 drop udp dport 10001 meta skuid 1 drop udp dport 10002 meta skuid 1 drop tcp dport 10001 meta skuid 2 drop tcp dport 10002 meta skuid 2 drop udp dport 10001 meta skuid 2 drop udp dport 10002 meta skuid 2 drop tcp dport { 10101, 10201, 10202 } meta skuid "root" meta skgid 0 accept tcp dport { 10102, 10201, 10202 } meta skuid "root" meta skgid 0 accept tcp dport { 10301, 10302 } reject with icmpx admin-prohibited udp dport { 10301, 10302 } reject with icmpx admin-prohibited update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } ip6 saddr ffe0::1 tcp dport 1006 accept ip6 saddr != ffe0::1 tcp dport 1007 accept ip6 saddr { ffe0:1::/64, ffe0:2::/64 } tcp dport 1008 accept tcp dport 10001 meta skuid 1 drop tcp dport 10002 meta skuid 1 drop udp dport 10001 meta skuid 1 drop udp dport 10002 meta skuid 1 drop tcp dport 10001 meta skuid 2 drop tcp dport 10002 meta skuid 2 drop udp dport 10001 meta skuid 2 drop udp dport 10002 meta skuid 2 drop tcp dport { 10101, 10201, 10202 } meta skuid "root" meta skgid 0 accept tcp dport { 10102, 10201, 10202 } meta skuid "root" meta skgid 0 accept tcp dport { 10301, 10302 } reject with icmpx admin-prohibited udp dport { 10301, 10302 } reject with icmpx admin-prohibited update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain nat_prerouting_dstnat { type nat hook prerouting priority dstnat + 5 iifname "eth4" tcp dport 123 dnat ip to 10.1.1.1:44 iifname "eth6" tcp dport 123 dnat ip6 to [ffe0::1]:66 } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/70-mark/000077500000000000000000000000001526436212200154455ustar00rootroot00000000000000foomuuri-0.34/test/70-mark/foomuuri.conf000066400000000000000000000001601526436212200201560ustar00rootroot00000000000000zone { localhost public } forward { iifname eth0 mark_set 42 } localhost-public { ssh mark_match 42 } foomuuri-0.34/test/70-mark/golden.txt000066400000000000000000000171331526436212200174630ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept meta mark set ct mark tcp dport 22 meta mark == 42 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } meta mark set ct mark tcp dport 22 meta mark == 42 accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain filter_forward_mangle { type filter hook forward priority mangle + 5 meta mark set ct mark iifname "eth0" meta mark set 42 ct mark set meta mark accept } chain route_output_mangle { type route hook output priority mangle + 5 meta mark set ct mark } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/70-mss/000077500000000000000000000000001526436212200153155ustar00rootroot00000000000000foomuuri-0.34/test/70-mss/foomuuri.conf000066400000000000000000000007731526436212200200400ustar00rootroot00000000000000zone { localhost public } macro { only_4 10.0.0.0/24 only_6 ff00::/64 both only_4 only_6 } forward { ipv4 iifname eth4 daddr both mss 1004 ipv6 iifname eth6 daddr both mss 1006 iifname eth0 daddr both mss pmtu ipv6 saddr only_4 mss pmtu counter log "empty" ipv6 saddr only_6 mss pmtu counter log "flood my log" ipv6 saddr both mss pmtu counter log "flood my log even more" } localhost-public { mss 1000 ipv4 mss 1004 ipv6 mss 1006 iifname eth6 daddr only_6 mss pmtu } foomuuri-0.34/test/70-mss/golden.txt000066400000000000000000000202231526436212200173250ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { tcp flags syn tcp option maxseg size set 1000 tcp flags syn tcp option maxseg size set 1004 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { tcp flags syn tcp option maxseg size set 1000 tcp flags syn tcp option maxseg size set 1006 iifname "eth6" ip6 daddr ff00::/64 tcp flags syn tcp option maxseg size set rt mtu jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain filter_forward_mangle { type filter hook forward priority mangle + 5 iifname "eth4" ip daddr 10.0.0.0/24 tcp flags syn tcp option maxseg size set 1004 iifname "eth6" ip6 daddr ff00::/64 tcp flags syn tcp option maxseg size set 1006 iifname "eth0" ip daddr 10.0.0.0/24 tcp flags syn tcp option maxseg size set rt mtu iifname "eth0" ip6 daddr ff00::/64 tcp flags syn tcp option maxseg size set rt mtu ip6 saddr ff00::/64 counter log prefix "flood my log " level info flags skuid tcp flags syn tcp option maxseg size set rt mtu ip6 saddr ff00::/64 counter log prefix "flood my log even more " level info flags skuid tcp flags syn tcp option maxseg size set rt mtu } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/70-netmask/000077500000000000000000000000001526436212200161555ustar00rootroot00000000000000foomuuri-0.34/test/70-netmask/foomuuri.conf000066400000000000000000000007161526436212200206750ustar00rootroot00000000000000zone { localhost public } localhost-public { tcp 2000 daddr 10.0.1.0/24 tcp 2001 daddr -10.0.1.0/24 tcp 2002 daddr fffe:fff0:fff1:fff2:fff3:fff4:fff5:fff6 tcp 2003 daddr fffe:fff0:fff1:fff2:fff3:fff4:fff5:fff6/96 tcp 2004 daddr ::10:0:0:f00/-64 tcp 2005 daddr -::10:0:0:f00/-64 tcp 1000 saddr_rate "30/second burst 50" tcp 1001 saddr_rate "30/second burst 50" saddr_rate_mask 24 56 tcp 1002 daddr_rate "40/second" daddr_rate_mask 8 64 } foomuuri-0.34/test/70-netmask/golden.txt000066400000000000000000000213731526436212200201740ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } set _rate_set_1_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_1_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_2_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_2_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_3_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_3_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept ip daddr 10.0.1.0/24 tcp dport 2000 accept ip daddr != 10.0.1.0/24 tcp dport 2001 accept tcp dport 1000 update @_rate_set_1_4 { ip saddr limit rate 30/second burst 50 packets } accept tcp dport 1001 update @_rate_set_2_4 { ip saddr and 255.255.255.0 limit rate 30/second burst 50 packets } accept tcp dport 1002 update @_rate_set_3_4 { ip daddr and 255.0.0.0 limit rate 40/second } accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } ip6 daddr fffe:fff0:fff1:fff2:fff3:fff4:fff5:fff6 tcp dport 2002 accept ip6 daddr fffe:fff0:fff1:fff2:fff3:fff4:fff5:fff6/96 tcp dport 2003 accept ip6 daddr & ::ffff:ffff:ffff:ffff == ::10:0:0:f00 tcp dport 2004 accept ip6 daddr & ::ffff:ffff:ffff:ffff != ::10:0:0:f00 tcp dport 2005 accept tcp dport 1000 update @_rate_set_1_6 { ip6 saddr limit rate 30/second burst 50 packets } accept tcp dport 1001 update @_rate_set_2_6 { ip6 saddr and ffff:ffff:ffff:ff00:: limit rate 30/second burst 50 packets } accept tcp dport 1002 update @_rate_set_3_6 { ip6 daddr and ffff:ffff:ffff:ffff:: limit rate 40/second } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/70-port-knocking/000077500000000000000000000000001526436212200173005ustar00rootroot00000000000000foomuuri-0.34/test/70-port-knocking/foomuuri.conf000066400000000000000000000012171526436212200220150ustar00rootroot00000000000000zone { localhost public } iplist { # List of IP addresses that have performed initial knock.. # It is valid for 30 seconds. @knock dynamic=yes element_timeout=30s } public-localhost { # Allow SSH if IP address is in @knock iplist. ssh saddr @knock # Add source IP address to @knock iplist if there is UDP packet to port 5042. udp 5042 iplist_update saddr @knock drop log + ":knock" # Delete IP address from @knock iplist if there is any packet to any other # port. This prevents opening SSH if port scan is received. saddr @knock iplist_delete saddr @knock continue log + ":unknock" # ...other normal rules... drop log } foomuuri-0.34/test/70-port-knocking/golden.txt000066400000000000000000000204401526436212200213110ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop ip saddr @knock_4 tcp dport 22 accept udp dport 5042 update @knock_4 { ip saddr } jump lograte_1 ip saddr @knock_4 delete @knock_4 { ip saddr } update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost CONTINUE:unknock " level info flags skuid update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop ip6 saddr @knock_6 tcp dport 22 accept udp dport 5042 update @knock_6 { ip6 saddr } jump lograte_4 ip6 saddr @knock_6 delete @knock_6 { ip6 saddr } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost CONTINUE:unknock " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain lograte_1 { update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP:knock " level info flags skuid drop } chain lograte_4 { update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP:knock " level info flags skuid drop } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } set knock_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 30s } set knock_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 30s } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/70-priority/000077500000000000000000000000001526436212200163745ustar00rootroot00000000000000foomuuri-0.34/test/70-priority/foomuuri.conf000066400000000000000000000003261526436212200211110ustar00rootroot00000000000000zone { localhost public } forward { iifname eth0 priority_set 1:4242 iifname eth1 priority_match none priority_set 1:4343 } localhost-public { ssh priority_match none drop ssh priority_match 1:4242 } foomuuri-0.34/test/70-priority/golden.txt000066400000000000000000000171111526436212200204060ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept tcp dport 22 meta priority "none" drop tcp dport 22 meta priority "1:4242" accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } tcp dport 22 meta priority "none" drop tcp dport 22 meta priority "1:4242" accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain filter_forward_mangle { type filter hook forward priority mangle + 5 iifname "eth0" meta priority set "1:4242" accept iifname "eth1" meta priority "none" meta priority set "1:4343" accept } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/70-rate/000077500000000000000000000000001526436212200154465ustar00rootroot00000000000000foomuuri-0.34/test/70-rate/foomuuri.conf000066400000000000000000000010451526436212200201620ustar00rootroot00000000000000zone { localhost public } public-localhost { global_rate "3/second" accept global_rate "4/minute burst 5" accept global_rate "5/hour burst 6 packets" accept global_rate "over 3/minute" drop global_rate "2 bytes/second" accept global_rate "3 kbytes/minute burst 4 mbytes" accept global_rate "over 1 mbytes/second" accept global_rate "over 6 kbytes/second burst 1 mbytes" accept global_rate "ct count 4" accept global_rate "ct count over 5" accept http saddr_rate "5/minute burst 5" ssh saddr_rate "20/hour burst 5" } foomuuri-0.34/test/70-rate/golden.txt000066400000000000000000000213101526436212200174540ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } set _rate_set_1_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_1_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } set _rate_set_2_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1h gc-interval 5m } set _rate_set_2_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1h gc-interval 5m } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop limit rate 3/second accept limit rate 4/minute burst 5 packets accept limit rate 5/hour burst 6 packets accept limit rate over 3/minute drop limit rate 2 bytes/second accept limit rate 3 kbytes/minute burst 4 mbytes accept limit rate over 1 mbytes/second accept limit rate over 6 kbytes/second burst 1 mbytes accept ct count 4 accept ct count over 5 accept tcp dport 80 update @_rate_set_1_4 { ip saddr limit rate 5/minute burst 5 packets } accept tcp dport 22 update @_rate_set_2_4 { ip saddr limit rate 20/hour burst 5 packets } accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop limit rate 3/second accept limit rate 4/minute burst 5 packets accept limit rate 5/hour burst 6 packets accept limit rate over 3/minute drop limit rate 2 bytes/second accept limit rate 3 kbytes/minute burst 4 mbytes accept limit rate over 1 mbytes/second accept limit rate over 6 kbytes/second burst 1 mbytes accept ct count 4 accept ct count over 5 accept tcp dport 80 update @_rate_set_1_6 { ip6 saddr limit rate 5/minute burst 5 packets } accept tcp dport 22 update @_rate_set_2_6 { ip6 saddr limit rate 20/hour burst 5 packets } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/70-snat/000077500000000000000000000000001526436212200154605ustar00rootroot00000000000000foomuuri-0.34/test/70-snat/foomuuri.conf000066400000000000000000000013151526436212200201740ustar00rootroot00000000000000zone { localhost public } snat { # basic oifname eth0 snat 2a03:1111:1111::1 saddr fc00:ffff:2222::/64 snat 2a03:1111:222:2222::2 # port tcp saddr fc00:ffff:3333::/64 snat [2a03:1111:222:3333::3]:9999-11000 # pool saddr fc00:ffff:4444::/64 snat 2a03:1111:222:4444::/64 # ipv6 prefix saddr fc00:ffff:5555::/64 snat_prefix 2a03:1111:222:5555::/64 # ipv4 port tcp saddr 10.1.2.0/24 snat 10.0.0.5:9999-11000 # ipv4 pool saddr 10.2.3.0/24 snat 10.0.0.2/31 saddr 10.3.4.0/24 snat 10.0.0.4-10.0.0.127 # IPv4 and IPv6 in one line oifname eth1 snat 10.0.0.6 2a03:1111:222:6666::6 # deprecated 'to' keyword is still accepted oifname eth2 snat to 10.0.0.7 2a03:1111:222:7777::7 } foomuuri-0.34/test/70-snat/golden.txt000066400000000000000000000177351526436212200175060ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain nat_postrouting_srcnat { type nat hook postrouting priority srcnat + 5 oifname "eth0" snat ip6 to 2a03:1111:1111::1 ip6 saddr fc00:ffff:2222::/64 snat ip6 to 2a03:1111:222:2222::2 ip6 saddr fc00:ffff:3333::/64 ip6 nexthdr tcp snat ip6 to [2a03:1111:222:3333::3]:9999-11000 ip6 saddr fc00:ffff:4444::/64 snat ip6 to 2a03:1111:222:4444::/64 ip6 saddr fc00:ffff:5555::/64 snat ip6 prefix to 2a03:1111:222:5555::/64 ip saddr 10.1.2.0/24 ip protocol tcp snat ip to 10.0.0.5:9999-11000 ip saddr 10.2.3.0/24 snat ip to 10.0.0.2/31 ip saddr 10.3.4.0/24 snat ip to 10.0.0.4-10.0.0.127 oifname "eth1" snat ip to 10.0.0.6 oifname "eth1" snat ip6 to 2a03:1111:222:6666::6 oifname "eth2" snat ip to 10.0.0.7 oifname "eth2" snat ip6 to 2a03:1111:222:7777::7 } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/70-template/000077500000000000000000000000001526436212200163265ustar00rootroot00000000000000foomuuri-0.34/test/70-template/foomuuri.conf000066400000000000000000000014131526436212200210410ustar00rootroot00000000000000zone { localhost public } template simple { tcp 1 tcp 2 tcp 3 drop } template double { udp 4 template simple udp 5 } template multi_1 { tcp udp } template multi_2 { dport 6 sport 7 } template multi_3 { reject } template services { tcp 10 tcp 11 tcp 12 udp 13 drop udp 14 drop } template addresses { saddr 10.0.0.5 saddr 10.0.0.6 } template issue209 { uid root https daddr 192.0.2.0 } localhost-public { template simple template double template services saddr 10.0.0.2 template services saddr 10.0.0.4 tcp 20 21 template addresses drop template services template addresses # saddr tcp/udp dport6/sport7 reject => 4 lines template multi_1 saddr 10.0.0.1 template multi_2 template multi_3 template issue209 } foomuuri-0.34/test/70-template/golden.txt000066400000000000000000000213201526436212200203350ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } tcp dport { 1, 2 } accept ip protocol igmp ip daddr 224.0.0.22 accept tcp dport 3 drop udp dport 4 accept tcp dport { 1, 2 } accept tcp dport 3 drop udp dport 5 accept ip saddr 10.0.0.2 tcp dport 10 accept ip saddr 10.0.0.2 tcp dport 11 accept ip saddr 10.0.0.2 tcp dport 12 accept ip saddr 10.0.0.2 udp dport 13 drop ip saddr 10.0.0.2 udp dport 14 drop ip saddr 10.0.0.4 tcp dport 10 accept ip saddr 10.0.0.4 tcp dport 11 accept ip saddr 10.0.0.4 tcp dport 12 accept ip saddr 10.0.0.4 udp dport 13 drop ip saddr 10.0.0.4 udp dport 14 drop ip saddr 10.0.0.5 tcp dport { 20, 21 } drop ip saddr 10.0.0.6 tcp dport { 20, 21 } drop ip saddr 10.0.0.5 tcp dport 10 accept ip saddr 10.0.0.6 tcp dport 10 accept ip saddr 10.0.0.5 tcp dport 11 accept ip saddr 10.0.0.6 tcp dport 11 accept ip saddr 10.0.0.5 tcp dport 12 accept ip saddr 10.0.0.6 tcp dport 12 accept ip saddr 10.0.0.5 udp dport 13 drop ip saddr 10.0.0.6 udp dport 13 drop ip saddr 10.0.0.5 udp dport 14 drop ip saddr 10.0.0.6 udp dport 14 drop ip saddr 10.0.0.1 tcp dport 6 reject with icmpx admin-prohibited ip saddr 10.0.0.1 tcp sport 7 reject with icmpx admin-prohibited ip saddr 10.0.0.1 udp dport 6 reject with icmpx admin-prohibited ip saddr 10.0.0.1 udp sport 7 reject with icmpx admin-prohibited ip daddr 192.0.2.0 tcp dport 443 meta skuid "root" accept ip daddr 192.0.2.0 udp dport 443 meta skuid "root" accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } tcp dport { 1, 2 } accept tcp dport 3 drop udp dport 4 accept tcp dport { 1, 2 } accept tcp dport 3 drop udp dport 5 accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/70-time/000077500000000000000000000000001526436212200154515ustar00rootroot00000000000000foomuuri-0.34/test/70-time/foomuuri.conf000066400000000000000000000013271526436212200201700ustar00rootroot00000000000000zone { localhost public } localhost-public { # day tcp 2000 time monday tcp 2001 time "<= tuesday" # hour tcp 3000 time 16:00 tcp 3001 time 16:00:01 tcp 3002 time "== 16:00:02" tcp 3003 time "> 17:00:23" tcp 3004 time "18:00-19:00" # nft 1.1.5 accepts "22:00-03:00" but nft 1.1.3 doesn't. Don't test it. # tcp 3005 time "22:00-03:00" # time tcp 4000 time 2024-05-03 tcp 4001 time ">= 2024-05-04 19:00" tcp 4002 time "< 2024-05-04 19:00:01" tcp 4003 time "!= 2024-05-05 19:30-21:00" # misc tcp 5001 time "monday 20:00-22:00 > 2025-01-01" tcp 5002 time "> 2025-01-02 != Tuesday" tcp 5003 time "!= Tuesday > 2025-01-02" tcp 5004 time Friday > 21:00 # Not recommended without " } foomuuri-0.34/test/70-time/golden.txt000066400000000000000000000212021526436212200174570ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept tcp dport 2000 day "Monday" accept tcp dport 2001 day <= "Tuesday" accept tcp dport 3000 hour "16:00" accept tcp dport 3001 hour "16:00:01" accept tcp dport 3002 hour "16:00:02" accept tcp dport 3003 hour > "17:00:23" accept tcp dport 3004 hour 18:00-19:00 accept tcp dport 4000 time "2024-05-03" accept tcp dport 4001 time >= "2024-05-04 19:00" accept tcp dport 4002 time < "2024-05-04 19:00:01" accept tcp dport 4003 time != "2024-05-05 19:30-21:00" accept tcp dport 5001 day "Monday" hour 20:00-22:00 time > "2025-01-01" accept tcp dport 5002 time > "2025-01-02" day != "Tuesday" accept tcp dport 5003 day != "Tuesday" time > "2025-01-02" accept tcp dport 5004 day "Friday" hour > "21:00" accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } tcp dport 2000 day "Monday" accept tcp dport 2001 day <= "Tuesday" accept tcp dport 3000 hour "16:00" accept tcp dport 3001 hour "16:00:01" accept tcp dport 3002 hour "16:00:02" accept tcp dport 3003 hour > "17:00:23" accept tcp dport 3004 hour 18:00-19:00 accept tcp dport 4000 time "2024-05-03" accept tcp dport 4001 time >= "2024-05-04 19:00" accept tcp dport 4002 time < "2024-05-04 19:00:01" accept tcp dport 4003 time != "2024-05-05 19:30-21:00" accept tcp dport 5001 day "Monday" hour 20:00-22:00 time > "2025-01-01" accept tcp dport 5002 time > "2025-01-02" day != "Tuesday" accept tcp dport 5003 day != "Tuesday" time > "2025-01-02" accept tcp dport 5004 day "Friday" hour > "21:00" accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/70-tproxy/000077500000000000000000000000001526436212200160605ustar00rootroot00000000000000foomuuri-0.34/test/70-tproxy/foomuuri.conf000066400000000000000000000010001526436212200205630ustar00rootroot00000000000000zone { localhost public } prerouting { # Use lower 8 bits to mark tproxy traffic mark_match -0x00/0xff # Anti-loop protection # Specific IPv4 port only. My own IP is 192.168.0.1 so that # sysctl net.ipv4.conf.eth0.route_localnet=1 is not needed. tcp 8888 tproxy 192.168.0.1:18888 mark_set 0x01/0xff # All IPv4 TCP traffic, does not match IPv6 tcp tproxy 192.168.0.1:12345 mark_set 0x02/0xff # All IPv4 and IPv6 TCP traffic tcp tproxy 192.168.0.1:42424 [::1]:42424 mark_set 0x03/0xff } foomuuri-0.34/test/70-tproxy/golden.txt000066400000000000000000000176521526436212200201040ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain filter_prerouting_mangle { type filter hook prerouting priority mangle + 5 meta mark set ct mark meta mark & 0xff != 0x00 accept tcp dport 8888 tproxy ip to 192.168.0.1:18888 meta mark set meta mark & 0xffffff00 | 0x01 ct mark set meta mark accept ip protocol tcp tproxy ip to 192.168.0.1:12345 meta mark set meta mark & 0xffffff00 | 0x02 ct mark set meta mark accept ip protocol tcp tproxy ip to 192.168.0.1:42424 meta mark set meta mark & 0xffffff00 | 0x03 ct mark set meta mark accept ip6 nexthdr tcp tproxy ip6 to [::1]:42424 meta mark set meta mark & 0xffffff00 | 0x03 ct mark set meta mark accept } chain route_output_mangle { type route hook output priority mangle + 5 meta mark set ct mark } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/70-uid/000077500000000000000000000000001526436212200152745ustar00rootroot00000000000000foomuuri-0.34/test/70-uid/foomuuri.conf000066400000000000000000000002721526436212200200110ustar00rootroot00000000000000zone { localhost public } localhost-public { # uid/gid only works on localhost-xxx tcp 1000 uid 123 tcp 1001 gid 234 tcp 1010 uid 111 gid 222 https uid 100-200 root 300 } foomuuri-0.34/test/70-uid/golden.txt000066400000000000000000000173041526436212200173120ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept tcp dport 1000 meta skuid 123 accept tcp dport 1001 meta skgid 234 accept tcp dport 1010 meta skuid 111 meta skgid 222 accept tcp dport 443 meta skuid { "root", 100-200, 300 } accept udp dport 443 meta skuid { "root", 100-200, 300 } accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } tcp dport 1000 meta skuid 123 accept tcp dport 1001 meta skgid 234 accept tcp dport 1010 meta skuid 111 meta skgid 222 accept tcp dport 443 meta skuid { "root", 100-200, 300 } accept udp dport 443 meta skuid { "root", 100-200, 300 } accept update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/70-zonemap/000077500000000000000000000000001526436212200161645ustar00rootroot00000000000000foomuuri-0.34/test/70-zonemap/foomuuri.conf000066400000000000000000000004251526436212200207010ustar00rootroot00000000000000zone { localhost public a b combine_orig_1 combine_orig_2 combine_new } zonemap { szone combine_orig_1 combine_orig_2 new_szone combine_new tcp 1000 new_dzone a tcp 1001 new_szone b tcp 1010 dzone public new_dzone a tcp 1011 szone public new_szone b } foomuuri-0.34/test/70-zonemap/golden.txt000066400000000000000000001554121526436212200202050ustar00rootroot00000000000000table inet foomuuri delete table inet foomuuri table inet foomuuri { chain allow_icmp_4 { icmp type { destination-unreachable, # 3, Destination Unreachable time-exceeded, # 11, Time Exceeded parameter-problem # 12, Parameter Problem } accept } chain allow_icmp_6 { icmpv6 type { destination-unreachable, # 1, Destination Unreachable packet-too-big, # 2, Packet Too Big time-exceeded, # 3, Time Exceeded parameter-problem, # 4, Parameter Problem nd-router-solicit, # 133, Router Solicitation nd-neighbor-solicit, # 135, Neighbor Solicitation nd-neighbor-advert, # 136, Neighbor Advertisement ind-neighbor-solicit, # 141, Inverse Neighbor Discovery Solicitation Message ind-neighbor-advert # 142, Inverse Neighbor Discovery Advertisement Message } accept icmpv6 type . ip6 saddr { nd-router-advert . fe80::/10, # 134, Router Advertisement mld-listener-query . fe80::/10, # 130, Multicast Listener Query mld-listener-report . fe80::/10, # 131, Multicast Listener Report mld-listener-done . fe80::/10, # 132, Multicast Listener Done 149 . fe80::/10, # 149, Certification Path Advertisement Message 151 . fe80::/10, # 151, Multicast Router Advertisement 152 . fe80::/10, # 152, Multicast Router Solicitation 153 . fe80::/10, # 153, Multicast Router Termination mld2-listener-report . fe80::/10, # 143, Version 2 Multicast Listener Report mld2-listener-report . ::, 148 . fe80::/10, # 148, Certification Path Solicitation Message 148 . :: } accept } chain smurfs_4 { ip saddr 0.0.0.0 return fib saddr type { broadcast, multicast } jump smurfs_drop } chain smurfs_6 { fib saddr type multicast jump smurfs_drop } chain invalid_drop { drop } chain smurfs_drop { drop } chain rpfilter_drop { udp sport 67 udp dport 68 return update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "RPFILTER DROP " level info flags skuid drop } chain input { type filter hook input priority filter + 5 iifname vmap @input_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "INPUT DROP " level info flags skuid drop } chain output { type filter hook output priority filter + 5 oifname vmap @output_zones ip protocol igmp ip daddr 224.0.0.22 accept ip6 saddr :: icmpv6 type mld2-listener-report accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "OUTPUT REJECT " level info flags skuid reject with icmpx admin-prohibited } chain forward { type filter hook forward priority filter + 5 iifname . oifname vmap @forward_zones update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "FORWARD DROP " level info flags skuid drop } map input_zones { type ifname : verdict elements = { "lo" : accept, } } map output_zones { type ifname : verdict elements = { "lo" : accept, } } map forward_zones { type ifname . ifname : verdict elements = { "lo" . "lo" : accept, } } chain localhost-localhost { meta nfproto vmap { ipv4 : goto localhost-localhost_4, ipv6 : goto localhost-localhost_6 } } chain localhost-localhost_4 { tcp dport 1000 goto localhost-a_4 tcp dport 1001 goto b-localhost_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } accept } chain localhost-localhost_6 { tcp dport 1000 goto localhost-a_6 tcp dport 1001 goto b-localhost_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } accept } chain localhost-public { meta nfproto vmap { ipv4 : goto localhost-public_4, ipv6 : goto localhost-public_6 } } chain localhost-public_4 { tcp dport 1000 goto localhost-a_4 tcp dport 1001 goto b-public_4 tcp dport 1010 goto localhost-a_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-public_6 { tcp dport 1000 goto localhost-a_6 tcp dport 1001 goto b-public_6 tcp dport 1010 goto localhost-a_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-public REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-a { meta nfproto vmap { ipv4 : goto localhost-a_4, ipv6 : goto localhost-a_6 } } chain localhost-a_4 { tcp dport 1001 goto b-a_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-a REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-a_6 { tcp dport 1001 goto b-a_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-a REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-b { meta nfproto vmap { ipv4 : goto localhost-b_4, ipv6 : goto localhost-b_6 } } chain localhost-b_4 { tcp dport 1000 goto localhost-a_4 tcp dport 1001 goto b-b_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-b REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-b_6 { tcp dport 1000 goto localhost-a_6 tcp dport 1001 goto b-b_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-b REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-combine_orig_1 { meta nfproto vmap { ipv4 : goto localhost-combine_orig_1_4, ipv6 : goto localhost-combine_orig_1_6 } } chain localhost-combine_orig_1_4 { tcp dport 1000 goto localhost-a_4 tcp dport 1001 goto b-combine_orig_1_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-combine_orig_1 REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-combine_orig_1_6 { tcp dport 1000 goto localhost-a_6 tcp dport 1001 goto b-combine_orig_1_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-combine_orig_1 REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-combine_orig_2 { meta nfproto vmap { ipv4 : goto localhost-combine_orig_2_4, ipv6 : goto localhost-combine_orig_2_6 } } chain localhost-combine_orig_2_4 { tcp dport 1000 goto localhost-a_4 tcp dport 1001 goto b-combine_orig_2_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-combine_orig_2 REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-combine_orig_2_6 { tcp dport 1000 goto localhost-a_6 tcp dport 1001 goto b-combine_orig_2_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-combine_orig_2 REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-combine_new { meta nfproto vmap { ipv4 : goto localhost-combine_new_4, ipv6 : goto localhost-combine_new_6 } } chain localhost-combine_new_4 { tcp dport 1000 goto localhost-a_4 tcp dport 1001 goto b-combine_new_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.22 accept update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "localhost-combine_new REJECT " level info flags skuid reject with icmpx admin-prohibited } chain localhost-combine_new_6 { tcp dport 1000 goto localhost-a_6 tcp dport 1001 goto b-combine_new_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "localhost-combine_new REJECT " level info flags skuid reject with icmpx admin-prohibited } chain public-localhost { meta nfproto vmap { ipv4 : goto public-localhost_4, ipv6 : goto public-localhost_6 } } chain public-localhost_4 { tcp dport 1000 goto public-a_4 tcp dport 1001 goto b-localhost_4 tcp dport 1011 goto b-localhost_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-localhost_6 { tcp dport 1000 goto public-a_6 tcp dport 1001 goto b-localhost_6 tcp dport 1011 goto b-localhost_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-localhost DROP " level info flags skuid drop } chain public-public { meta nfproto vmap { ipv4 : goto public-public_4, ipv6 : goto public-public_6 } } chain public-public_4 { tcp dport 1000 goto public-a_4 tcp dport 1001 goto b-public_4 tcp dport 1010 goto public-a_4 tcp dport 1011 goto b-public_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-public_6 { tcp dport 1000 goto public-a_6 tcp dport 1001 goto b-public_6 tcp dport 1010 goto public-a_6 tcp dport 1011 goto b-public_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-public DROP " level info flags skuid drop } chain public-a { meta nfproto vmap { ipv4 : goto public-a_4, ipv6 : goto public-a_6 } } chain public-a_4 { tcp dport 1001 goto b-a_4 tcp dport 1011 goto b-a_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-a DROP " level info flags skuid drop } chain public-a_6 { tcp dport 1001 goto b-a_6 tcp dport 1011 goto b-a_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-a DROP " level info flags skuid drop } chain public-b { meta nfproto vmap { ipv4 : goto public-b_4, ipv6 : goto public-b_6 } } chain public-b_4 { tcp dport 1000 goto public-a_4 tcp dport 1001 goto b-b_4 tcp dport 1011 goto b-b_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-b DROP " level info flags skuid drop } chain public-b_6 { tcp dport 1000 goto public-a_6 tcp dport 1001 goto b-b_6 tcp dport 1011 goto b-b_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-b DROP " level info flags skuid drop } chain public-combine_orig_1 { meta nfproto vmap { ipv4 : goto public-combine_orig_1_4, ipv6 : goto public-combine_orig_1_6 } } chain public-combine_orig_1_4 { tcp dport 1000 goto public-a_4 tcp dport 1001 goto b-combine_orig_1_4 tcp dport 1011 goto b-combine_orig_1_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-combine_orig_1 DROP " level info flags skuid drop } chain public-combine_orig_1_6 { tcp dport 1000 goto public-a_6 tcp dport 1001 goto b-combine_orig_1_6 tcp dport 1011 goto b-combine_orig_1_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-combine_orig_1 DROP " level info flags skuid drop } chain public-combine_orig_2 { meta nfproto vmap { ipv4 : goto public-combine_orig_2_4, ipv6 : goto public-combine_orig_2_6 } } chain public-combine_orig_2_4 { tcp dport 1000 goto public-a_4 tcp dport 1001 goto b-combine_orig_2_4 tcp dport 1011 goto b-combine_orig_2_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-combine_orig_2 DROP " level info flags skuid drop } chain public-combine_orig_2_6 { tcp dport 1000 goto public-a_6 tcp dport 1001 goto b-combine_orig_2_6 tcp dport 1011 goto b-combine_orig_2_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-combine_orig_2 DROP " level info flags skuid drop } chain public-combine_new { meta nfproto vmap { ipv4 : goto public-combine_new_4, ipv6 : goto public-combine_new_6 } } chain public-combine_new_4 { tcp dport 1000 goto public-a_4 tcp dport 1001 goto b-combine_new_4 tcp dport 1011 goto b-combine_new_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "public-combine_new DROP " level info flags skuid drop } chain public-combine_new_6 { tcp dport 1000 goto public-a_6 tcp dport 1001 goto b-combine_new_6 tcp dport 1011 goto b-combine_new_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "public-combine_new DROP " level info flags skuid drop } chain a-localhost { meta nfproto vmap { ipv4 : goto a-localhost_4, ipv6 : goto a-localhost_6 } } chain a-localhost_4 { tcp dport 1000 goto a-a_4 tcp dport 1001 goto b-localhost_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "a-localhost DROP " level info flags skuid drop } chain a-localhost_6 { tcp dport 1000 goto a-a_6 tcp dport 1001 goto b-localhost_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "a-localhost DROP " level info flags skuid drop } chain a-public { meta nfproto vmap { ipv4 : goto a-public_4, ipv6 : goto a-public_6 } } chain a-public_4 { tcp dport 1000 goto a-a_4 tcp dport 1001 goto b-public_4 tcp dport 1010 goto a-a_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "a-public DROP " level info flags skuid drop } chain a-public_6 { tcp dport 1000 goto a-a_6 tcp dport 1001 goto b-public_6 tcp dport 1010 goto a-a_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "a-public DROP " level info flags skuid drop } chain a-a { meta nfproto vmap { ipv4 : goto a-a_4, ipv6 : goto a-a_6 } } chain a-a_4 { tcp dport 1001 goto b-a_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "a-a DROP " level info flags skuid drop } chain a-a_6 { tcp dport 1001 goto b-a_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "a-a DROP " level info flags skuid drop } chain a-b { meta nfproto vmap { ipv4 : goto a-b_4, ipv6 : goto a-b_6 } } chain a-b_4 { tcp dport 1000 goto a-a_4 tcp dport 1001 goto b-b_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "a-b DROP " level info flags skuid drop } chain a-b_6 { tcp dport 1000 goto a-a_6 tcp dport 1001 goto b-b_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "a-b DROP " level info flags skuid drop } chain a-combine_orig_1 { meta nfproto vmap { ipv4 : goto a-combine_orig_1_4, ipv6 : goto a-combine_orig_1_6 } } chain a-combine_orig_1_4 { tcp dport 1000 goto a-a_4 tcp dport 1001 goto b-combine_orig_1_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "a-combine_orig_1 DROP " level info flags skuid drop } chain a-combine_orig_1_6 { tcp dport 1000 goto a-a_6 tcp dport 1001 goto b-combine_orig_1_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "a-combine_orig_1 DROP " level info flags skuid drop } chain a-combine_orig_2 { meta nfproto vmap { ipv4 : goto a-combine_orig_2_4, ipv6 : goto a-combine_orig_2_6 } } chain a-combine_orig_2_4 { tcp dport 1000 goto a-a_4 tcp dport 1001 goto b-combine_orig_2_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "a-combine_orig_2 DROP " level info flags skuid drop } chain a-combine_orig_2_6 { tcp dport 1000 goto a-a_6 tcp dport 1001 goto b-combine_orig_2_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "a-combine_orig_2 DROP " level info flags skuid drop } chain a-combine_new { meta nfproto vmap { ipv4 : goto a-combine_new_4, ipv6 : goto a-combine_new_6 } } chain a-combine_new_4 { tcp dport 1000 goto a-a_4 tcp dport 1001 goto b-combine_new_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "a-combine_new DROP " level info flags skuid drop } chain a-combine_new_6 { tcp dport 1000 goto a-a_6 tcp dport 1001 goto b-combine_new_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "a-combine_new DROP " level info flags skuid drop } chain b-localhost { meta nfproto vmap { ipv4 : goto b-localhost_4, ipv6 : goto b-localhost_6 } } chain b-localhost_4 { tcp dport 1000 goto b-a_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "b-localhost DROP " level info flags skuid drop } chain b-localhost_6 { tcp dport 1000 goto b-a_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "b-localhost DROP " level info flags skuid drop } chain b-public { meta nfproto vmap { ipv4 : goto b-public_4, ipv6 : goto b-public_6 } } chain b-public_4 { tcp dport 1000 goto b-a_4 tcp dport 1010 goto b-a_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "b-public DROP " level info flags skuid drop } chain b-public_6 { tcp dport 1000 goto b-a_6 tcp dport 1010 goto b-a_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "b-public DROP " level info flags skuid drop } chain b-a { meta nfproto vmap { ipv4 : goto b-a_4, ipv6 : goto b-a_6 } } chain b-a_4 { jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "b-a DROP " level info flags skuid drop } chain b-a_6 { jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "b-a DROP " level info flags skuid drop } chain b-b { meta nfproto vmap { ipv4 : goto b-b_4, ipv6 : goto b-b_6 } } chain b-b_4 { tcp dport 1000 goto b-a_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "b-b DROP " level info flags skuid drop } chain b-b_6 { tcp dport 1000 goto b-a_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "b-b DROP " level info flags skuid drop } chain b-combine_orig_1 { meta nfproto vmap { ipv4 : goto b-combine_orig_1_4, ipv6 : goto b-combine_orig_1_6 } } chain b-combine_orig_1_4 { tcp dport 1000 goto b-a_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "b-combine_orig_1 DROP " level info flags skuid drop } chain b-combine_orig_1_6 { tcp dport 1000 goto b-a_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "b-combine_orig_1 DROP " level info flags skuid drop } chain b-combine_orig_2 { meta nfproto vmap { ipv4 : goto b-combine_orig_2_4, ipv6 : goto b-combine_orig_2_6 } } chain b-combine_orig_2_4 { tcp dport 1000 goto b-a_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "b-combine_orig_2 DROP " level info flags skuid drop } chain b-combine_orig_2_6 { tcp dport 1000 goto b-a_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "b-combine_orig_2 DROP " level info flags skuid drop } chain b-combine_new { meta nfproto vmap { ipv4 : goto b-combine_new_4, ipv6 : goto b-combine_new_6 } } chain b-combine_new_4 { tcp dport 1000 goto b-a_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "b-combine_new DROP " level info flags skuid drop } chain b-combine_new_6 { tcp dport 1000 goto b-a_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "b-combine_new DROP " level info flags skuid drop } chain combine_orig_1-localhost { meta nfproto vmap { ipv4 : goto combine_orig_1-localhost_4, ipv6 : goto combine_orig_1-localhost_6 } } chain combine_orig_1-localhost_4 { goto combine_new-localhost_4 tcp dport 1000 goto combine_orig_1-a_4 tcp dport 1001 goto b-localhost_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_1-localhost DROP " level info flags skuid drop } chain combine_orig_1-localhost_6 { goto combine_new-localhost_6 tcp dport 1000 goto combine_orig_1-a_6 tcp dport 1001 goto b-localhost_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_1-localhost DROP " level info flags skuid drop } chain combine_orig_1-public { meta nfproto vmap { ipv4 : goto combine_orig_1-public_4, ipv6 : goto combine_orig_1-public_6 } } chain combine_orig_1-public_4 { goto combine_new-public_4 tcp dport 1000 goto combine_orig_1-a_4 tcp dport 1001 goto b-public_4 tcp dport 1010 goto combine_orig_1-a_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_1-public DROP " level info flags skuid drop } chain combine_orig_1-public_6 { goto combine_new-public_6 tcp dport 1000 goto combine_orig_1-a_6 tcp dport 1001 goto b-public_6 tcp dport 1010 goto combine_orig_1-a_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_1-public DROP " level info flags skuid drop } chain combine_orig_1-a { meta nfproto vmap { ipv4 : goto combine_orig_1-a_4, ipv6 : goto combine_orig_1-a_6 } } chain combine_orig_1-a_4 { goto combine_new-a_4 tcp dport 1001 goto b-a_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_1-a DROP " level info flags skuid drop } chain combine_orig_1-a_6 { goto combine_new-a_6 tcp dport 1001 goto b-a_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_1-a DROP " level info flags skuid drop } chain combine_orig_1-b { meta nfproto vmap { ipv4 : goto combine_orig_1-b_4, ipv6 : goto combine_orig_1-b_6 } } chain combine_orig_1-b_4 { goto combine_new-b_4 tcp dport 1000 goto combine_orig_1-a_4 tcp dport 1001 goto b-b_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_1-b DROP " level info flags skuid drop } chain combine_orig_1-b_6 { goto combine_new-b_6 tcp dport 1000 goto combine_orig_1-a_6 tcp dport 1001 goto b-b_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_1-b DROP " level info flags skuid drop } chain combine_orig_1-combine_orig_1 { meta nfproto vmap { ipv4 : goto combine_orig_1-combine_orig_1_4, ipv6 : goto combine_orig_1-combine_orig_1_6 } } chain combine_orig_1-combine_orig_1_4 { goto combine_new-combine_orig_1_4 tcp dport 1000 goto combine_orig_1-a_4 tcp dport 1001 goto b-combine_orig_1_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_1-combine_orig_1 DROP " level info flags skuid drop } chain combine_orig_1-combine_orig_1_6 { goto combine_new-combine_orig_1_6 tcp dport 1000 goto combine_orig_1-a_6 tcp dport 1001 goto b-combine_orig_1_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_1-combine_orig_1 DROP " level info flags skuid drop } chain combine_orig_1-combine_orig_2 { meta nfproto vmap { ipv4 : goto combine_orig_1-combine_orig_2_4, ipv6 : goto combine_orig_1-combine_orig_2_6 } } chain combine_orig_1-combine_orig_2_4 { goto combine_new-combine_orig_2_4 tcp dport 1000 goto combine_orig_1-a_4 tcp dport 1001 goto b-combine_orig_2_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_1-combine_orig_2 DROP " level info flags skuid drop } chain combine_orig_1-combine_orig_2_6 { goto combine_new-combine_orig_2_6 tcp dport 1000 goto combine_orig_1-a_6 tcp dport 1001 goto b-combine_orig_2_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_1-combine_orig_2 DROP " level info flags skuid drop } chain combine_orig_1-combine_new { meta nfproto vmap { ipv4 : goto combine_orig_1-combine_new_4, ipv6 : goto combine_orig_1-combine_new_6 } } chain combine_orig_1-combine_new_4 { goto combine_new-combine_new_4 tcp dport 1000 goto combine_orig_1-a_4 tcp dport 1001 goto b-combine_new_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_1-combine_new DROP " level info flags skuid drop } chain combine_orig_1-combine_new_6 { goto combine_new-combine_new_6 tcp dport 1000 goto combine_orig_1-a_6 tcp dport 1001 goto b-combine_new_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_1-combine_new DROP " level info flags skuid drop } chain combine_orig_2-localhost { meta nfproto vmap { ipv4 : goto combine_orig_2-localhost_4, ipv6 : goto combine_orig_2-localhost_6 } } chain combine_orig_2-localhost_4 { goto combine_new-localhost_4 tcp dport 1000 goto combine_orig_2-a_4 tcp dport 1001 goto b-localhost_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_2-localhost DROP " level info flags skuid drop } chain combine_orig_2-localhost_6 { goto combine_new-localhost_6 tcp dport 1000 goto combine_orig_2-a_6 tcp dport 1001 goto b-localhost_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_2-localhost DROP " level info flags skuid drop } chain combine_orig_2-public { meta nfproto vmap { ipv4 : goto combine_orig_2-public_4, ipv6 : goto combine_orig_2-public_6 } } chain combine_orig_2-public_4 { goto combine_new-public_4 tcp dport 1000 goto combine_orig_2-a_4 tcp dport 1001 goto b-public_4 tcp dport 1010 goto combine_orig_2-a_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_2-public DROP " level info flags skuid drop } chain combine_orig_2-public_6 { goto combine_new-public_6 tcp dport 1000 goto combine_orig_2-a_6 tcp dport 1001 goto b-public_6 tcp dport 1010 goto combine_orig_2-a_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_2-public DROP " level info flags skuid drop } chain combine_orig_2-a { meta nfproto vmap { ipv4 : goto combine_orig_2-a_4, ipv6 : goto combine_orig_2-a_6 } } chain combine_orig_2-a_4 { goto combine_new-a_4 tcp dport 1001 goto b-a_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_2-a DROP " level info flags skuid drop } chain combine_orig_2-a_6 { goto combine_new-a_6 tcp dport 1001 goto b-a_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_2-a DROP " level info flags skuid drop } chain combine_orig_2-b { meta nfproto vmap { ipv4 : goto combine_orig_2-b_4, ipv6 : goto combine_orig_2-b_6 } } chain combine_orig_2-b_4 { goto combine_new-b_4 tcp dport 1000 goto combine_orig_2-a_4 tcp dport 1001 goto b-b_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_2-b DROP " level info flags skuid drop } chain combine_orig_2-b_6 { goto combine_new-b_6 tcp dport 1000 goto combine_orig_2-a_6 tcp dport 1001 goto b-b_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_2-b DROP " level info flags skuid drop } chain combine_orig_2-combine_orig_1 { meta nfproto vmap { ipv4 : goto combine_orig_2-combine_orig_1_4, ipv6 : goto combine_orig_2-combine_orig_1_6 } } chain combine_orig_2-combine_orig_1_4 { goto combine_new-combine_orig_1_4 tcp dport 1000 goto combine_orig_2-a_4 tcp dport 1001 goto b-combine_orig_1_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_2-combine_orig_1 DROP " level info flags skuid drop } chain combine_orig_2-combine_orig_1_6 { goto combine_new-combine_orig_1_6 tcp dport 1000 goto combine_orig_2-a_6 tcp dport 1001 goto b-combine_orig_1_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_2-combine_orig_1 DROP " level info flags skuid drop } chain combine_orig_2-combine_orig_2 { meta nfproto vmap { ipv4 : goto combine_orig_2-combine_orig_2_4, ipv6 : goto combine_orig_2-combine_orig_2_6 } } chain combine_orig_2-combine_orig_2_4 { goto combine_new-combine_orig_2_4 tcp dport 1000 goto combine_orig_2-a_4 tcp dport 1001 goto b-combine_orig_2_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_2-combine_orig_2 DROP " level info flags skuid drop } chain combine_orig_2-combine_orig_2_6 { goto combine_new-combine_orig_2_6 tcp dport 1000 goto combine_orig_2-a_6 tcp dport 1001 goto b-combine_orig_2_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_2-combine_orig_2 DROP " level info flags skuid drop } chain combine_orig_2-combine_new { meta nfproto vmap { ipv4 : goto combine_orig_2-combine_new_4, ipv6 : goto combine_orig_2-combine_new_6 } } chain combine_orig_2-combine_new_4 { goto combine_new-combine_new_4 tcp dport 1000 goto combine_orig_2-a_4 tcp dport 1001 goto b-combine_new_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_2-combine_new DROP " level info flags skuid drop } chain combine_orig_2-combine_new_6 { goto combine_new-combine_new_6 tcp dport 1000 goto combine_orig_2-a_6 tcp dport 1001 goto b-combine_new_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "combine_orig_2-combine_new DROP " level info flags skuid drop } chain combine_new-localhost { meta nfproto vmap { ipv4 : goto combine_new-localhost_4, ipv6 : goto combine_new-localhost_6 } } chain combine_new-localhost_4 { tcp dport 1000 goto combine_new-a_4 tcp dport 1001 goto b-localhost_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } ip protocol igmp ip daddr 224.0.0.1 accept fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "combine_new-localhost DROP " level info flags skuid drop } chain combine_new-localhost_6 { tcp dport 1000 goto combine_new-a_6 tcp dport 1001 goto b-localhost_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "combine_new-localhost DROP " level info flags skuid drop } chain combine_new-public { meta nfproto vmap { ipv4 : goto combine_new-public_4, ipv6 : goto combine_new-public_6 } } chain combine_new-public_4 { tcp dport 1000 goto combine_new-a_4 tcp dport 1001 goto b-public_4 tcp dport 1010 goto combine_new-a_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "combine_new-public DROP " level info flags skuid drop } chain combine_new-public_6 { tcp dport 1000 goto combine_new-a_6 tcp dport 1001 goto b-public_6 tcp dport 1010 goto combine_new-a_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "combine_new-public DROP " level info flags skuid drop } chain combine_new-a { meta nfproto vmap { ipv4 : goto combine_new-a_4, ipv6 : goto combine_new-a_6 } } chain combine_new-a_4 { tcp dport 1001 goto b-a_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "combine_new-a DROP " level info flags skuid drop } chain combine_new-a_6 { tcp dport 1001 goto b-a_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "combine_new-a DROP " level info flags skuid drop } chain combine_new-b { meta nfproto vmap { ipv4 : goto combine_new-b_4, ipv6 : goto combine_new-b_6 } } chain combine_new-b_4 { tcp dport 1000 goto combine_new-a_4 tcp dport 1001 goto b-b_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "combine_new-b DROP " level info flags skuid drop } chain combine_new-b_6 { tcp dport 1000 goto combine_new-a_6 tcp dport 1001 goto b-b_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "combine_new-b DROP " level info flags skuid drop } chain combine_new-combine_orig_1 { meta nfproto vmap { ipv4 : goto combine_new-combine_orig_1_4, ipv6 : goto combine_new-combine_orig_1_6 } } chain combine_new-combine_orig_1_4 { tcp dport 1000 goto combine_new-a_4 tcp dport 1001 goto b-combine_orig_1_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "combine_new-combine_orig_1 DROP " level info flags skuid drop } chain combine_new-combine_orig_1_6 { tcp dport 1000 goto combine_new-a_6 tcp dport 1001 goto b-combine_orig_1_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "combine_new-combine_orig_1 DROP " level info flags skuid drop } chain combine_new-combine_orig_2 { meta nfproto vmap { ipv4 : goto combine_new-combine_orig_2_4, ipv6 : goto combine_new-combine_orig_2_6 } } chain combine_new-combine_orig_2_4 { tcp dport 1000 goto combine_new-a_4 tcp dport 1001 goto b-combine_orig_2_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "combine_new-combine_orig_2 DROP " level info flags skuid drop } chain combine_new-combine_orig_2_6 { tcp dport 1000 goto combine_new-a_6 tcp dport 1001 goto b-combine_orig_2_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "combine_new-combine_orig_2 DROP " level info flags skuid drop } chain combine_new-combine_new { meta nfproto vmap { ipv4 : goto combine_new-combine_new_4, ipv6 : goto combine_new-combine_new_6 } } chain combine_new-combine_new_4 { tcp dport 1000 goto combine_new-a_4 tcp dport 1001 goto b-combine_new_4 jump allow_icmp_4 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_4, untracked : jump smurfs_4 } fib daddr type { broadcast, multicast } drop update @_lograte_set_4 { ip saddr limit rate 1/second burst 3 packets } log prefix "combine_new-combine_new DROP " level info flags skuid drop } chain combine_new-combine_new_6 { tcp dport 1000 goto combine_new-a_6 tcp dport 1001 goto b-combine_new_6 jump allow_icmp_6 ct state vmap { established : accept, related : accept, invalid : jump invalid_drop, new : jump smurfs_6, untracked : jump smurfs_6 } fib daddr type multicast drop update @_lograte_set_6 { ip6 saddr limit rate 1/second burst 3 packets } log prefix "combine_new-combine_new DROP " level info flags skuid drop } set _lograte_set_4 { type ipv4_addr size 65535 flags dynamic,timeout timeout 1m } set _lograte_set_6 { type ipv6_addr size 65535 flags dynamic,timeout timeout 1m } chain rpfilter { type filter hook prerouting priority filter + 5 fib saddr . mark . iif oif 0 meta ipsec missing jump rpfilter_drop } } foomuuri-0.34/test/Makefile000066400000000000000000000015311526436212200157270ustar00rootroot00000000000000# Foomuuri - Multizone bidirectional nftables firewall. SUBDIRS ?= $(sort $(wildcard ??-*)) SOURCE ?= ../src/foomuuri ../src/tests/ ../prometheus/prometheus-foomuuri-exporter .PHONY: all lint test clean distclean $(SUBDIRS) ifdef COVERAGE export RUN = unshare --map-root-user --net coverage run --append --data-file=$(CURDIR)/../.coverage else export RUN = python3 endif all: lint test lint: ruff check $(SOURCE) ruff format --diff $(SOURCE) ty check --ignore=unresolved-import --ignore=unused-ignore-comment $(SOURCE) pylint $(SOURCE) test: $(SUBDIRS) cd ../ ; $(RUN) -m unittest clean distclean: rm -f */*.fw rm -f */iplist-cache.json rm -f */zone $(SUBDIRS): [ ! -f $@/Makefile ] || $(MAKE) -C $@ $(RUN) ../src/foomuuri --set=etc_dir=$@ --set=share_dir=../etc --set=state_dir=$@ --set=run_dir=$@ check diff -u $@/golden.txt $@/next.fw