pax_global_header00006660000000000000000000000064147461120520014514gustar00rootroot0000000000000052 comment=6ce705203e5417f3aad90b143e53e878c327838c foomuuri-0.27/000077500000000000000000000000001474611205200133115ustar00rootroot00000000000000foomuuri-0.27/.gitignore000066400000000000000000000000531474611205200152770ustar00rootroot00000000000000foomuuri-*.tar.gz test/*/*.fw test/*/zone foomuuri-0.27/CHANGELOG.md000066400000000000000000000172611474611205200151310ustar00rootroot00000000000000# ChangeLog ## 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.27/COPYING000066400000000000000000000432541474611205200143540ustar00rootroot00000000000000 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.27/Makefile000066400000000000000000000064741474611205200147640ustar00rootroot00000000000000# Foomuuri - Multizone bidirectional nftables firewall. .PHONY: all test devel clean distclean install sysupdate release tar CURRENT_VERSION ?= $(shell grep ^VERSION src/foomuuri | awk -F\' '{ print $$2 }') # Default target is to run tests all: test # Run testsuite and check source test: $(MAKE) -C test # Generate firewall ruleset to file, used in development devel: 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 -f 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)/ cp systemd/foomuuri-resolve.service $(DESTDIR)$(SYSTEMD_SYSTEM_LOCATION)/ cp systemd/foomuuri-resolve.timer $(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/ cp systemd/fi.foobar.Foomuuri1.conf $(DESTDIR)/usr/share/dbus-1/system.d/ 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 doc/foomuuri.8 $(DESTDIR)/usr/share/man/man8/ # 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 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)@" doc/foomuuri.md sed -i -e "s@^\(date: \).*@\1$(shell date +'%b %d, %Y')@" doc/foomuuri.md make --directory=doc git diff git add CHANGELOG.md src/foomuuri doc/foomuuri.md doc/foomuuri.8 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.27/README.md000066400000000000000000000026321474611205200145730ustar00rootroot00000000000000# Foomuuri Foomuuri is a multizone bidirectional nftables firewall. See [wiki](https://github.com/FoobarOy/foomuuri/wiki) for documentation and [host firewall](https://github.com/FoobarOy/foomuuri/wiki/Host-Firewall) or [router firewall](https://github.com/FoobarOy/foomuuri/wiki/Router-Firewall) for example configuration files. [Getting started](https://github.com/FoobarOy/foomuuri/wiki/Getting-Started) page contains quick instructions how to install Foomuuri. Help is available via [discussions](https://github.com/FoobarOy/foomuuri/discussions). ## Features * Firewall zones * Bidirectional firewalling for incoming, outgoing and forwarding traffic * Suitable for all systems from personal laptop to corporate firewalls * Rich rule language for flexible and complex rules * Predefined list of services for simple rule writing * Rule language supports macros and templates * IPv4 and IPv6 support with automatic rule splitting per protocol * SNAT, DNAT and masquerading support * Logging and counting * Rate limiting * DNS hostname lookup support with dynamic IP address refreshing * Multi-ISP support with internal network connectivity monitor * Country database support aka geolocation * IPsec matching support * Ability to map certain traffic to separate zones * D-Bus API * FirewallD emulation for NetworkManager's zone support * Raw nftables rules can be used * Fresh design, written to use modern nftables's features foomuuri-0.27/doc/000077500000000000000000000000001474611205200140565ustar00rootroot00000000000000foomuuri-0.27/doc/Makefile000066400000000000000000000001261474611205200155150ustar00rootroot00000000000000foomuuri.8: foomuuri.md pandoc --standalone --to=man foomuuri.md --output foomuuri.8 foomuuri-0.27/doc/foomuuri.8000066400000000000000000000056131474611205200160210ustar00rootroot00000000000000.\" Automatically generated by Pandoc 3.1.11.1 .\" .TH "FOOMUURI" "8" "Jan 28, 2025" "Foomuuri 0.27" "User Manual" .SH NAME foomuuri \- multizone bidirectional nftables firewall .SH SYNOPSIS \f[B]foomuuri\f[R] [\f[I]OPTION\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\[cq]s zone support. .SH OPTIONS .TP \f[CR]\-\-help\f[R] display this help and exit .TP \f[CR]\-\-version\f[R] output version information and exit .TP \f[CR]\-\-verbose\f[R] verbose output .TP \f[CR]\-\-quiet\f[R] be quiet .TP \f[CR]\-\-force\f[R] force some operations, don\[cq]t check anything .TP \f[CR]\-\-soft\f[R] don\[cq]t force operations, check more .TP \f[CR]\-\-set=option=value\f[R] set 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 resolve and iplist refresh .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 \[lq]block all traffic\[rq] ruleset .TP \f[B]list\f[R] list active ruleset currently loaded to kernel .TP \f[B]list zone\-zone {zone\-zone\&...}\f[R] list active ruleset for \f[B]zone\-zone\f[R] currently loaded to kernel .TP \f[B]list macro\f[R] list all known macros .TP \f[B]list macro keyword {keyword\&...}\f[R] list all macros with specified name or value .TP \f[B]list counter\f[R] list all named counters .TP \f[B]list counter keyword {keyword\&...}\f[R] list named counter with specified name .TP \f[B]iplist list\f[R] list entries in all configured iplists and resolves .TP \f[B]iplist list name {name\&...}\f[R] list entries in named iplist/resolve .TP \f[B]iplist add name {timeout} ipaddress {ipaddress\&...}\f[R] add or refresh IP address to iplist .TP \f[B]iplist del name ipaddress {ipaddress\&...}\f[R] delete IP address from iplist .TP \f[B]iplist flush name {name\&...}\f[R] delete all IP addresses from iplist .TP \f[B]iplist refresh name {name\&...}\f[R] refresh iplist \[at]name entries now .TP \f[B]set interface {interface} zone {zone}\f[R] change interface to zone .TP \f[B]set interface {interface} zone \-\f[R] remove interface from all zones .SH FILES \f[B]Foomuuri\f[R] reads configuration files from \f[I]/etc/foomuuri/*.conf\f[R]. See full documentation for configuration syntax. .SH AUTHORS Kim B. Heino, b\[at]bbbs.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://github.com/FoobarOy/foomuuri/wiki .UE \c foomuuri-0.27/doc/foomuuri.md000066400000000000000000000050341474611205200162470ustar00rootroot00000000000000--- title: FOOMUURI section: 8 header: User Manual footer: Foomuuri 0.27 date: Jan 28, 2025 --- # NAME foomuuri - multizone bidirectional nftables firewall # SYNOPSIS **foomuuri** [*OPTION*] [*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` : display this help and exit `--version` : output 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 `--set=option=value` : set 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 resolve and iplist refresh **status** : show current status: running, zone-interface mapping **check** : load configuration files and verify syntax **block** : load "block all traffic" ruleset **list** : list active ruleset currently loaded to kernel **list zone-zone {zone-zone...}** : list active ruleset for **zone-zone** currently loaded to kernel **list macro** : list all known macros **list macro keyword {keyword...}** : list all macros with specified name or value **list counter** : list all named counters **list counter keyword {keyword...}** : list named counter with specified name **iplist list** : list entries in all configured iplists and resolves **iplist list name {name...}** : list entries in named iplist/resolve **iplist add name {timeout} ipaddress {ipaddress...}** : add or refresh IP address to iplist **iplist del name ipaddress {ipaddress...}** : delete IP address from iplist **iplist flush name {name...}** : delete all IP addresses from iplist **iplist refresh name {name...}** : refresh iplist @name entries now **set interface {interface} zone {zone}** : change interface to zone **set interface {interface} zone -** : remove interface from all zones # FILES **Foomuuri** reads configuration files from */etc/foomuuri/\*.conf*. See full documentation for configuration syntax. # AUTHORS Kim B. Heino, b@bbbs.net, Foobar Oy # BUG REPORTS Submit bug reports # SEE ALSO Full documentation foomuuri-0.27/doc/monitor-example-command.sh000077500000000000000000000007701474611205200211550ustar00rootroot00000000000000#!/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.27/doc/monitor.event000077500000000000000000000015641474611205200166210ustar00rootroot00000000000000#!/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.27/etc/000077500000000000000000000000001474611205200140645ustar00rootroot00000000000000foomuuri-0.27/etc/50-foomuuri.conf000066400000000000000000000013411474611205200170210ustar00rootroot00000000000000# 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 # Enable packet forwarding # net.ipv4.conf.all.forwarding = 1 # net.ipv6.conf.all.forwarding = 1 foomuuri-0.27/etc/block.fw000066400000000000000000000010301474611205200155060ustar00rootroot00000000000000# "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". 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.27/etc/default.services.conf000066400000000000000000000105421474611205200202030ustar00rootroot00000000000000# 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. 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 amqp tcp 5672 android tcp 5228-5230 4070 4460; udp 5228-5230 2002; https apple tcp 2197 5223; https 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 udp 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-client tcp 24007 49152-60999 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 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 ospf multicast protocol "ospf" daddr 224.0.0.5 224.0.0.6 ff02::5 ff02::6 fe80::/10; multicast protocol "igmp" daddr 224.0.0.5 224.0.0.6 ping icmp echo-request; icmpv6 echo-request pop3s tcp 995 postgresql tcp 5432 pxe udp 4011 razor tcp 2703 redis tcp 6379 redis-sentinel tcp 26379 rdp tcp 3389 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 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.27/etc/static.nft000066400000000000000000000034331474611205200160670ustar00rootroot00000000000000 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.27/firewalld/000077500000000000000000000000001474611205200152625ustar00rootroot00000000000000foomuuri-0.27/firewalld/dbus-firewalld.conf000066400000000000000000000000421474611205200210310ustar00rootroot00000000000000foomuuri { dbus_firewalld yes } foomuuri-0.27/firewalld/fi.foobar.Foomuuri-FirewallD.conf000066400000000000000000000023441474611205200234540ustar00rootroot00000000000000 foomuuri-0.27/src/000077500000000000000000000000001474611205200141005ustar00rootroot00000000000000foomuuri-0.27/src/foomuuri000077500000000000000000004167261474611205200157130ustar00rootroot00000000000000#!/usr/bin/python3 # pylint: disable=too-many-lines """Foomuuri - Multizone bidirectional nftables firewall. Copyright 2023, Kim B. Heino, Foobar Oy License: GPL-2.0-or-later """ import concurrent.futures import datetime 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 time import unicodedata import dbus import dbus.mainloop.glib import dbus.service import requests from gi.repository import GLib # SystemD notify support is optional try: from systemd.daemon import notify HAVE_NOTIFY = True except ImportError: HAVE_NOTIFY = False VERSION = '0.27' CONFIG = { # Parsed foomuuri{} from config files 'log_rate': '1/second burst 3', 'log_input': 'yes', 'log_output': 'yes', 'log_forward': 'yes', 'log_rpfilter': 'yes', 'log_invalid': 'no', 'log_smurfs': 'no', 'log_level': 'level info flags skuid', 'localhost_zone': 'localhost', 'dbus_zone': 'public', 'rpfilter': 'yes', 'counter': 'no', 'set_size': '65535', 'recursion_limit': '65535', 'priority_offset': '5', 'dbus_firewalld': 'no', 'nft_bin': 'nft', # Directories and files. Files are relative to state_dir. 'etc_dir': '/etc/foomuuri', 'share_dir': '/usr/share/foomuuri', 'state_dir': '/var/lib/foomuuri', 'run_dir': '/run/foomuuri', 'good_file': 'good.fw', 'next_file': 'next.fw', 'dbus_file': 'dbus.fw', 'resolve_file': 'resolve.fw', 'iplist_file': 'iplist.fw', 'iplist_manual_file': 'iplist-manual.fw', 'zone_file': 'zone', 'monitor_statistics_file': 'monitor.statistics', # Parsed command line parameters - used internally 'command': '', 'parameters': [], 'root_power': True, 'verbose': 0, 'force': 0, } OUT = [] # Generated nftables ruleset / commands LOGRATES = {} # Lograte names and limits HELPERS = [] # List of helpers: (helper-object, protocol, ports) def fail(error=None): """Exit with error message.""" if error: print(f'Error: {error}') sys.exit(1) def warning(text): """Print warning message.""" if CONFIG['verbose'] >= 0: print(f'Warning: {text}', flush=True) def verbose(line, level=1): """Print line if --verbose was given in command line.""" if CONFIG['verbose'] >= level: print(line, flush=True) def out(line): """Add single line to ruleset.""" OUT.append(line) def run_program_rc(args, *, env=None, print_output=True, quiet=False): """Run external program and return its errorcode. Print its output.""" if not args: return 0 verbose(' '.join(map(str, args))) try: proc = subprocess.run(args, check=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf-8', env=env, timeout=60) except (OSError, subprocess.TimeoutExpired) as error: print(f'Error: Failed to run command "{shlex.join(args)}": {error}', flush=True) return 1 output = proc.stdout.rstrip() if ( output and (proc.returncode or print_output or CONFIG['verbose'] > 0) and not quiet ): print(output, flush=True) return proc.returncode def run_program_output(args, fileline): """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, **kwargs): """Run "nft cmd", wrapper to run_program_rc().""" return run_program_rc(CONFIG['nft_bin'] + [cmd], **kwargs) def nft_json(cmd): """Run "nft --json cmd", return its output as json, ignore errors.""" if not cmd: return {} args = CONFIG['nft_bin'] + ['--json', cmd] verbose(' '.join(map(str, 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: return None try: verbose(proc.stdout, 2) return json.loads(proc.stdout) except json.decoder.JSONDecodeError: return None def shell_expansion(content, fileline): """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_output(shell, fileline)}{postfix}' return content def read_config(): """Read att 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-statements # pylint: disable=no-member # rglob configfiles = (sorted(list(CONFIG['share_dir'].rglob('*.conf'))) + sorted(list(CONFIG['etc_dir'].rglob('*.conf')))) configfiles = [name for name in configfiles if name.name[0] not in ('.', '#')] # There characters will combine to single word in shlex wordchars = ''.join(chr(letter) for letter in range(33, 256) # excludes: " # ' ; { } if letter not in (34, 35, 39, 59, 123, 125)) # Read all config files config = {} # Final config dict section = None # Currently open section name section_line = {} # Section_name -> filename_line for error messages for filename in configfiles: 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 # 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, punctuation_chars=';{') 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] == '}': # End of section if not section: fail(f'{fileline}Extra "}}"') section = None # "foo {" is section start elif len(tokens) == 2 and tokens[1] == '{': if section: fail(f'{fileline}New "{" ".join(tokens)}" while section ' f'"{section}" is still open') section = tokens[0] if section.startswith('_'): # _name is protected fail(f'{fileline}Unknown section: {section}') if section not in config: config[section] = [] section_line[section] = fileline # "template foo {" / "target foo" / "group foo" is section start elif ( len(tokens) == 3 and tokens[0] in ('template', 'target', 'group') and tokens[2] == '{' ): if section: fail(f'{fileline}New "{" ".join(tokens)}" while section ' f'"{section}" is still open') section = f'{tokens[0]} {tokens[1]}' if section not in config: config[section] = [] section_line[section] = fileline # "foo" which is not inside section elif not section: fail(f'{fileline}Unknown line: {" ".join(tokens)}') # "foo" inside section else: config[section].append((fileline, tokens)) # End of file checks if continuation: fail(f'File {filename}: Continuation "\\" at end of file') if section: fail(f'File {filename}: Section "{section}" is missing "}}" at ' f'end of file') # Include section_name -> filename_line to config for error messages config['_section_line'] = section_line return config def config_to_pathlib(fix_files): """Convert str paths in CONFIG{} to pathlib.Paths.""" # "*_dir" are needed for reading config files keys = list(CONFIG) for key in keys: if key.endswith('_dir'): CONFIG[key] = pathlib.Path(CONFIG[key]) # "*_file" are needed to save current state. "state_dir" can be changed # in command line and in foomuuri section in config. Fix "*_file" only # after reading minimal config. if fix_files: for key in keys: if key.endswith('_file'): CONFIG[key] = CONFIG['state_dir'] / CONFIG[key] # "*_bin" are binaries with optional arguments for key in keys: if key.endswith('_bin') and isinstance(CONFIG[key], str): CONFIG[key] = shlex.split(CONFIG[key]) def parse_config_macros(config): """Parse macro{} from config. Recursively expand macro in macro{}.""" # Parse macro{} to dict 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 CONFIG['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. while True: found = False for check, cvalue in macros.items(): for macro, mvalue in macros.items(): try: pos = mvalue.index(check) # Full word expansion only except ValueError: continue if check == macro: # Macro "foo" expands to "foo bar" fail(f'{macroline[macro]}Macro "{macro}" expands to ' f'itself: {" ".join(mvalue)}') # Expand macro macros[macro] = mvalue[:pos] + cvalue + mvalue[pos + 1:] found = True if not found: # No new expansion was done return macros def macro_isdigit(word, separator): """Check if word contains number after separator.""" if word.count(separator) != 1: return False return word.split(separator)[1].isdigit() def expand_single_line(fileline, line, macros): """Expand first macro in line. Repeat call to expand all. Expansion can return multiple lines. Returns None if no expansion was done. """ # 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] == '-': # 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): """Expand all macros in all config sections.""" macros = parse_config_macros(config) for section, orig_lines in config.items(): if section in ('foomuuri', 'zone') or section.startswith('_'): continue # Don't expand in these sections new_lines = [] 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)) config[section] = new_lines def remove_quotes(config): """Change "foo" to foo in config entries. This is called after macro expansion so that '"ssh"' is '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 item.startswith('"') and item.endswith('"'): # "foo" line[index] = item[1:-1] elif item.startswith("'") and item.endswith("'"): # 'foo' line[index] = item[1:-1] def parse_config_foomuuri(config): """Parse foomuuri{} from config to CONFIG{}.""" for fileline, line in config.pop('foomuuri', []): name = line[0] value = ' '.join(line[1:]) if name not in CONFIG: fail(f'{fileline}Unknown foomuuri{{}} option: {" ".join(line)}') if value.startswith('+ '): # append CONFIG[name] = f'{CONFIG[name]} {value[2:]}' else: CONFIG[name] = value # overwrite config_to_pathlib(True) # Paths can be changed in foomuuri{} # Convert chain priority offset to nft. It is already converted on D-Bus # handler reload. if not CONFIG['priority_offset']: priority = 0 else: try: priority = int(CONFIG['priority_offset'].replace(' ', '')) except ValueError: fail(f'Invalid foomuuri{{}} priority_offset: ' f'{CONFIG["priority_offset"]}') if priority == 0: CONFIG['priority_offset'] = '' elif priority > 0: CONFIG['priority_offset'] = f' + {priority}' else: CONFIG['priority_offset'] = f' - {-priority}' # Add "packets" to log rates for key in list(CONFIG): if ( key.startswith('log_') and re.match(r'^\d+/(second|minute|hour) burst \d+$', CONFIG[key]) ): CONFIG[key] += ' packets' def check_name(name, fileline, prefix=''): """Check that name starts with letter, not number.""" if not re.fullmatch(r'[a-zA-Z_]', name[:1]): fail(f'{fileline}Invalid name: {prefix}{name}') def parse_config_zones(config): """Parse zone{} from config.""" zones = {} for fileline, line in config.pop('zone', []): check_name(line[0], fileline) if line[0] in zones: fail(f'{fileline}Zone is already defined: {line[0]}') zones[line[0]] = {'interface': line[1:]} return zones def parse_config_zonemap(config): """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'is a no-op: {" ".join(line)}') zonemap.append(rule) return zonemap def parse_config_rule_section(config, section): """Parse snat{}, dnat{}, prerouting{} etc. rules from config.""" lines = config.pop(section, []) rules = [parse_rule_line(line) for line in lines] return rules def parse_resolve(config, section, timeout=None, refresh=None): """Parse resolve{} and iplist{} from config. Line syntax for resolve: @name fqdn fqdn2 fqdn3 ipv4 ipv6 ip/mask Line syntax for iplist: @name url filename ipv4 ipv6 ip/mask Entry "timeout 10h 30m" (defaults to 24h if "resolve", 10d if "iplist") is how long found IP addresses are remembered. Entry "refresh 3h" is how often iplist entries will be fetched. """ ret = { 'timeout': timeout, 'refresh': refresh, } for fileline, line in config.pop(section, []): if line[0] in ('timeout', 'refresh') and len(line) > 1: ret[line[0]] = ''.join(line[1:]) else: if not line[0].startswith('@') or len(line[0]) == 1: fail(f'{fileline}Invalid {section} name: {" ".join(line)}') check_name(line[0][1:], fileline, '@') for ipv in (4, 6): # "@foo" to "@foo_4" and "@foo_6" name = f'{line[0]}_{ipv}' if len(line) == 1 or line[1:] == ['-']: ret[name] = [] elif line[1] == '+': # append ret[name] = ret.get(name, []) + line[2:] else: # overwrite ret[name] = line[1:] return ret def parse_config_hook(config): """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)}') CONFIG[line[0]] = line[1:] def minimal_config(): """Read and parse minimal config.""" config = read_config() expand_macros(config) remove_quotes(config) parse_config_foomuuri(config) return config def is_ipv4_address(value): """Is value IPv4 address, network or interval.""" if value.count('-') == 1: # Interval "IP-IP" addr_from, addr_to = value.split('-') return is_ipv4_address(addr_from) and is_ipv4_address(addr_to) try: # Address "IP" return isinstance(ipaddress.ip_address(value), ipaddress.IPv4Address) except ValueError: try: # Network "IP/mask" return isinstance(ipaddress.ip_network(value, strict=False), ipaddress.IPv4Network) except ValueError: return False def is_ipv6_address(value): """Is value IPv6 address, network or interval.""" if value.count('/-') == 1: # Suffix mask "IP/-mask" addr, maskstr = value.split('/-') try: mask = int(maskstr) return 0 <= mask <= 128 and is_ipv6_address(addr) except ValueError: return False if value.count('-') == 1: # Interval "IP-IP" addr_from, addr_to = value.split('-') return is_ipv6_address(addr_from) and is_ipv6_address(addr_to) # Python's ipaddress library doesn't handle "[ipv6]" notation. # Strip [] before validating the address. nft handles [] fine, it will # strip them. if value.startswith('['): if value.endswith(']'): # "[ipv6]" value = value[1:-1] elif ']/' in value: # "[ipv6]/56" value = value[1:].replace(']/', '/') try: # Address "IP" return isinstance(ipaddress.ip_address(value), ipaddress.IPv6Address) except ValueError: try: # Network "IP/mask" return isinstance(ipaddress.ip_network(value, strict=False), ipaddress.IPv6Network) except ValueError: return False def is_ip_address(value): """Check if value is IPv4 or IPv6 address. Return 4, 6, or 0 if not detected. """ if value.startswith('-'): # Negative is handled in single_or_set() value = value[1:] if is_ipv4_address(value): return 4 if is_ipv6_address(value): return 6 return 0 def is_port(value, protocol): """Check if value is port: "1", "1-2" or "1,2", or any combination. This is used in parse_rule_line so protocol must specified. Allow special keywords for protocol icmp/icmpv6. """ if not protocol: return False for item in value.split(','): if protocol in ('icmp', 'icmpv6') and ( # See: "nft describe icmp type; nft describe icmpv6 type" item == 'redirect' or re.match(r'^[a-z2]{2,}-[-a-z]{5,}$', item) ): continue for number in item.split('-'): if not number.isnumeric(): return False return True def rule_item_only_one(rule, keyword, value): """Verify that keyword is set only once, or set to same value.""" old = rule.get(f'_only_one_{keyword}', value) if old != value: fail(f'{rule["fileline"]}Rule\'s {keyword} is already set to ' f'"{old}": {" ".join(rule["line"])}') rule[f'_only_one_{keyword}'] = value def verify_rule_sanity(rule, fileline): """Do some basic verify that single rule is valid.""" # pylint: disable=too-many-branches for key, value in rule.items(): if value == '' and key not in ('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 ('global_rate', 'saddr_rate', 'daddr_rate', 'saddr_daddr_rate'): if not rule[key]: continue if re.match(r'^\d+/(second|minute|hour) burst \d+$', rule[key]): rule[key] += ' packets' if not ( re.match(r'^\d+/(second|minute|hour)( burst \d+ packets)?$', rule[key]) or re.match(r'^ct count (over )?\d+$', rule[key]) ): fail(f'{fileline}Invalid "{key}" value: {rule[key]}') for basic, extra in ( ('protocol', 'sport'), ('protocol', 'dport'), ('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 rule['statement'] in ('snat', 'dnat') and not rule['to']: fail(f'{fileline}"{rule["statement"]}" without "to" is not valid') if rule['statement'] == 'masquerade' and rule['to']: fail(f'{fileline}"{rule["statement"]}" with "to" 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): """Parse single config section line to rule dict. 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 ret = { # Basic rules 'statement': 'accept', 'cast': 'unicast', 'protocol': None, 'saddr': None, 'sport': None, 'daddr': None, 'dport': None, 'oifname': None, 'iifname': None, 'mac_saddr': None, 'mac_daddr': None, # Is this IPv4/6 specific rule? 'ipv4': False, 'ipv6': False, # Rate limits 'global_rate': None, 'saddr_rate': None, 'saddr_rate_mask': None, 'saddr_rate_name': None, 'daddr_rate': None, 'daddr_rate_mask': None, 'daddr_rate_name': None, 'saddr_daddr_rate': None, 'saddr_daddr_rate_mask': None, 'saddr_daddr_rate_name': None, # User limits 'uid': None, 'gid': None, # Zonemap specific rules 'szone': None, 'dzone': None, 'new_szone': None, 'new_dzone': None, # Misc rules 'to': None, # snat/dnat to 'queue': None, # optional queue flags 'counter': None, 'helper': None, 'sipsec': None, 'dipsec': None, 'log': None, 'log_level': None, 'nft': None, 'mss': None, 'template': None, 'mark_set': None, 'mark_match': None, 'priority_set': None, 'priority_match': None, 'cgroup': None, 'ct_status': None, 'time': None, 'after_conntrack': True, # Internal housekeeping 'plain': True, # Plain "log" or "counter" without anything else 'fileline': fileline, # For error messages 'line': line, # Original line for error messages } 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" if not keyword and is_port(item, ret['protocol']): keyword = 'dport' ret['plain'] = False if ret[keyword] is None: ret[keyword] = '' # 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 ret[keyword] and keyword not in ('log', 'counter'): ret[keyword] = item if keyword == 'protocol': # Single word only keyword = None # Non-start keywords elif item in ('accept', 'drop', 'return', 'continue', 'masquerade', 'snat', 'dnat'): rule_item_only_one(ret, 'statement', item) ret['statement'] = item ret['plain'] = False keyword = None elif item == 'reject': rule_item_only_one(ret, 'statement', item) ret['statement'] = 'reject with icmpx admin-prohibited' ret['plain'] = False keyword = None elif item in ('multicast', 'broadcast'): rule_item_only_one(ret, 'cast', item) ret['cast'] = item ret['plain'] = False keyword = None elif item in ('tcp', 'udp', 'icmp', 'icmpv6', 'igmp', 'esp'): # "igmp" and "esp" are for backward compability (v0.21) rule_item_only_one(ret, 'protocol', item) ret['protocol'] = item ret['plain'] = False keyword = None elif item in ('ipv4', 'ipv6'): ret[item] = True ret['plain'] = False keyword = None elif item in ('sipsec', 'dipsec'): ret[item] = 'exists' ret['plain'] = False keyword = None elif item in ('-sipsec', '-dipsec'): ret[item[1:]] = 'missing' ret['plain'] = False keyword = None elif item in ('conntrack', '-conntrack'): rule_item_only_one(ret, 'conntrack', item) ret['after_conntrack'] = item == 'conntrack' ret['plain'] = False keyword = None # Start keywords elif item in ('protocol', 'saddr', 'sport', 'daddr', 'dport', 'oifname', 'iifname', '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', 'to', 'counter', 'helper', 'log', 'log_level', 'nft', 'mss', 'template', 'queue', 'mark_set', 'mark_match', 'priority_set', 'priority_match', 'cgroup', 'ct_status', 'time', ): keyword = item if ret[keyword] is None: ret[keyword] = '' if item == 'queue': # statement and start keyword rule_item_only_one(ret, 'statement', item) ret['statement'] = item if item not in ('counter', 'log', 'log_level'): ret['plain'] = False # More parameters for keyword elif keyword: if ret[keyword]: ret[keyword] += ' ' ret[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 ret['plain']: ret['statement'] = 'continue' ret['after_conntrack'] = False verify_rule_sanity(ret, fileline) return ret def parse_config_templates(config): """Parse "template foo { ... }" rules from config.""" templates = {} names = [item for item in config if item.startswith('template ')] for name in names: lines = config.pop(name) templates[name[9:]] = [parse_rule_line(line) for line in lines] return templates def parse_config_rules(config): """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('_'): continue try: szone, dzone = section.split('-') except ValueError: fail(f'{config["_section_line"][section]}Unknown section: ' f'{section}') rules[(szone, dzone)] = [parse_rule_line(line) for line in lines] return rules def filter_any_zonelist(rule, srcdst): """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, rules, szone, dzone): """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, rules): """Insert "any-zone", "zone-any" and "any-any" rules to "zone-zone" rules. These are inserted to beginning of zone-zone rules. """ 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((zone, 'any'), []) # zone-any for dzone in zones: insert_single_any(any_rules, rules, zone, dzone) 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 expand_templates(rules, templates): """Expand rule "template foo" in rules.""" for zonepair in rules: index = 0 while index < len(rules[zonepair]): # Is this rule "template foo"? template = rules[zonepair][index]['template'] if not template: index += 1 continue fileline = rules[zonepair][index]['fileline'] # Replace current rule with template's content if template not in templates: fail(f'{fileline}Unknown template name: {template}') rules[zonepair] = (rules[zonepair][:index] + templates[template] + rules[zonepair][index + 1:]) if len(rules[zonepair]) > int(CONFIG['recursion_limit']): fail(f'{fileline}Possible template loop: ' f'{zonepair[0]}-{zonepair[1]} template {template}') def verify_config(config, zones, rules): """Verify config data.""" if not zones: fail('No zones defined in section zone{}') localhost = CONFIG['localhost_zone'] if localhost not in zones: zones[localhost] = {'interface': []} warning(f'{config["_section_line"]["zone"]}Zone "{localhost}" ' f'is missing from zone{{}}, adding it') if zones[localhost]['interface']: fail(f'{config["_section_line"]["zone"]}Zone "{localhost}" has ' f'interfaces "{" ".join(zones[localhost]["interface"])}", ' 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): """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. 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') else: timeout = '1h' if 'hour' in rule[rate] else '1m' out('flags dynamic,timeout') out(f'timeout {timeout}') out('}') def suffix_mask(value, compare): """Convert suffix mask "IP/-mask" to nft.""" if not compare: compare = '== ' addr, mask = value.split('/-') mask = hex(pow(2, int(mask)) - 1)[2:] # As long hex splitted = '' # Convert to ":1234" parts while mask: splitted = f':{mask[-4:]}{splitted}' mask = mask[:-4] return f'& :{splitted} {compare}{addr}' def single_or_set(data, fileline='', quote=False): """Convert data to single item or set if multiple values.""" # Convert to list if isinstance(data, list): values = data else: values = data.split() # Handle negative: add "!=" to final rule neg = '' for index, value in enumerate(values): if value.startswith('-'): if index and not neg: fail(f'{fileline}Can\'t mix "+" and "-" items: ' f'{" ".join(values)}') neg = '!= ' elif neg: fail(f'{fileline}Can\'t mix "+" and "-" items: {" ".join(values)}') if neg: values[index] = value[1:] # Quote interface names and similar items. "inet" is a reserved word # but can also be an interface name. It must be quoted. if quote: values = [value if value.isnumeric() else f'"{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], neg) return neg + values[0] # nft doesn't support "saddr { @foo, @bar }" if any(value.startswith('@') for value in values): fail(f'{fileline}Only single @list can be used: {" ".join(values)}') # Multiple, use "{set}" return f'{neg}{{ {", ".join(sorted(set(values)))} }}' def netmask_to_and(masklist, ipv, fileline): """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): """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, ipv): """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): """Convert "x" to "x", "x/y" to "mark and ~y or x".""" value = 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): """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: ' f'{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, oldvalue, newvalue): """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, compare, value): """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) or # hh:mm:ss, hh:mm 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): """Convert "time Saturday" to nft, supporting time/day/hour.""" if not rule['time']: return '' compare = '' value = [] ret = '' for item in rule['time'].split(): if item in ('==', '!=', '<', '>', '<=', '>='): 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): """Parse cgroup to nft.""" cgroup = rule['cgroup'] if not cgroup: return '' # cgroupv2: non-numeric, single value only if not re.match(r'[-0-9]', cgroup[0]) and ' ' not in cgroup: level = cgroup.count('/') + 1 return f'socket cgroupv2 level {level} "{cgroup}" ' # cgroup return f'meta cgroup {single_or_set(cgroup, rule["fileline"])} ' def rule_statement(szone, dzone, rule, ipv, *, force_statement=None, do_lograte=True): """Return rule's rate, log and statement as nft command.""" # pylint: disable=too-many-arguments statement = force_statement or 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 counters 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 counterlist = CONFIG['counter'].split() if not rule['nft'] and ( # pylint: disable=too-many-boolean-expressions rule['counter'] is not None or # "counter" in this rule 'yes' in counterlist or # global "yes" f'{szone}-{dzone}' in counterlist or # matching zone-zone f'{szone}-any' in counterlist or f'any-{dzone}' in counterlist ): 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 text is "zone-zone STATEMENT". if rule['log'] is not None or rule['log_level'] is not None: log_prefix = rule['log'] or (f'{szone}-{dzone} ' f'{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(LOGRATES) + 1}' LOGRATES[logname] = (f'{rate}{log_nft}', statement) return f'{prefix}jump {logname}' prefix += f'{log_nft} ' return prefix + statement def output_icmp(szone, dzone, rules, ipv): """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) proto_ports = parse_protocol_ports(rule, ipv, skip_icmp=False) out(f'{proto_ports}{match}{statement}') if ( rule['dport'] and ping not in rule['dport'].split() and 'echo-request' not in rule['dport'].split() ): continue # Continue to next rule if this wasn't ping or match all # This rule was for ping, usually accepting non-flood pings. Add # explicit rule to drop overflow and all other pings. has_ping_rule = True if (match + 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, direction, ipv): """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", used by resolve ips.append(f'{item}_{ipv}') else: ipv_addr = is_ip_address(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: ' f'{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, direction): """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.match(r'^(-)?([0-9a-f]{2}:){5}[0-9a-f]{2}$', item): macs.append(item) else: fail(f'{rule["fileline"]}Invalid MAC address "{item}" in: ' f'{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): """Parse iifname/oifname to nft rule.""" iifname = '' if rule['iifname']: iifname = f'iifname "{rule["iifname"]}" ' oifname = '' if rule['oifname']: oifname = f'oifname "{rule["oifname"]}" ' return iifname + oifname def parse_protocol_ports(rule, ipv, skip_icmp=True): """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 '' if ipv == 6 and protocol == 'igmp': return None # IPv6 uses Multicast Listener Discovery ICMP ports = '' for key in ('sport', 'dport'): if rule[key]: protokey = key if key == 'dport': # Change "dport" to protocol-specific key 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_to(rule, ipv): """Parse snat/dnat "to" rule to nft rule.""" if not rule['to']: return '' if rule['statement'] == 'queue': # "to 3", "to 1-3", "to numgen", ... return f' to {rule["to"]}' # "to" can be IPv4+IPv6, find correct one target = [] for check in rule['to'].split(): if check.count(':') == 1 and is_ip_address(check.split(':')[0]) == 4: check_ipv = 4 # IPv4 address with port elif ( check.startswith('[') and ']:' in check and is_ip_address(check[1:].split(']:')[0]) == 6 ): check_ipv = 6 # IPv6 address with port else: check_ipv = is_ip_address(check) if check_ipv == 0: fail(f'{rule["fileline"]}Invalid IP address in "to": {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 "to" targets: {" ".join(target)}') return f' {"ip" if ipv == 4 else "ip6"} to {target[0]}' def rule_matchers(rule, ipv, *, cast=None, skip_options=True, skip_icmp=True): """Parse rule's matchers to nft rule.""" # pylint: disable=too-many-branches # pylint: disable=too-many-return-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']: 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'meta pkttype {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 ' f'{single_or_set(rule["uid"], rule["fileline"], quote=True)} ') gid = '' if rule['gid']: gid = (f'meta skgid ' f'{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)} ' f'ct mark set meta mark ') if rule['priority_set']: meta_set += f'meta priority set "{rule["priority_set"]}" ' 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) 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}{uid}{gid}{meta_set}') def output_cast(cast, szone, dzone, rules, ipv, *, after_conntrack=True): """Output all uni/multi/broadcast rules for single zone-zone.""" # pylint: disable=too-many-arguments has_mark_restore = 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 match == '' and cast is None and rule['cast'] != 'unicast': continue # Don't convert "multicast accept" to nft "accept" 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') out(f'{match}{statement}') # Does this rule need kernel helper? if rule['helper']: if rule['helper'].count('-') != 1: fail(f'{rule["fileline"]}Invalid helper name: ' f'{rule["helper"]}') HELPERS.append((rule['helper'], rule['protocol'], rule['dport'])) kernelname = rule['helper'].split('-')[0].replace('_', '-') out(f'ct helper \"{kernelname}\" {rule["statement"]}') def output_zonemap(zonemap, szone, dzone, ipv): """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) if match is None: continue out(f'{match}jump {new_szone}-{new_dzone}_{ipv}') def output_options(rules, ipv): """Output "mss" etc options.""" for rule in rules: if rule['mss'] and ipv == 4: match = rule_matchers(rule, ipv, skip_options=False) out(f'{match}tcp flags syn tcp option ' f'maxseg size set {rule["mss"]}') def output_zone(zonemap, szone, dzone, rules, ipv): """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(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. # # "meta pkttype" works only for incoming packets so skip these if # szone=localhost (outgoing). # # "meta pkttype" 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: output_cast('multicast', szone, dzone, rules, ipv) output_cast('broadcast', szone, dzone, rules, ipv) if ipv == 4: out('meta pkttype { broadcast, multicast } drop') else: out('meta pkttype 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 "meta pkttype" # 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, rules): """Output interface verdict maps to jump to correct zone-zone.""" # pylint: disable=too-many-branches # Vmap must have interval-flag if there is wildcard-interface. localhost = CONFIG['localhost_zone'] has_wildcard = False for value in zones.values(): for interface in value['interface']: if '*' in interface: has_wildcard = True # 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['interface']: 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['interface']: 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['interface']: for dinterface in dvalue['interface']: out(f'"{sinterface}" . "{dinterface}" : ' f'jump {szone}-{dzone},') out('}') out('}') def output_zone2zone_rules(rules, zonemap): """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 : jump {szone}-{dzone}_4,') out(f'ipv6 : jump {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, section): """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_to(rule, ipv) if to_rule is None: continue match = rule_matchers(rule, ipv) 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_rule_section(rules, section): """Output snat, dnat, prerouting postrouting, etc. chain + rules.""" # Output-chain must restore mark from conntrack if mark_set was used # for locally generated packets (usually in multi-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: return hooktype = { 'snat': 'type nat hook postrouting priority srcnat', 'dnat': 'type nat hook prerouting priority dstnat', 'prerouting': 'type filter hook prerouting priority mangle', 'postrouting': 'type filter hook postrouting priority mangle', 'forward': 'type filter hook forward priority mangle', 'output': 'type route hook output priority mangle', }.get(section) hooktype += CONFIG['priority_offset'] chain_name = hooktype.split() out(f'chain {chain_name[1]}_{chain_name[3]}_{chain_name[5]} {{') out(hooktype) if need_mark_restore: out('meta mark set ct mark') output_rule_section_no_header(rules, section) out('}') def output_static_chain_logging(chain, statement): """Output logging rules for input/output/forward/invalid/smurfs chains.""" lograte = CONFIG[f'log_{chain}'] # Enabled in foomuuri{} if lograte.lower() == 'no': return if lograte.lower() == 'yes': # "yes" means use standard log rate lograte = CONFIG['log_rate'] flags = ' flags ip options' if chain == 'invalid' else '' 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): """Output generic nft header.""" command_stop(False) # Stop previous foomuuri out('') out('table inet foomuuri {') # Add new foomuuri out('') # Insert include files # pylint: disable=no-member # rglob, read_text for filename in (sorted(list(CONFIG['share_dir'].rglob('*.nft'))) + sorted(list(CONFIG['etc_dir'].rglob('*.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[chain], 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{CONFIG["priority_offset"]}') out('iifname vmap @input_zones') output_static_chain_logging('input', 'drop') out('drop') out('}') out('chain output {') out(f'type filter hook output priority filter{CONFIG["priority_offset"]}') out('oifname vmap @output_zones') # 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('}') out('chain forward {') out(f'type filter hook forward priority filter{CONFIG["priority_offset"]}') out('iifname . oifname vmap @forward_zones') output_static_chain_logging('forward', 'drop') out('drop') out('}') MERGES = [ # Preferred order for rules 'meta pkttype multicast udp dport', 'meta pkttype broadcast udp dport', 'udp dport', 'udp sport', 'tcp dport', 'tcp sport', 'ct helper', ] def merge_accepts(accepts, linenum): """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(): regex = f'^{key} (\\{{ )?([-\\d, ]+)( \\}})? accept$' match = re.match(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 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(): """Optimize ruleset accepts. This will change multiple accepts to single accept using set. """ 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 ')) ): accepts.append(line) del OUT[linenum] else: linenum += merge_accepts(accepts, linenum) + 1 accepts = [] def optimize_jumps(): """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 = LOGRATES.pop(logname) OUT[linenum] = log_nft OUT.insert(linenum + 1, statement) linenum += 1 def optimize_final_rules(): """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(): """Output non-optimized lograte entries as chains.""" for logname, (log_nft, statement) in 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_resolve_sets(resolve, automerge=False): """Output empty resolve{} and iplist{} sets.""" for name in resolve: if not name.startswith('@'): continue out(f'set {name[1:]} {{') out(f'type ipv{name[-1]}_addr') out('flags interval,timeout') if automerge: out('auto-merge') out('}') def output_resolve_elements(resolve, statefile): """Add resolve{} and iplist{} elements from state file to ruleset.""" # Read previous resolve results filename = CONFIG[statefile] try: # pylint: disable=no-member content = filename.read_text(encoding='utf-8') except FileNotFoundError: return except PermissionError as error: fail(f'File {filename}: Can\'t read: {error}') # Known resolve names in current config files known = [name[1:] for name in resolve if name.startswith('@')] if not known: return # Add previous result if resolve name is known out('') now = datetime.datetime.now(datetime.timezone.utc) for line in content.splitlines(): if line.startswith('# '): # Try "didn't exist in active ruleset" now line = line[2:] tokens = line.split() if ( line.startswith('add element inet foomuuri ') and len(tokens) >= 10 and tokens[7] == 'timeout' and tokens[4] in known # It is known name ): # Check optional expire timestamp, used in iplist-manual.fw if len(tokens) == 12 and tokens[10] == '#': try: expire = datetime.datetime.strptime(tokens[11], '%Y-%m-%dT%H:%M:%S') expire = expire.replace(tzinfo=datetime.timezone.utc) except ValueError: continue # Skip expired entries, update timeout for rest seconds = int((expire - now).total_seconds()) if seconds <= 0: continue line = f'{" ".join(tokens[:8])} {seconds}s }}' out(line) def output_named_counters(rules, chain_rules): """Output named counters.""" # Collect all counter names names = set() for rulelist in list(rules.values()) + list(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} {{') out('}') def output_helpers(): """Output helpers.""" # Convert helper list to helper->proto->set(ports) dict helpers = {} for name, proto, ports in 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'{CONFIG["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(): """Prerouting chain to check rpfilter.""" if CONFIG['rpfilter'] == 'no': return out('chain rpfilter {') out(f'type filter hook prerouting priority filter' f'{CONFIG["priority_offset"]}') interfaces = '' if CONFIG['rpfilter'] != 'yes': # Specific interfaces? interfaces = (f'iifname ' f'{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(): """Output generic ruleset footer.""" out('}') def save_file(filename, lines): """Write lines to file.""" try: filename.unlink(missing_ok=True) filename.write_text('\n'.join(lines) + '\n', 'utf-8') filename.chmod(0o600) except PermissionError as error: fail(f'File {filename}: Can\'t write: {error}') except FileNotFoundError: # Simultaneos saves and chmod gives error pass def env_cleanup(text): """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): """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 save_file(filename, lines) def signal_childs(): """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 try: os.kill(pid, signal.SIGHUP) except OSError: pass def apply_final(): """Use final ruleset.""" # Check config if CONFIG['command'] == 'check': if CONFIG['root_power']: # "nft check" requires root ret = run_program_rc(CONFIG['nft_bin'] + ['--check', '--file', CONFIG['next_file']]) else: ret = 0 warning('Not running as "root", skipping "nft check"') if ret: print(f'Error: Nftables failed to check ruleset, error code {ret}') else: print('check success') return ret # Run pre_start / pre_stop hook run_program_rc(CONFIG.get(f'pre_{CONFIG["command"]}')) # Load "next" ret = run_program_rc(CONFIG['nft_bin'] + ['--file', CONFIG['next_file']], print_output=False) # Check failure if ret: print(f'Error: Failed to load ruleset to nftables, error code {ret}') return 1 # Success. Rename "next" to "good", signal dbus to reload and run hook if CONFIG['command'] == 'start': # pylint: disable=no-member CONFIG['good_file'].unlink(missing_ok=True) CONFIG['next_file'].rename(CONFIG['good_file']) signal_childs() run_program_rc(CONFIG.get(f'post_{CONFIG["command"]}')) print(f'{CONFIG["command"]} success') return 0 def command_start(): """Process "start" or "check" command.""" # Read full config config = minimal_config() zones = parse_config_zones(config) zonemap = parse_config_zonemap(config) resolve = parse_resolve(config, 'resolve') iplist = parse_resolve(config, 'iplist') chain_rules = {} for name in ('snat', 'dnat', 'prerouting', 'postrouting', 'forward', 'output', 'invalid', 'rpfilter', 'smurfs'): chain_rules[name] = parse_config_rule_section(config, name) templates = parse_config_templates(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) expand_templates(rules, templates) verify_config(config, zones, rules) # Generate output output_header(chain_rules) output_rate_names(rules) output_zone_vmaps(zones, rules) output_zone2zone_rules(rules, zonemap) for name in ('snat', 'dnat', 'prerouting', 'postrouting', 'forward', 'output'): output_rule_section(chain_rules[name], name) optimize_jumps() optimize_final_rules() optimize_accepts() output_logrates() output_resolve_sets(resolve) output_resolve_sets(iplist, automerge=True) output_named_counters(rules, chain_rules) output_helpers() output_rpfilter() output_footer() output_resolve_elements(resolve, 'resolve_file') output_resolve_elements(iplist, 'iplist_file') output_resolve_elements(iplist, 'iplist_manual_file') # Save known zones to file save_file(CONFIG['zone_file'], zones.keys()) def command_stop(parse_config=True): """Process "stop" command. This will remove all foomuuri rules.""" if parse_config: # Needed for pre_stop and post_stop hooks config = minimal_config() parse_config_hook(config) out('table inet foomuuri') out('delete table inet foomuuri') def command_block(): """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(): """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(): """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() # Running ruleset = nft_json('list table inet foomuuri') if not ruleset: fail('Foomuuri is not running') 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') # Zones if not zone_interface: print() print('Warning: There are no interfaces assigned to any zones') print() print('zone {') for zone in zones: interfaces = [interface for interface, int_zones in zone_interface.items() if zone == int_zones] print(f' {zone:15s} {" ".join(interfaces)}') print('}') return 0 class FoomuuriDbusException(dbus.DBusException): """Exception class for D-Bus interface.""" _dbus_error_name = 'fi.foobar.Foomuuri1.exception' class DbusCommon: """D-Bus server - Common Functions.""" zones = None def set_data(self, zones): """Save config data: zone list is static.""" self.zones = zones @staticmethod def clean_out(): """Remove all entries from current OUT[] variable.""" while OUT: del OUT[0] @staticmethod def apply_out(): """Apply current OUT commands.""" save_final(CONFIG['dbus_file']) run_program_rc(CONFIG['nft_bin'] + ['--file', CONFIG['dbus_file']]) @staticmethod def remove_interface(interface_zone, interface): """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 ' f'{{ "{interface}" : jump {zone}-{CONFIG["localhost_zone"]} }}') out(f'delete element inet foomuuri output_zones ' f'{{ "{interface}" : jump {CONFIG["localhost_zone"]}-{zone} }}') # Remove from forward for other, otherzone in interface_zone.items(): out(f'delete element inet foomuuri forward_zones ' f'{{ "{other}" . "{interface}" : jump {otherzone}-{zone} }}') if other != interface: out(f'delete element inet foomuuri forward_zones ' f'{{ "{interface}" . "{other}" : ' f'jump {zone}-{otherzone} }}') return zone @staticmethod def add_interface(interface_zone, interface, zone): """Add interface to zone. It must be already removed from others.""" # Add to input and output out(f'add element inet foomuuri input_zones ' f'{{ "{interface}" : jump {zone}-{CONFIG["localhost_zone"]} }}') out(f'add element inet foomuuri output_zones ' f'{{ "{interface}" : jump {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} }}') def change_interface_zone(self, interface, new_zone): """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" ' f'to zone "{new_zone}"') return '', '' interface_zone = parse_active_interface_zone() self.clean_out() old_zone = self.remove_interface(interface_zone, interface) if new_zone: self.add_interface(interface_zone, interface, new_zone) self.apply_out() return old_zone, new_zone def parse_default_zone(self, interface, zone): """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['interface']: return key return CONFIG['dbus_zone'] def method_get_zones(self): """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, interface): """Remove interface from zone, or from all if zone is empty. This is currently always handled as "from all". """ print(f'Interface "{interface}" remove from zone "{zone}"', flush=True) return self.change_interface_zone(interface, '')[0] def method_add_interface(self, zone, interface): """Add interface to zone. There can be only one zone per interface so it will be removed from previous zone if needed. """ zone = self.parse_default_zone(interface, zone) print(f'Interface "{interface}" add to zone "{zone}"', flush=True) return self.change_interface_zone(interface, zone)[1] def method_change_zone_of_interface(self, zone, interface): """Change interface to zone.""" zone = self.parse_default_zone(interface, zone) print(f'Interface "{interface}" change to zone "{zone}"', flush=True) return self.change_interface_zone(interface, zone)[0] class DbusFoomuuri(dbus.service.Object, 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') def getZones(self): """Get list of available zones.""" return self.method_get_zones() @dbus.service.method('fi.foobar.Foomuuri1.zone', in_signature='ss', out_signature='s') def removeInterface(self, zone, interface): """Remove interface from zone, or from all if zone is empty. Return: previous zone """ return self.method_remove_interface(zone, interface) @dbus.service.method('fi.foobar.Foomuuri1.zone', in_signature='ss', out_signature='s') def addInterface(self, zone, interface): """Add interface to zone. Return: new zone """ return self.method_add_interface(zone, interface) @dbus.service.method('fi.foobar.Foomuuri1.zone', in_signature='ss', out_signature='s') def changeZoneOfInterface(self, zone, interface): """Change interface to zone. Return: previous zone """ return self.method_change_zone_of_interface(zone, interface) class DbusFirewallD(dbus.service.Object, 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') def getZones(self): """Get list of available zones.""" return self.method_get_zones() @dbus.service.method('org.fedoraproject.FirewallD1.zone', in_signature='ss', out_signature='s') def removeInterface(self, zone, interface): """Remove interface from zone, or from all if zone is empty. Return: previous zone """ return self.method_remove_interface(zone, interface) @dbus.service.method('org.fedoraproject.FirewallD1.zone', in_signature='ss', out_signature='s') def addInterface(self, zone, interface): """Add interface to zone. Return: new zone """ return self.method_add_interface(zone, interface) @dbus.service.method('org.fedoraproject.FirewallD1.zone', in_signature='ss', out_signature='s') def changeZoneOfInterface(self, zone, interface): """Change interface to zone. Return: previous zone """ return self.method_change_zone_of_interface(zone, interface) def command_dbus(): """Start D-Bus daemon.""" CONFIG['keep_going'] = True while CONFIG['keep_going']: # Read minimal config config = minimal_config() zones = parse_config_zones(config) # 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') foomuuri_object.set_data(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 to get rid of pylint except dbus.exceptions.DBusException: fail('Can\'t bind to system D-Bus: ' 'org.federaproject.FirewallD1') firewalld_object = DbusFirewallD(bus, '/org/fedoraproject/FirewallD1') firewalld_object.set_data(zones) # Define reload/stop signal handler mainloop = GLib.MainLoop() def signal_handler(sig, _dummy_frame): if sig == signal.SIGINT: CONFIG['keep_going'] = False if HAVE_NOTIFY: notify('STOPPING=1') elif HAVE_NOTIFY: 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 print('D-Bus handler ready', flush=True) if HAVE_NOTIFY: 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(): """Modify runtime config by calling D-Bus methods.""" if ( len(sys.argv) != 6 or sys.argv[2] != 'interface' or sys.argv[4] != 'zone' ): command_help() try: bus = dbus.SystemBus() obj = bus.get_object('fi.foobar.Foomuuri1', '/fi/foobar/Foomuuri1') if sys.argv[5] in ('', '-'): obj.removeInterface('', sys.argv[3]) else: obj.changeZoneOfInterface(sys.argv[5], sys.argv[3]) except dbus.exceptions.DBusException as error: fail(error) def resolve_one_hostname(hostname): """Resolve hostname and return its IP addresses.""" if is_ip_address(hostname): # Return as is if already IP or IP/mask return (hostname,) ret = set() try: for item in socket.getaddrinfo(hostname, None): if item[4] and item[4][0]: ret.add(item[4][0]) except socket.gaierror: pass return ret def resolve_all_hostnames(resolve): """Return dict of hostname -> set of IP addresses.""" # Collect list of hostnames to resolve todo = set() for hostlist in resolve.values(): todo.update(hostlist) # Resolve them hosts = {} 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): hosts[jobs[future]] = future.result() return hosts def active_sets(): """Return set names in currently active firewall. Return value is dict of "setname -> entries". """ ret = {} data = nft_json('list sets table inet foomuuri') if not data: return ret for item in data['nftables']: if 'set' in item: ret[item['set']['name']] = item['set'].get('elem') return ret def apply_resolve_iplist(filename, error_txt, pending_error): """Save and apply resolve/iplist update.""" if not OUT: return pending_error save_final(CONFIG[filename]) ret = run_program_rc(CONFIG['nft_bin'] + ['--file', CONFIG[filename]]) if ret: print(f'Error: Failed to update {error_txt}, error code {ret}') return ret or pending_error def config_resolve_to_iplist(): """Parse config's resolve{} and return "setname_4 -> iplist" dict.""" config = minimal_config() resolve = parse_resolve(config, 'resolve', '24h') timeout = resolve.pop('timeout') resolve.pop('refresh') hosts = resolve_all_hostnames(resolve) # Convert resolve "@setname_4 -> list of hostnames" to # "setname_4 -> list of IP addresses" current = {} warned = set() for setname, hostlist in resolve.items(): iplist = set() ipv = int(setname[-1]) for hostname in hostlist: if not hosts[hostname] and hostname not in warned: warned.add(hostname) warning(f'No IP address found for hostname ' f'"{hostname}" in resolve "{setname[:-2]}"') for ipaddr in hosts[hostname]: if is_ip_address(ipaddr) == ipv: iplist.add(ipaddr) current[setname[1:]] = sorted(iplist) return current, timeout def warn_once(setname, warned, error_pre, error_post): """Print warning for setname if not already warned.""" if setname[:-2] in warned: return # Already warned print(f'{error_pre} "@{setname[:-2]}" {error_post}', flush=True) warned.add(setname[:-2]) def command_resolve(): """Resolve hostnames.""" # Read config and resolve current, timeout = config_resolve_to_iplist() previous = read_previous_resolve_iplist('resolve_file')[0] known = active_sets() # Output entries warned = set() for setname, addresses in current.items(): for address in addresses: # Is this set active in current ruleset? If not, add entry as # comment if setname not in known: comment = '# ' warn_once(setname, warned, 'Warning: Resolve', 'does not exist in currently active firewall') else: # Success, update elements to ruleset. comment = '' out(f'add element inet foomuuri {setname} {{ {address} }}') out(f'delete element inet foomuuri {setname} {{ {address} }}') out(f'{comment}add element inet foomuuri {setname} ' f'{{ {address} timeout {timeout} }}') # All lookups failed. Add previous entries as comments. if not addresses and setname in previous: for address in sorted(previous[setname]): out(f'# add element inet foomuuri {setname} ' f'{{ {address} timeout {timeout} }}') # Error if set in currently active firewall is / will be empty has_entries = set() for setname, addresses in current.items(): if addresses or known.get(setname): has_entries.add(setname[:-2]) error = 0 warned = set() for setname in current: if setname[:-2] not in has_entries: error = 1 warn_once(setname, warned, 'Error: Resolve', 'is empty') # Apply update return apply_resolve_iplist('resolve_file', 'resolve', error) def get_url(url, setname): """Download URL and return it as text, or None if failure.""" for retry in range(3): if retry: time.sleep(10) try: response = requests.get(url, timeout=50) except OSError as error: err = f'error: {error}' else: if response.status_code == 200: return response.text err = f'status code: {response.status_code}' warning(f'Iplist "@{setname}", can\'t download {url}, {err}') return None def get_file(wildcard, setname): """Read all files and return them as single text.""" wildpath = pathlib.Path(wildcard) filenames = sorted(wildpath.parent.glob(wildpath.name)) if not filenames: warning(f'Iplist "@{setname}", can\'t read file ' f'{wildcard}: No such file') return None text = '' for filename in filenames: try: content = filename.read_text(encoding='utf-8') except PermissionError as error: warning(f'Iplist "@{setname}", can\'t read file ' f'{filename}: {error}') return None text = text + '\n' + content return text def parse_hour_min(timespec, fallback): """Parse 4h3m to seconds. This is a dummy parser without any good error checking. """ if not timespec: return fallback timespec = timespec.replace(' ', '') hours = mins = 0 if 'h' in timespec: hours, timespec = timespec.split('h', 1) if 'm' in timespec: mins, timespec = timespec.split('m', 1) if timespec: return fallback try: return int(hours) * 3600 + int(mins) * 60 except ValueError: return fallback def get_all_urls_and_files(setname, namelist, last_refresh_time, default_refresh): """Read all URLs and filename-wildcards and return them as text.""" # Is it time to update this list? refresh = parse_hour_min(default_refresh, 0) for item in namelist: if item.startswith('refresh='): refresh = parse_hour_min(item[8:], refresh) if not last_refresh_time or CONFIG['force'] >= 0: time_left = 0 else: now = datetime.datetime.now(datetime.timezone.utc) time_left = int((last_refresh_time + datetime.timedelta(seconds=refresh) - now).total_seconds()) if time_left > 0: verbose(f'Iplist "@{setname}" refresh skipped, {time_left} seconds ' f'to next refresh') return '_refresh' # Not yet # It is time. Fetch! text = '' for filename in namelist: if filename.startswith('refresh='): continue if filename.startswith(('https:', 'http:')): content = get_url(filename, setname) elif is_ip_address(filename): content = filename else: content = get_file(filename, setname) if content is None: return None # Return None if any fails text = text + '\n' + content verbose(f'Iplist "@{setname} refreshed') return text def read_previous_resolve_iplist(filename): """Read previous resolve/iplist state file and parse it to dict.""" # Read state file, silently ignore if missing try: # pylint: disable=no-member content = CONFIG[filename].read_text(encoding='utf-8') except FileNotFoundError: return {}, {} except PermissionError as error: fail(f'File {CONFIG["iplist_file"]}: Can\'t read: {error}') # Parse lines to setname->set(ipaddrs) addrs = {} last_refresh = {} for line in content.splitlines(): if line.startswith('# last_refresh'): try: last_refresh[line.split()[2]] = datetime.datetime.strptime( line.split()[3], '%Y-%m-%dT%H:%M:%S').replace( tzinfo=datetime.timezone.utc) except (IndexError, ValueError): pass continue tokens = line.replace('# ', '').split() if len(tokens) != 10 or tokens[7] != 'timeout': continue setname = tokens[4] if setname not in addrs: addrs[setname] = set() addrs[setname].add(tokens[6]) return addrs, last_refresh def read_all_iplists(iplist, last_refresh, default_refresh): """Read all iplist files and return dict setname_ipv->iplist.""" ret = {} for setname, filenames in iplist.items(): # iplist has "name_6" and "name_4" with same content, ignore "_6" if setname.endswith('_6'): continue setname = setname[1:-2] # Strip "@" and "_4" # Get content text = get_all_urls_and_files(setname, filenames, last_refresh.get(setname), default_refresh) if text is None: # Fetch failure continue if text == '_refresh': # It wasn't time to refresh ret[setname] = text continue # Parse each line to IPv4/IPv6 lists addr_4 = set() addr_6 = set() for line in text.splitlines(): # Strip comments and empty lines if '#' in line: line = line.split('#')[0] if ';' in line: line = line.split(';')[0] line = line.strip() if not line: continue # Parse single item per line if is_ipv4_address(line): addr_4.add(line) elif is_ipv6_address(line): addr_6.add(line) else: warning(f'Invalid content in iplist {{ @{setname} }}: {line}') break else: # Include it to reply if all success, update its timestamp ret[f'{setname}_4'] = addr_4 ret[f'{setname}_6'] = addr_6 last_refresh[setname] = datetime.datetime.now( datetime.timezone.utc) return ret def iterate_set_elements(data): """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', 864000) # 10 days else: ipaddr = elem expire = 864000 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"]}/' f'{ipaddr["prefix"]["len"]}', expire) def iplist_manual_state(iplist): """Save manual iplist status to state file. There is no locking so simultaneous add/del can result one missing/extra IP address. Next add/del will fix it. """ manual = [] known = active_sets() for setname, filenames in iplist.items(): # Skip if not manual list, has filenames if filenames or not setname.startswith('@'): continue setname = setname[1:] if setname not in known: continue # Get set entries data = nft_json(f'list set inet foomuuri {setname}') # Collect IP addresses now = datetime.datetime.now(datetime.timezone.utc) for ipaddr, expire in iterate_set_elements(data): stamp = (now + datetime.timedelta(seconds=expire)).strftime( '%Y-%m-%dT%H:%M:%S') manual.append(f'add element inet foomuuri {setname} ' f'{{ {ipaddr} timeout {expire}s }} # {stamp}') # Save manual list save_file(CONFIG['iplist_manual_file'], manual) def command_iplist_list(): """List iplist entries.""" config = minimal_config() known = set() for item in ( list(parse_resolve(config, 'iplist')) + list(parse_resolve(config, 'resolve')) ): if item.startswith('@'): known.add(item[:-2]) ret = 1 # Default to no output, failure for setname in CONFIG['parameters'][1:] or sorted(known): if setname.startswith('@'): setname = setname[1:] if setname.endswith(('_4', '_6')): if f'@{setname[:-2]}' not in known: fail(f'Unknown iplist name: @{setname[:-2]}') data4 = nft_json(f'list set inet foomuuri {setname}') data6 = {} else: if f'@{setname}' not in known: fail(f'Unknown iplist name: @{setname}') data4 = nft_json(f'list set inet foomuuri {setname}_4') data6 = nft_json(f'list set inet foomuuri {setname}_6') elem4 = [ipaddr for ipaddr, _dummy in iterate_set_elements(data4)] elem6 = [ipaddr for ipaddr, _dummy in iterate_set_elements(data6)] elems = sorted(elem4) + sorted(elem6) if not elems: print(f'@{setname}') else: ret = 0 # Outputted something, return ok for elem in elems: print(f'@{setname:20s} {elem}') return ret def command_iplist_add(): """Add entries to iplist.""" config = minimal_config() iplist = parse_resolve(config, 'iplist', '10d') known = set(iplist) | set(parse_resolve(config, 'resolve')) timeout = iplist.pop('timeout') setname = CONFIG['parameters'][1] if setname.startswith('@'): setname = setname[1:] if f'@{setname}_4' not in known: fail(f'Unknown iplist name: @{setname}') ret = 0 for address in CONFIG['parameters'][2:]: ipv = is_ip_address(address) if not ipv: if address[-1] in 'smhd': timeout = address.replace(' ', '') continue fail(f'Invalid IP address {address}') # nftables/kernel workaround to get entry's timeout+expire # values updated. This is not atomic update! # With "auto-merge" enabled this seems to be the only. nft_command(f'delete element inet foomuuri {setname}_{ipv} ' f'{{ {address} }}', quiet=True) ret += nft_command(f'add element inet foomuuri {setname}_{ipv} ' f'{{ {address} timeout {timeout} }}') iplist_manual_state(iplist) return ret def command_iplist_del(): """Delete entries from iplist.""" config = minimal_config() iplist = parse_resolve(config, 'iplist') known = set(iplist) | set(parse_resolve(config, 'resolve')) setname = CONFIG['parameters'][1] if setname.startswith('@'): setname = setname[1:] if f'@{setname}_4' not in known: fail(f'Unknown iplist name: @{setname}') for address in CONFIG['parameters'][2:]: ipv = is_ip_address(address) if not ipv: fail(f'Invalid IP address {address}') nft_command(f'delete element inet foomuuri {setname}_{ipv} ' f'{{ {address} }}', quiet=True) iplist_manual_state(iplist) return 0 def command_iplist_flush(): """Delete all entries from iplist.""" config = minimal_config() iplist = parse_resolve(config, 'iplist') known = set(iplist) | set(parse_resolve(config, 'resolve')) ret = 0 for setname in CONFIG['parameters'][1:]: if setname.startswith('@'): setname = setname[1:] if f'@{setname}_4' not in known: fail(f'Unknown iplist name: @{setname}') for ipv in (4, 6): ret += nft_command(f'flush set inet foomuuri {setname}_{ipv}') iplist_manual_state(iplist) return ret def command_iplist_refresh(): """Refresh iplist{} entries.""" # pylint: disable=too-many-locals # Read minimal config and read iplist{} entries config = minimal_config() iplist = parse_resolve(config, 'iplist', '10d', '24h') timeout = iplist.pop('timeout') refresh = iplist.pop('refresh') previous, last_refresh = read_previous_resolve_iplist('iplist_file') for param in CONFIG['parameters'][1:]: # Refresh parameters now if param.startswith('@'): param = param[1:] last_refresh[param] = None current = read_all_iplists(iplist, last_refresh, refresh) known = active_sets() # Output last_refresh entries for setname, timestamp in last_refresh.items(): if f'@{setname}_4' in iplist and timestamp: out(f'# last_refresh {setname} {timestamp:%Y-%m-%dT%H:%M:%S}') # Output address entries warned = set() error = 0 for setname, filenames in iplist.items(): if not filenames: continue # Don't touch sets without filenames setname = setname[1:] # Strip "@" if current.get(setname[:-2]) == '_refresh': # It wasn't time to refresh comment = '# ' addrlist = previous.get(setname, []) elif setname not in current: # Fetch for this set failed. Add previous fetch as comments error = 1 comment = '# ' addrlist = previous.get(setname, []) warn_once(setname, warned, 'Error: Iplist', 'failed to refresh') elif setname not in known: # Not active in current ruleset, add items as comments comment = '# ' addrlist = current[setname] warn_once(setname, warned, 'Warning: Iplist', 'does not exist in currently active firewall') else: # Success, update elements to ruleset. comment = '' out(f'flush set inet foomuuri {setname}') addrlist = current[setname] for ipaddr in sorted(addrlist): out(f'{comment}add element inet foomuuri {setname} ' f'{{ {ipaddr} timeout {timeout} }}') # Apply update return apply_resolve_iplist('iplist_file', 'iplist', error) def command_iplist(): """Parse "foomuuri iplist" subcommand.""" if len(CONFIG['parameters']) >= 1 and CONFIG['parameters'][0] == 'list': return command_iplist_list() if len(CONFIG['parameters']) >= 3 and CONFIG['parameters'][0] == 'add': return command_iplist_add() if len(CONFIG['parameters']) >= 3 and CONFIG['parameters'][0] == 'del': return command_iplist_del() if len(CONFIG['parameters']) >= 2 and CONFIG['parameters'][0] == 'flush': return command_iplist_flush() if len(CONFIG['parameters']) >= 1 and CONFIG['parameters'][0] == 'refresh': return command_iplist_refresh() return command_help() def command_list(): """List currently active ruleset or other things.""" # List all if not CONFIG['parameters']: return nft_command('list ruleset') # Parse list of possible zone-zone pairs config = minimal_config() zones = parse_config_zones(config) zonepairs = [f'{szone}-{dzone}' for szone in zones for dzone in zones] # Parse parameters list_type = CONFIG['parameters'][0] list_params = CONFIG['parameters'][1:] ret = 0 if list_type == 'macro': # List macros. config{} doesn't have macro{} anymore so config # must be re-read. macros = parse_config_macros(read_config()) print('macro {') for macro in sorted(macros): if ( not list_params or macro in list_params or any(item in list_params for item in macros[macro]) ): print(f' {macro:15s} {" ".join(macros[macro])}') print('}') return 0 if list_type == 'counter': # List named counters if not list_params: ret |= nft_command('list counters table inet foomuuri') else: for counter in list_params: ret |= nft_command(f'list counter inet foomuuri {counter}') return ret # List zone-zone rules for zone in CONFIG['parameters']: if zone not in zonepairs: fail(f'Unknown zone-zone: {zone}') for ipv in (4, 6): ret += nft_command(f'list chain inet foomuuri {zone}_{ipv}') return ret def command_reload(): """Run start and refresh resolve and iplist.""" # Use same args args = [sys.argv[0]] for arg in sys.argv[1:]: if arg.startswith('--'): args.append(arg) # Run commands for sub, fatal in ((['start'], True), (['resolve', 'refresh'], False), (['iplist', 'refresh'], False)): ret = run_program_rc(args + sub) if ret and fatal: return ret return 0 def seconds_to_human(seconds): """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, groups, cfg, grouptarget, name): """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(0, 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' print(f'{grouptarget} {name} changed state to {updown}, {extra}', flush=True) if history: print(f'{grouptarget} {name} history: {history}', flush=True) cfg['state_time'] = now # Run external command if configured. It will receive current state change # event info and all states in environment variables. 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(cfg[f'command_{updown}'], env=env) def monitor_update_groups(targets, groups): """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 and state: cfg['state'] = False monitor_state_command(targets, groups, cfg, 'group', group) elif any_up and not state: cfg['state'] = True monitor_state_command(targets, groups, cfg, 'group', group) def monitor_update_target(targets, groups, target, state): """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: 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) else: # Currently down. Target goes up if: # - history has enough ups # AND # - last n items were up if ( count_up >= cfg['history_up'] and all(cfg['history'][-cfg['consecutive_up']:]) ): cfg['state'] = True monitor_state_command(targets, groups, cfg, 'target', target) def monitor_parse_line(targets, groups, target, line): """Parse result line from monitor's command.""" # Generic "OK" or "ERROR" reply stat_ms = None # Value for statistics, assume error if line in ('OK', 'ERROR'): state = line == 'OK' if state: stat_ms = 1 # There is no time, use 1 else: # fping reply match = re.match(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: try: stat_ms = float(result.split()[2]) except ValueError: pass # 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 monitor_start_targets(targets, groups): """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 # fping requires --loop, add it if missing cmd = cfg['command'] if 'fping' in cmd[0] and '--loop' not in cmd: cmd = cmd[:1] + ['--loop'] + cmd[1:] # Assume it is up on first startup if 'state' not in cfg: cfg['state'] = True # Start command (pylint: disable=consider-using-with) 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: print(f'target {target} monitoring command failed: {error}') 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, groups, target): """Target monitoring command died. Mark it as down.""" # Terminate proc and schedule restart print(f'target {target} monitoring command died, restarting it in 30s', flush=True) 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, groups): """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): """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): """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': [], '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 # Internal items: # 'state': True, # 'state_time': time(), # '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] in ('command', 'command_up', 'command_down'): cfg[line[0]] = line[1:] else: if len(line) != 2: fail(f'{fileline}Can\'t parse line: {" ".join(line)}') if line[0] not in cfg: fail(f'{fileline}Unknown keyword: {" ".join(line)}') try: cfg[line[0]] = int(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] > 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'] - 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, targets): """Parse "group foo { ... }" entries from config.""" groups = {} names = [item for item in config if item.startswith('group ')] for name in names: name = name[6:] groups[name] = { 'target': [], 'command_up': [], 'command_down': [], } fileline = '' for fileline, line in config.pop(f'group {name}'): if line[0] not in groups[name]: fail(f'{fileline}Unknown keyword: {" ".join(line)}') groups[name][line[0]] = line[1:] # Verify section target = groups[name].get('target') if not target: fail(f'{fileline}Missing "target" keyword in "group {name}"') for item in target: if item not in targets: fail(f'{fileline}Undefined target "{item}" in "group {name}"') return groups def monitor_write_statistics(targets): """Write statistics file.""" stats = { 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['history'], # fping time 'time': cfg['time'], } for target, cfg in targets.items() } save_file(CONFIG['monitor_statistics_file'], [json.dumps(stats, indent=2, sort_keys=True)]) def command_monitor(): """Monitor targets and run command when their state changes up or down.""" CONFIG['keep_going'] = 1 while CONFIG['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: if HAVE_NOTIFY: notify('READY=1') notify('STOPPING=1') return 0 # Define reload/stop signal handler def signal_handler(sig, _dummy_frame): if sig == signal.SIGINT: CONFIG['keep_going'] = 0 if HAVE_NOTIFY: notify('STOPPING=1') else: CONFIG['keep_going'] = 2 if HAVE_NOTIFY: notify('RELOADING=1') CONFIG['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 print('Target monitor ready', flush=True) if HAVE_NOTIFY: notify('READY=1') stat_interval = min(cfg['statistics_interval'] for cfg in targets.values()) next_stat = time.time() + stat_interval / 5 while CONFIG['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) monitor_terminate_targets(targets) return 0 def command_help(error=True): """Print command line help.""" # pylint: disable=too-many-statements print(f'Foomuuri {VERSION}') print() print(f'Usage: {sys.argv[0]} {{options}} command') print() print('Available commands:') print() print(' start Load configuration files and generate ruleset') print(' stop Remove ruleset') print(' reload Same as start, followed by resolve and iplist ' 'refresh') print(' status Show current status: running, zone-interface ' 'mapping') print(' check Verify configuration files') print(' block Load "block all traffic" ruleset') print(' list List active ruleset') print(' list zone-zone {zone-zone...}') print(' List active ruleset for zone-zone') print(' list macro List all known macros') print(' list macro keyword {keyword...}') print(' List all macros with specified name or value') print(' list counter List all named counters') print(' list counter keyword {keyword}') print(' List named counter with specified name') print(' iplist list List entries in all configured iplists and ' 'resolves') print(' iplist list name {name...}') print(' List entries in named iplist/resolve') print(' iplist add name {timeout} ipaddress {ipaddress...}') print(' Add or refresh IP address to iplist') print(' iplist del name ipaddress {ipaddress...}') print(' Delete IP address from iplist') print(' iplist flush name {name...}') print(' Delete all IP addresses from iplist') print(' iplist refresh name {name...}') print(' Refresh iplist @name now') print(' set interface {interface} zone {zone}') print(' Change interface to zone') print(' set interface {interface} zone -') print(' Remove interface from all zones') print() print('Available options:') print() print(' --version Print version') print(' --verbose Verbose output') print(' --quiet Be quiet') print(' --force Force some operations, don\'t check anything') print(' --soft Don\'t force operations, check more') print(' --set=option=value') print(' Set config option to value') print() print('Internally used commands:') print() print(' iplist refresh Refresh iplist entries') print(' resolve refresh Refresh resolve hostnames') print(' dbus Start D-Bus daemon') print(' monitor Start target monitor daemon') if error: fail() return 0 def parse_command_line(): """Parse command line to CONFIG[command] and CONFIG[parameters].""" for arg in sys.argv[1:]: if arg == '--help': CONFIG['command'] = 'help' elif arg == '--version': print(VERSION) sys.exit(0) elif arg in ('--verbose', '--force'): CONFIG[arg[2:]] += 1 elif arg == '--quiet': CONFIG['verbose'] -= 1 elif arg == '--soft': CONFIG['force'] -= 1 elif arg.startswith('--set='): if arg.count('=') == 1: fail(f'Invalid syntax for --set=option=value: {arg}') _dummy, option, value = arg.split('=', 2) if option not in CONFIG: fail(f'Unknown foomuuri{{}} option: {arg}') CONFIG[option] = value elif not CONFIG['command']: CONFIG['command'] = arg else: CONFIG['parameters'].append(arg) if not CONFIG['command']: CONFIG['command'] = 'help' config_to_pathlib(False) # Needed to read conf, and --set=x_dir=y def run_command(): """Run CONFIG[command].""" # Only some commands can take arguments if CONFIG['parameters'] and CONFIG['command'] not in ( 'list', 'iplist', 'set', 'resolve', 'help'): return command_help() # Help doesn't need root if CONFIG['command'] == 'help': return command_help(error=False) # Warning if not running as root CONFIG['root_power'] = not os.getuid() if not CONFIG['root_power']: warning('Foomuuri should be run as "root"') # Run simple commands without ruleset output handler = { 'reload': command_reload, 'status': command_status, 'block': command_block, 'list': command_list, 'set': command_set, 'iplist': command_iplist, 'resolve': command_resolve, 'dbus': command_dbus, 'monitor': command_monitor, }.get(CONFIG['command']) if handler: return handler() # Run commands with ruleset output if CONFIG['command'] in ('start', 'check'): command_start() elif CONFIG['command'] == 'stop': command_stop() else: fail(f'Unknown command: {CONFIG["command"]}') # Save and apply changes save_final(CONFIG['next_file']) return apply_final() def main(): """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.27/systemd/000077500000000000000000000000001474611205200150015ustar00rootroot00000000000000foomuuri-0.27/systemd/fi.foobar.Foomuuri1.conf000066400000000000000000000005461474611205200214070ustar00rootroot00000000000000 foomuuri-0.27/systemd/foomuuri-boot.service000066400000000000000000000005431474611205200211730ustar00rootroot00000000000000[Unit] Description=Multizone bidirectional nftables firewall - Early boot Documentation=https://github.com/FoobarOy/foomuuri/wiki 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 foomuuri-0.27/systemd/foomuuri-dbus.service000066400000000000000000000005371474611205200211700ustar00rootroot00000000000000[Unit] Description=Multizone bidirectional nftables firewall - D-Bus handler Documentation=https://github.com/FoobarOy/foomuuri/wiki After=dbus.service After=polkit.service After=foomuuri.service Requires=foomuuri.service PartOf=foomuuri.service Conflicts=firewalld.service [Service] Type=notify ExecStart=foomuuri dbus ExecReload=kill -HUP $MAINPID foomuuri-0.27/systemd/foomuuri-iplist.service000066400000000000000000000003171474611205200215330ustar00rootroot00000000000000[Unit] Description=Multizone bidirectional nftables firewall - Refresh iplist entries Documentation=https://github.com/FoobarOy/foomuuri/wiki [Service] Type=oneshot ExecStart=foomuuri --soft iplist refresh foomuuri-0.27/systemd/foomuuri-iplist.timer000066400000000000000000000003541474611205200212140ustar00rootroot00000000000000[Unit] Description=Multizone bidirectional nftables firewall - Refresh iplist entries Documentation=https://github.com/FoobarOy/foomuuri/wiki Requires=foomuuri.service PartOf=foomuuri.service [Timer] OnActiveSec=3m OnUnitActiveSec=15m foomuuri-0.27/systemd/foomuuri-monitor.service000066400000000000000000000004471474611205200217220ustar00rootroot00000000000000[Unit] Description=Multizone bidirectional nftables firewall - Connectivity monitor Documentation=https://github.com/FoobarOy/foomuuri/wiki After=network-online.target After=foomuuri.service PartOf=foomuuri.service [Service] Type=notify ExecStart=foomuuri monitor ExecReload=kill -HUP $MAINPID foomuuri-0.27/systemd/foomuuri-resolve.service000066400000000000000000000003231474611205200217030ustar00rootroot00000000000000[Unit] Description=Multizone bidirectional nftables firewall - Refresh resolve hostnames Documentation=https://github.com/FoobarOy/foomuuri/wiki [Service] Type=oneshot ExecStart=foomuuri --soft resolve refresh foomuuri-0.27/systemd/foomuuri-resolve.timer000066400000000000000000000003571474611205200213720ustar00rootroot00000000000000[Unit] Description=Multizone bidirectional nftables firewall - Refresh resolve hostnames Documentation=https://github.com/FoobarOy/foomuuri/wiki Requires=foomuuri.service PartOf=foomuuri.service [Timer] OnActiveSec=2m OnUnitActiveSec=15m foomuuri-0.27/systemd/foomuuri.service000066400000000000000000000010071474611205200202260ustar00rootroot00000000000000[Unit] Description=Multizone bidirectional nftables firewall Documentation=https://github.com/FoobarOy/foomuuri/wiki 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 Wants=foomuuri-resolve.timer [Service] Type=oneshot RemainAfterExit=yes ExecStart=foomuuri start ExecReload=foomuuri reload ExecStop=foomuuri stop [Install] WantedBy=multi-user.target foomuuri-0.27/systemd/foomuuri.tmpfilesd000066400000000000000000000000411474611205200205520ustar00rootroot00000000000000d /run/foomuuri 0755 root root - foomuuri-0.27/test/000077500000000000000000000000001474611205200142705ustar00rootroot00000000000000foomuuri-0.27/test/10-host-multizone/000077500000000000000000000000001474611205200175075ustar00rootroot00000000000000foomuuri-0.27/test/10-host-multizone/foomuuri.conf000066400000000000000000000010431474611205200222210ustar00rootroot00000000000000zone { localhost public home } public-localhost { dhcp-client dhcpv6-client ping saddr_rate "5/second burst 20" ssh saddr_rate "5/minute burst 5" drop log } home-localhost { dhcp-client dhcpv6-client lsdp mdns ping ssdp ssh drop log } template outgoing_services { dhcp-server dhcpv6-server domain http https imap ntp ping smtp ssh } localhost-public { template outgoing_services reject log } localhost-home { template outgoing_services googlemeet ipp mdns ssdp reject log } foomuuri-0.27/test/10-host-multizone/golden.txt000066400000000000000000000344131474611205200215250ustar00rootroot00000000000000table 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 : jump public-localhost_4, ipv6 : jump 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 } meta pkttype broadcast udp dport 68 accept ip protocol igmp ip daddr 224.0.0.1 accept meta pkttype { 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 } meta pkttype 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 : jump home-localhost_4, ipv6 : jump 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 meta pkttype multicast ip daddr 224.0.0.251 udp dport 5353 accept meta pkttype multicast ip daddr 224.0.0.251 ip protocol igmp accept meta pkttype multicast ip daddr 239.255.255.250 udp dport 1900 accept meta pkttype multicast ip daddr 239.255.255.250 ip protocol igmp accept meta pkttype broadcast udp dport { 11430, 68 } accept meta pkttype { 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 } meta pkttype multicast ip6 daddr ff02::fb udp dport 5353 accept meta pkttype multicast ip6 daddr ff02::c udp dport 1900 accept meta pkttype 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 : jump localhost-public_4, ipv6 : jump 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 : jump localhost-home_4, ipv6 : jump 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 : jump localhost-localhost_4, ipv6 : jump 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 : jump public-public_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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 : jump public-home_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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 : jump home-public_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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 : jump home-home_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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.27/test/10-host/000077500000000000000000000000001474611205200154635ustar00rootroot00000000000000foomuuri-0.27/test/10-host/foomuuri.conf000066400000000000000000000002111474611205200201710ustar00rootroot00000000000000zone { localhost public } public-localhost { dhcp-client dhcpv6-client ping ssh drop log } localhost-public { accept } foomuuri-0.27/test/10-host/golden.txt000066400000000000000000000161531474611205200175020ustar00rootroot00000000000000table 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 : jump public-localhost_4, ipv6 : jump 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 } meta pkttype broadcast udp dport 68 accept ip protocol igmp ip daddr 224.0.0.1 accept meta pkttype { 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 } meta pkttype 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 : jump localhost-public_4, ipv6 : jump 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 : jump localhost-localhost_4, ipv6 : jump 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 : jump public-public_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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.27/test/10-multi-isp/000077500000000000000000000000001474611205200164315ustar00rootroot00000000000000foomuuri-0.27/test/10-multi-isp/foomuuri.conf000066400000000000000000000062141474611205200211500ustar00rootroot00000000000000# 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.27/test/10-multi-isp/golden.txt000066400000000000000000000362461474611205200204550ustar00rootroot00000000000000table 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 : jump public-localhost_4, ipv6 : jump 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 meta pkttype { 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 } meta pkttype 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 : jump internal-localhost_4, ipv6 : jump 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 } meta pkttype broadcast udp dport 67 accept ip protocol igmp ip daddr 224.0.0.1 accept meta pkttype { 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 } meta pkttype multicast ip6 daddr ff02::1:2 udp sport 546 udp dport 547 accept meta pkttype 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 : jump localhost-public_4, ipv6 : jump 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 : jump internal-public_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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 : jump public-internal_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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 : jump localhost-internal_4, ipv6 : jump 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 : jump localhost-localhost_4, ipv6 : jump 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 : jump public-public_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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 : jump internal-internal_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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.27/test/20-router/000077500000000000000000000000001474611205200160275ustar00rootroot00000000000000foomuuri-0.27/test/20-router/foomuuri.conf000066400000000000000000000063211474611205200205450ustar00rootroot00000000000000zone { 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 to 10.1.0.2 # public -> dmz iifname eth0 http dnat to 10.1.0.2 # public -> dmz iifname eth0 https dnat to 10.1.0.2 # public -> dmz iifname eth0 tcp 111 112 113 dnat to 10.1.0.2 # set of ports iifname eth1 daddr 192.0.2.32 smtp dnat to 10.1.0.2 # internal -> dmz iifname eth1 daddr 192.0.2.32 http dnat to 10.1.0.2 # internal -> dmz iifname eth1 daddr 192.0.2.32 https dnat to 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.27/test/20-router/golden.txt000066400000000000000000000605701474611205200200500ustar00rootroot00000000000000table 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 : jump public-localhost_4, ipv6 : jump 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 meta pkttype { 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 } meta pkttype 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 : jump internal-localhost_4, ipv6 : jump 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 } meta pkttype broadcast udp dport 67 accept ip protocol igmp ip daddr 224.0.0.1 accept meta pkttype { 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 } meta pkttype multicast ip6 daddr ff02::1:2 udp sport 546 udp dport 547 accept meta pkttype 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 : jump dmz-localhost_4, ipv6 : jump 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 } meta pkttype broadcast udp dport 67 accept ip protocol igmp ip daddr 224.0.0.1 accept meta pkttype { 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 } meta pkttype multicast ip6 daddr ff02::1:2 udp sport 546 udp dport 547 accept meta pkttype 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 : jump localhost-public_4, ipv6 : jump 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 : jump internal-public_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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 : jump dmz-public_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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 : jump public-internal_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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 : jump localhost-internal_4, ipv6 : jump 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 : jump dmz-internal_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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 : jump localhost-dmz_4, ipv6 : jump 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 : jump public-dmz_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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 : jump internal-dmz_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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 : jump localhost-localhost_4, ipv6 : jump 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 : jump public-public_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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 : jump internal-internal_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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 : jump dmz-dmz_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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.27/test/50-any/000077500000000000000000000000001474611205200153015ustar00rootroot00000000000000foomuuri-0.27/test/50-any/foomuuri.conf000066400000000000000000000004711474611205200200170ustar00rootroot00000000000000zone { 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.27/test/50-any/golden.txt000066400000000000000000000446301474611205200173210ustar00rootroot00000000000000table 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 : jump localhost-public_4, ipv6 : jump 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 public-public { meta nfproto vmap { ipv4 : jump public-public_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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 foo-public { meta nfproto vmap { ipv4 : jump foo-public_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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 : jump bar-public_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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 public-localhost { meta nfproto vmap { ipv4 : jump public-localhost_4, ipv6 : jump 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 meta pkttype { 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 } meta pkttype 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-foo { meta nfproto vmap { ipv4 : jump public-foo_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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 : jump public-bar_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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-foo { meta nfproto vmap { ipv4 : jump localhost-foo_4, ipv6 : jump 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 : jump localhost-bar_4, ipv6 : jump 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 : jump foo-localhost_4, ipv6 : jump 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 meta pkttype { 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 } meta pkttype 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 : jump foo-foo_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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 : jump foo-bar_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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 : jump bar-localhost_4, ipv6 : jump 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 meta pkttype { 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 } meta pkttype 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 : jump bar-foo_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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 : jump bar-bar_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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 : jump localhost-localhost_4, ipv6 : jump 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.27/test/50-tcp/000077500000000000000000000000001474611205200153005ustar00rootroot00000000000000foomuuri-0.27/test/50-tcp/foomuuri.conf000066400000000000000000000004761474611205200200230ustar00rootroot00000000000000zone { 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.27/test/50-tcp/golden.txt000066400000000000000000000160411474611205200173130ustar00rootroot00000000000000table 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 : jump localhost-public_4, ipv6 : jump 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 : jump public-localhost_4, ipv6 : jump 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 meta pkttype { 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 } meta pkttype multicast drop ip6 nexthdr udp accept drop } chain localhost-localhost { meta nfproto vmap { ipv4 : jump localhost-localhost_4, ipv6 : jump 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 : jump public-public_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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.27/test/60-cgroup/000077500000000000000000000000001474611205200160125ustar00rootroot00000000000000foomuuri-0.27/test/60-cgroup/foomuuri.conf000066400000000000000000000003471474611205200205320ustar00rootroot00000000000000zone { 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.27/test/60-cgroup/golden.txt000066400000000000000000000174321474611205200200320ustar00rootroot00000000000000table 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 : jump localhost-public_4, ipv6 : jump 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 : jump public-localhost_4, ipv6 : jump 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 meta pkttype { 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 } meta pkttype 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 : jump localhost-localhost_4, ipv6 : jump 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 : jump public-public_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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.27/test/60-conntrack/000077500000000000000000000000001474611205200164755ustar00rootroot00000000000000foomuuri-0.27/test/60-conntrack/foomuuri.conf000066400000000000000000000015211474611205200212100ustar00rootroot00000000000000zone { 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 # post-ct: normal rules ssh drop } foomuuri-0.27/test/60-conntrack/golden.txt000066400000000000000000000214111474611205200205050ustar00rootroot00000000000000table 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 : jump localhost-public_4, ipv6 : jump 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 : jump public-localhost_4, ipv6 : jump 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 meta pkttype { broadcast, multicast } drop 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 } meta pkttype multicast drop tcp dport 22 accept drop } chain localhost-localhost { meta nfproto vmap { ipv4 : jump localhost-localhost_4, ipv6 : jump 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 : jump public-public_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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 } 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.27/test/60-queue/000077500000000000000000000000001474611205200156375ustar00rootroot00000000000000foomuuri-0.27/test/60-queue/foomuuri.conf000066400000000000000000000004751474611205200203610ustar00rootroot00000000000000zone { 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.27/test/60-queue/golden.txt000066400000000000000000000166431474611205200176620ustar00rootroot00000000000000table 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 : jump localhost-localhost_4, ipv6 : jump 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 : jump localhost-public_4, ipv6 : jump 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 : jump public-localhost_4, ipv6 : jump 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 meta pkttype { 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 } meta pkttype 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 : jump public-public_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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.27/test/70-macro/000077500000000000000000000000001474611205200156155ustar00rootroot00000000000000foomuuri-0.27/test/70-macro/foomuuri.conf000066400000000000000000000010471474611205200203330ustar00rootroot00000000000000zone { 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:: } dnat { iifname eth4 tcp 123 dnat to single:44 iifname eth6 tcp 123 dnat to [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 } foomuuri-0.27/test/70-macro/golden.txt000066400000000000000000000176251474611205200176410ustar00rootroot00000000000000table 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 : jump localhost-public_4, ipv6 : jump 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 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 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 : jump localhost-localhost_4, ipv6 : jump 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 : jump public-localhost_4, ipv6 : jump 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 meta pkttype { 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 } meta pkttype 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 : jump public-public_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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.27/test/70-mark/000077500000000000000000000000001474611205200154465ustar00rootroot00000000000000foomuuri-0.27/test/70-mark/foomuuri.conf000066400000000000000000000001601474611205200201570ustar00rootroot00000000000000zone { localhost public } forward { iifname eth0 mark_set 42 } localhost-public { ssh mark_match 42 } foomuuri-0.27/test/70-mark/golden.txt000066400000000000000000000171231474611205200174630ustar00rootroot00000000000000table 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 : jump localhost-public_4, ipv6 : jump 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 : jump localhost-localhost_4, ipv6 : jump 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 : jump public-localhost_4, ipv6 : jump 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 meta pkttype { 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 } meta pkttype 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 : jump public-public_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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.27/test/70-priority/000077500000000000000000000000001474611205200163755ustar00rootroot00000000000000foomuuri-0.27/test/70-priority/foomuuri.conf000066400000000000000000000003261474611205200211120ustar00rootroot00000000000000zone { 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.27/test/70-priority/golden.txt000066400000000000000000000171011474611205200204060ustar00rootroot00000000000000table 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 : jump localhost-public_4, ipv6 : jump 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 : jump localhost-localhost_4, ipv6 : jump 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 : jump public-localhost_4, ipv6 : jump 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 meta pkttype { 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 } meta pkttype 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 : jump public-public_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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.27/test/70-time/000077500000000000000000000000001474611205200154525ustar00rootroot00000000000000foomuuri-0.27/test/70-time/foomuuri.conf000066400000000000000000000012131474611205200201630ustar00rootroot00000000000000zone { 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" 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.27/test/70-time/golden.txt000066400000000000000000000213141474611205200174640ustar00rootroot00000000000000table 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 : jump localhost-public_4, ipv6 : jump 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 3005 hour 22:00-03: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 3005 hour 22:00-03: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 : jump localhost-localhost_4, ipv6 : jump 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 : jump public-localhost_4, ipv6 : jump 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 meta pkttype { 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 } meta pkttype 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 : jump public-public_4, ipv6 : jump 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 } meta pkttype { 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 } meta pkttype 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.27/test/Makefile000066400000000000000000000005521474611205200157320ustar00rootroot00000000000000SUBDIRS ?= $(sort $(wildcard ??-*)) .PHONY: all clean distclean $(SUBDIRS) all: $(SUBDIRS) flake8 ../src/foomuuri pycodestyle ../src/foomuuri pylint ../src/foomuuri clean distclean: rm -f */*.fw rm -f */zone $(SUBDIRS): ../src/foomuuri --set=etc_dir=$@ --set=share_dir=../etc --set=state_dir=$@ --set=run_dir=$@ check diff -u $@/golden.txt $@/next.fw