confd: initial zone-based firewall support, based on firewalld

Add supoprt for infix-firewall.yang, modeled on the zone-based firewalld
The terminology is a mix of firewalld, classic netfilter and inspired by
Ubiquity.  E.g., zone 'policy' -> 'action', and the zone matrix overview.

 - Port forwarding allows forwarding a range of ports
 - Operational data comes from firewalld active rules
 - Firewall logging goes to /var/log/firewall.log
 - Show implicit/built-in rules and zones (HOST) in firewall matrix,
   includes "locked" policy for the default-drop behavior
 - The zone services field in admin-exec 'show firewall' shows ANY when
   the zone default action is set to 'accept'
 - Zone 'forwarding' and 'masquerade' settings live in Infix in the
   policys instead, meaning users need to explicitly add a policy
   to allow both intra-zone and inter-zone forwarding
 - Support for emergency lockdown (kill switch)
 - Pre-defined services (xml+enums) are filtered and included as a
   separate YANG model, extensions added for netconf and restconf
 - Includes initial support for firewalld rich rules

firewalld policy rules, including rich rules, have an obnoxious priority
field which is extremely hard to get right, so in Infix we use the far
superior YANG construct 'ordered-by user;'.  This ensure all rules are
generated in that order by setting the priority field, on read-back from
firewalld (operational) the priority field is used to sort the output
of rules in the CLI.

Fixes #448

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2025-10-10 15:14:12 +02:00
parent c4ac9e44a7
commit 3224f49b65
37 changed files with 4035 additions and 17 deletions
@@ -0,0 +1,3 @@
service [2345] <!pid/syslogd> reload:'firewall-cmd -q --reload' \
firewalld --nofork --log-target syslog \
-- Firewall daemon
@@ -0,0 +1,6 @@
# Log firewall denied/rejected packet logs to dedicated file
# https://www.cyberciti.biz/faq/enable-firewalld-logging-for-denied-packets-on-linux/
:msg, contains, "_DROP"
kern.* -/var/log/firewall.log
:msg, contains, "_REJECT"
kern.* -/var/log/firewall.log
+9 -2
View File
@@ -1,11 +1,18 @@
#!/bin/sh
opts="-n1"
if [ "$1" = "-q" ]; then
opts="$opts -s"
shift
fi
Q=$@
/bin/echo -n "$Q, are you sure (y/N)? "
read -n1 yorn
read $opts yorn
echo
if [ x$yorn != "xy" ] && [ x$yorn != "xY" ]; then
if [ "x$yorn" != "xy" ] && [ "x$yorn" != "xY" ]; then
echo "OK, aborting."
exit 1
fi
+1
View File
@@ -73,6 +73,7 @@ BR2_PACKAGE_CONNTRACK_TOOLS=y
BR2_PACKAGE_DNSMASQ=y
BR2_PACKAGE_ETHTOOL=y
BR2_PACKAGE_FPING=y
BR2_PACKAGE_FIREWALL=y
BR2_PACKAGE_FRR=y
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
BR2_PACKAGE_IPERF3=y
+1
View File
@@ -67,6 +67,7 @@ BR2_PACKAGE_AVAHI_DEFAULT_SERVICES=y
BR2_PACKAGE_CHRONY=y
BR2_PACKAGE_DNSMASQ=y
BR2_PACKAGE_ETHTOOL=y
BR2_PACKAGE_FIREWALL=y
BR2_PACKAGE_FRR=y
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
BR2_PACKAGE_IPROUTE2=y
+1
View File
@@ -91,6 +91,7 @@ BR2_PACKAGE_CONNTRACK_TOOLS=y
BR2_PACKAGE_DNSMASQ=y
BR2_PACKAGE_ETHTOOL=y
BR2_PACKAGE_FPING=y
BR2_PACKAGE_FIREWALL=y
BR2_PACKAGE_FRR=y
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
BR2_PACKAGE_IPERF3=y
+1
View File
@@ -86,6 +86,7 @@ BR2_PACKAGE_CONNTRACK_TOOLS=y
BR2_PACKAGE_DNSMASQ=y
BR2_PACKAGE_ETHTOOL=y
BR2_PACKAGE_FPING=y
BR2_PACKAGE_FIREWALL=y
BR2_PACKAGE_FRR=y
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
BR2_PACKAGE_IPERF3=y
+1
View File
@@ -71,6 +71,7 @@ BR2_PACKAGE_CONNTRACK_TOOLS=y
BR2_PACKAGE_DNSMASQ=y
BR2_PACKAGE_ETHTOOL=y
BR2_PACKAGE_FPING=y
BR2_PACKAGE_FIREWALL=y
BR2_PACKAGE_FRR=y
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
BR2_PACKAGE_IPERF3=y
+1
View File
@@ -65,6 +65,7 @@ BR2_PACKAGE_AVAHI_DEFAULT_SERVICES=y
BR2_PACKAGE_CHRONY=y
BR2_PACKAGE_DNSMASQ=y
BR2_PACKAGE_ETHTOOL=y
BR2_PACKAGE_FIREWALL=y
BR2_PACKAGE_FRR=y
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
BR2_PACKAGE_IPROUTE2=y
+50
View File
@@ -1,3 +1,53 @@
* TODO Add support for firewall
- [X] All "implicit" policies in zones are now policies: intra- and inter-zone policies
- [X] Add locked rules for implicit drop/reject policy as last rule in policy (ANY, ANY)
- [X] Firewall logs should show IN=iface (IIF) before SOURCE
- [X] Interfaces are not defaulting to the default zone, must handle bridge
ports and changes to enslavement, so regenerate every time is a must!
- [ ] firewalld helpers -- possibly for conntrack, e.g., ftp
- [ ] With =modprobe br_netfilter= firewalld would see *all* traffic, but there are
issues, <https://github.com/firewalld/firewalld/issues/1236>, and limits to what
seem to be possible atm. You may also need to enable these callbacks:
=echo 1 | sudo tee /proc/sys/net/bridge/bridge-nf-call-iptables=
=echo 1 | sudo tee /proc/sys/net/bridge/bridge-nf-call-ip6tables=
=echo 1 | sudo tee /proc/sys/net/bridge/bridge-nf-call-arptables=
- [X] Add missing upper to port forward since port ranges are supported, see services!
- [X] =[do] show firewall= not available yet
- [X] Add RPC to pause firewall using =firewall-cmd --panic-on= and restart
firewall again with =firewall-cmd --panic-off=. The current state can
be queried using =firewall-cmd --query-panic=, which returns =yes=
- [X] Remove debug log messages!
- [X] Add "Log Messages" section to =show firewall= when =LogDenied ≠ off=
- [ ] Investigate filtering out firewall log messages from other log files
- [1/2] Rename policy->policy to policy->action, and replace allow->forward
- [X] Rename zone->sources to networks
- [X] A zone's action is for ingress, clarify this if missing!
- [X] Any services/ports listed in a zone with policy:accept are a NO-OP
- [X] =firwall-cmd --reload= takes fooooorever! :-(
- [X] If forwarding is disabled in a zone then the zone matrix should
show deny for the same zone-to-zone communication
- [X] We should show the implicit rules for communicating with the HOST
- [X] Investigate "padlock" on built-in policys (and zones?) and expose more?
- [X] Document established,related somewhere, fixed/padlocked policy? Also,
document why this is a good idea to always have enabled. See RH docs.
- [ ] Podman published ports, <https://firewalld.org/2024/11/strict-forward-ports>
- [ ] Software fastpath <https://firewalld.org/2023/05/nftables-flowtable>
- +[ ] Allow overriding/editing immutable policies and zones+
- [X] Add tests: basic (end device), wan-lan, wan-lan-dmz, +hammer (stress)+
- [X] Add documentation
- See <https://docs.rockylinux.org/guides/security/firewalld-beginners/>
- Add some tool tips: nc, nmap, ping, and socat to stress the firewall
- [X] Fix inference so we can remove defaults from factory-config!
- [X] Add iperf service
- [X] Add nftables ownership=yes setting, introduced in later firewalld versions
- [ ] Investigate fail2ban integration with firewalld, for more info, see:
https://github.com/firewalld/firewalld/issues/1466#issuecomment-2773130569
- [ ] Update screenshots for documentation
- [ ] Review both cli-pretty and yanger code
- [ ] Review default-zone handling (needed?)
- [ ] Clean up =INFER_POLICY= ifdefs
- [ ] Revisit built-in fallback zones (public, block, drop)
* TODO doc: User Guide
- Feature set and scope, e.g.
+1
View File
@@ -15,6 +15,7 @@ source "$BR2_EXTERNAL_INFIX_PATH/package/statd/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/factory/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/faux/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/finit/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/firewall/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/greenpak-programmer/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/ifupdown-ng/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/iito/Config.in"
+8
View File
@@ -0,0 +1,8 @@
config BR2_PACKAGE_FIREWALL
bool "firewall"
select BR2_PACKAGE_FIREWALLD
help
Meta pacakge to select firewall related packages for the OS
as well as adapt and integrate properly with the system.
https://github.com/kernelkit/infix
+90
View File
@@ -0,0 +1,90 @@
#!/bin/sh
set -e
TARGET_DIR="$1"
FIREWALL_SERVICES_YANG="$2"
FIREWALL_DAEMON_DIR="${TARGET_DIR}/usr/lib/firewalld"
# Cleanup — remove unnecessary firewalld files and create required directories
cleanup()
{
rm -rf "${TARGET_DIR}/etc/firewall"*
rm -f "${TARGET_DIR}/usr/bin/firewall-applet"
rm -rf "${TARGET_DIR}/usr/share/firewalld"
# Keep only the three zones required by firewalld (core/fw.py)
find "${FIREWALL_DAEMON_DIR}/zones" -type f \
! -name block.xml \
! -name drop.xml \
! -name trusted.xml \
-delete
mkdir -p "${TARGET_DIR}/etc/firewalld/zones"
mkdir -p "${TARGET_DIR}/etc/firewalld/policies"
mkdir -p "${TARGET_DIR}/etc/firewalld/services"
touch "${TARGET_DIR}/etc/firewalld/firewalld.conf"
mkdir -p "${FIREWALL_DAEMON_DIR}/services"
}
# Prune services — keep only those that match YANG enums
prune_services()
{
if [ ! -f "${FIREWALL_SERVICES_YANG}" ]; then
echo "ERROR: ${FIREWALL_SERVICES_YANG} not found"
exit 1
fi
# Extract enum values from YANG model
ENUMS=$(grep 'enum "' "${FIREWALL_SERVICES_YANG}" | \
sed 's/.*enum "\([^"]*\)".*/\1/')
# Validate that all YANG enums have corresponding .xml files
MISSING=0
for service in ${ENUMS}; do
if [ ! -f "${FIREWALL_DAEMON_DIR}/services/${service}.xml" ]; then
echo "Service ${service} is not a known firewalld service"
MISSING=1
fi
done
if [ ${MISSING} -eq 1 ]; then
exit 1
fi
# Remove .xml files that are not in YANG enums
cd "${FIREWALL_DAEMON_DIR}/services/"
for xmlfile in *.xml; do
service="${xmlfile%.xml}"
if ! echo "${ENUMS}" | grep -q "^${service}$"; then
rm "${xmlfile}"
fi
done
}
# Mark built-in zones and policies as immutable
mark_builtins()
{
FIREWALL_XML_FILES="${FIREWALL_DAEMON_DIR}/policies/*.xml ${FIREWALL_DAEMON_DIR}/zones/*.xml"
for xmlfile in ${FIREWALL_XML_FILES}; do
[ -f "${xmlfile}" ] || continue
grep -q "(immutable)" "${xmlfile}" && continue
if grep -q '<short>' "${xmlfile}"; then
sed -i 's|<short>\(.*\)</short>|<short>\1 (immutable)</short>|' \
"${xmlfile}"
else
if echo "${xmlfile}" | grep -q "/policies/"; then
sed -i 's|<policy|<short>(immutable)</short>\n&|' \
"${xmlfile}"
else
sed -i 's|<zone|<short>(immutable)</short>\n&|' \
"${xmlfile}"
fi
fi
done
}
cleanup
prune_services
mark_builtins
+25
View File
@@ -0,0 +1,25 @@
################################################################################
#
# Firewall support
#
################################################################################
FIREWALL_PACKAGE_VERSION = 1.0
FIREWALL_PACKAGE_LICENSE = MIT
FIREWALL_DEPENDENCIES = firewalld
FIREWALL_SERVICES_YANG = $(CONFD_SRCDIR)/yang/confd/infix-firewall-services.yang
# Copy custom service definitions and run finalization script
define FIREWALL_INSTALL_CUSTOM_SERVICES
mkdir -p $(TARGET_DIR)/usr/lib/firewalld/services
cp $(FIREWALL_PKGDIR)/services/*.xml $(TARGET_DIR)/usr/lib/firewalld/services/
endef
define FIREWALL_FINALIZE
$(FIREWALL_PKGDIR)/finalize.sh $(TARGET_DIR) $(FIREWALL_SERVICES_YANG)
endef
FIREWALL_POST_INSTALL_TARGET_HOOKS += FIREWALL_INSTALL_CUSTOM_SERVICES
FIREWALL_TARGET_FINALIZE_HOOKS += FIREWALL_FINALIZE
$(eval $(generic-package))
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<service>
<short>NETCONF</short>
<description>
NETCONF (Network Configuration Protocol) is a protocol for configuration
and monitoring of networked devices. Essentially it can be seen as XML
over SSH, for configuration and state/status, it also support RPC calls
(Remote Procedure Call), e.g., set date-time or reboot device.
</description>
<port protocol="tcp" port="830"/>
</service>
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<service>
<short>RESTCONF</short>
<description>
RESTCONF (RESTful Network Configuration Protocol) is a JSON over
HTTP-based protocol that provides a RESTful API for configuration
and operational data, as well as RPCs. Like NETCONF, but it can
be managed using only curl.
</description>
<port protocol="tcp" port="443"/>
<port protocol="tcp" port="8443"/>
</service>
@@ -0,0 +1,40 @@
diff --git a/src/firewall/core/fw.py b/src/firewall/core/fw.py
index 0a51cfff..8afd6113 100644
--- a/src/firewall/core/fw.py
+++ b/src/firewall/core/fw.py
@@ -223,7 +223,7 @@ class Firewall(object):
self.ipset_backend.set_list()
except ValueError:
if self.nftables_enabled:
- log.info1("ipset not usable, disabling ipset usage in firewall. Other set backends (nftables) remain usable.")
+ log.debug1("ipset not usable, disabling ipset usage in firewall. Other set backends (nftables) remain usable.")
else:
log.warning("ipset not usable, disabling ipset usage in firewall.")
self.ipset_supported_types = [ ]
@@ -240,7 +240,7 @@ class Firewall(object):
"individual calls for IPv4 firewall.")
else:
if self.nftables_enabled:
- log.info1("iptables-restore and iptables are missing, "
+ log.debug1("iptables-restore and iptables are missing, "
"IPv4 direct rules won't be usable.")
else:
log.warning("iptables-restore and iptables are missing, "
@@ -260,7 +260,7 @@ class Firewall(object):
"individual calls for IPv6 firewall.")
else:
if self.nftables_enabled:
- log.info1("ip6tables-restore and ip6tables are missing, "
+ log.debug1("ip6tables-restore and ip6tables are missing, "
"IPv6 direct rules won't be usable.")
else:
log.warning("ip6tables-restore and ip6tables are missing, "
@@ -280,7 +280,7 @@ class Firewall(object):
"individual calls for bridge firewall.")
else:
if self.nftables_enabled:
- log.info1("ebtables-restore and ebtables are missing, "
+ log.debug1("ebtables-restore and ebtables are missing, "
"eb direct rules won't be usable.")
else:
log.warning("ebtables-restore and ebtables are missing, "
+1 -1
View File
@@ -1,4 +1,4 @@
pkglibexec_SCRIPTS = bootstrap error load gen-service gen-hostname \
gen-interfaces gen-motd gen-hardware gen-version \
mstpd-wait-online wait-interface
sbin_SCRIPTS = dagger migrate
sbin_SCRIPTS = dagger migrate firewall
+403
View File
@@ -0,0 +1,403 @@
#!/bin/sh
# Firewall debug and management utility using D-Bus API
#
# SPDX-License-Identifier: BSD-3-Clause
DEST="org.fedoraproject.FirewallD1"
OBJECT="/org/fedoraproject/FirewallD1"
INTERFACE="org.fedoraproject.FirewallD1"
VERBOSE=0
print() {
if [ "$VERBOSE" -eq 1 ]; then
printf '%s\n' "$*"
fi
}
check_firewalld()
{
gdbus call --system --dest "$DEST" --object-path "$OBJECT" \
--method "$INTERFACE.getDefaultZone" >/dev/null 2>&1
}
call_reload()
{
output=$(gdbus call --system --dest "$DEST" --object-path "$OBJECT" \
--method "$INTERFACE.reload" 2>&1)
ret=$?
# Validate both return code and output
if [ $ret -eq 0 ] && [ "$output" = "()" ]; then
return 0
else
print "Error: Reload method failed (exit code: $ret, output: '$output')" >&2
return 1
fi
}
wait_for_reload()
{
timeout_val=$1
timeout "$timeout_val" gdbus monitor --system --dest "$DEST" \
--object-path "$OBJECT" 2>/dev/null | \
while IFS= read line; do
if echo "$line" | grep -q "Reloaded"; then
return 0
fi
done
print "Timeout waiting for firewall reload completion" >&2
return 1
}
gdbus_call()
{
method=$1
result=$(gdbus call --system --dest "$DEST" --object-path "$OBJECT" \
--method "$INTERFACE.$method" 2>/dev/null | \
sed 's/^(//; s/,)$//; s/[(),]//g' | tr -d ' ')
# Check if call succeeded (non-empty result indicates success)
if [ -n "$result" ]; then
echo "$result"
return 0
else
return 1
fi
}
is_panic_enabled()
{
result=$(gdbus_call "queryPanicMode")
if [ $? -eq 0 ] && [ "$result" = "true" ]; then
return 0
fi
return 1
}
panic_on()
{
is_panic_enabled && return 0
if ! gdbus call --system --dest "$DEST" --object-path "$OBJECT" \
--method "$INTERFACE.enablePanicMode" >/dev/null 2>&1; then
print "Error: Failed to activate lockdown mode" >&2
return 1
fi
logger -p user.emerg "LOCKDOWN MODE ACTIVATED - All network traffic blocked"
}
panic_off()
{
is_panic_enabled || return 0
if ! gdbus call --system --dest "$DEST" --object-path "$OBJECT" \
--method "$INTERFACE.disablePanicMode" >/dev/null 2>&1; then
print "Error: Failed to deactivate lockdown mode" >&2
return 1
fi
logger -p user.emerg "LOCKDOWN MODE DEACTIVATED - Normal network operation restored"
}
panic_status()
{
if is_panic_enabled; then
print "Lockdown mode: ACTIVE"
return 0
fi
print "Lockdown mode: INACTIVE"
return 1
}
show_status()
{
echo "=== Firewall Status ==="
if check_firewalld; then
echo " Firewalld : RUNNING"
else
echo "Firewalld NOT RUNNING"
return 1
fi
if is_panic_enabled; then
panic="on"
else
panic="off"
fi
echo " Lockdown Mode : $panic"
default_zone=$(gdbus_call "getDefaultZone" | sed "s/[']//g")
logging=$(gdbus_call "getLogDenied" | sed "s/[']//g")
echo " Default Zone : $default_zone"
echo " Log Denied : $logging"
echo
echo "=== Active Zones ==="
zones_output=$(gdbus call --system --dest "$DEST" --object-path "$OBJECT" \
--method org.fedoraproject.FirewallD1.zone.getActiveZones 2>/dev/null | \
sed 's/^(//; s/,)$//' | tr "'" '"' | sed 's/@as \[\]/[]/g')
if echo "$zones_output" | jq -e . >/dev/null 2>&1; then
echo "$zones_output" | jq -r 'to_entries[] |
" \(.key):" +
(if (.value.interfaces | length) > 0 then "\n Interfaces: " + (.value.interfaces | join(", ")) else "" end) +
(if (.value.sources | length) > 0 then "\n Networks : " + (.value.sources | join(", ")) else "" end) +
(if (.value.interfaces | length) == 0 and (.value.sources | length) == 0 then "\n Members : (none)" else "" end)
' 2>/dev/null || echo " Failed to parse zones"
else
echo " No zones or failed to retrieve"
fi
echo
echo "=== Available Services ==="
services=$(gdbus call --system --dest "$DEST" --object-path "$OBJECT" \
--method "$INTERFACE.listServices" 2>/dev/null | \
sed 's/^(//; s/,)$//' | tr "'" '"')
if echo "$services" | jq -e . >/dev/null 2>&1; then
echo "$services" | jq -r '.[] | " " + .' 2>/dev/null | head -20
count=$(echo "$services" | jq -r '. | length' 2>/dev/null)
if [ "$count" -gt 20 ]; then
echo " ... and $((count - 20)) more"
fi
else
echo " Failed to retrieve services"
fi
echo
echo "=== Policies ==="
policies=$(gdbus call --system --dest "$DEST" --object-path "$OBJECT" \
--method org.fedoraproject.FirewallD1.policy.getPolicies 2>/dev/null | \
sed 's/^(//; s/,)$//' | tr "'" '"')
if echo "$policies" | jq -e . >/dev/null 2>&1; then
policy_count=$(echo "$policies" | jq -r '. | length' 2>/dev/null)
echo " Total policies: $policy_count"
if [ "$policy_count" -gt 0 ]; then
echo "$policies" | jq -r '.[]' 2>/dev/null | while read policy_name; do
echo " Policy: $policy_name"
policy_settings=$(gdbus call --system --dest "$DEST" --object-path "$OBJECT" \
--method org.fedoraproject.FirewallD1.policy.getPolicySettings \
"$policy_name" 2>/dev/null)
if [ -n "$policy_settings" ] && [ "${policy_settings#*Error}" = "$policy_settings" ]; then
target=$(echo "$policy_settings" | grep -o "'target': <'[^']*'" | cut -d"'" -f4)
description=$(echo "$policy_settings" | grep -o "'description': <'[^']*'" | cut -d"'" -f4)
masquerade=$(echo "$policy_settings" | grep -o "'masquerade': <[^>]*>" | sed "s/.*<\([^>]*\)>.*/\1/")
priority=$(echo "$policy_settings" | grep -o "'priority': <[^>]*>" | sed "s/.*<\([^>]*\)>.*/\1/")
if echo "$policy_settings" | grep -q "'ingress_zones'"; then
# Match: 'ingress_zones': <['internal']> or 'ingress_zones': <['dmz', 'internal']>
ingress_zones=$(echo "$policy_settings" | grep -o "'ingress_zones': <\[[^]]*\]>" | sed "s/'ingress_zones': <\[//; s/\]>//; s/'//g" | sed 's/, */, /g')
fi
if echo "$policy_settings" | grep -q "'egress_zones'"; then
# Match: 'egress_zones': <['external']> or 'egress_zones': <['HOST']>
egress_zones=$(echo "$policy_settings" | grep -o "'egress_zones': <\[[^]]*\]>" | sed "s/'egress_zones': <\[//; s/\]>//; s/'//g" | sed 's/, */, /g')
fi
if echo "$policy_settings" | grep -q "'rich_rules'"; then
rich_rules=$(echo "$policy_settings" | grep -o "'rich_rules': <\[[^]]*\]>" | sed "s/'rich_rules': <\[//; s/\]>//")
fi
# Extract port forwarding information
if echo "$policy_settings" | grep -q "'forward_ports'"; then
forward_ports=$(echo "$policy_settings" | grep -o "'forward_ports': <\[[^]]*\]>" | sed "s/'forward_ports': <\[//; s/\]>//")
fi
echo " Target : ${target:-unknown}"
echo " Description: ${description:-none}"
echo " Priority : ${priority:-unknown}"
echo " Ingress : ${ingress_zones:-none}"
echo " Egress : ${egress_zones:-none}"
echo " Masquerade : ${masquerade:-false}"
if [ -n "$rich_rules" ] && [ "$rich_rules" != "" ]; then
rule_count=$(echo "$rich_rules" | grep -o "'" | wc -l)
rule_count=$((rule_count / 2))
if [ "$rule_count" -gt 0 ]; then
echo " Rich Rules ($rule_count):"
# Extract individual rules
echo "$rich_rules" | grep -o "'[^']*'" | sed "s/'//g" | while read rule; do
echo " $rule"
done
else
echo " Rich Rules: none"
fi
else
echo " Rich Rules: none"
fi
# Display port forwarding rules
if [ -n "$forward_ports" ] && [ "$forward_ports" != "" ]; then
echo " Port FWD :"
# Parse forward_ports which contains tuples like ('8080', 'tcp', '80', '10.0.1.100')
# Extract individual port forward entries
echo "$forward_ports" | sed "s/), (/\n/g" | sed "s/^(//; s/)$//" | while IFS= read forward_rule; do
if [ -n "$forward_rule" ]; then
# Parse the tuple: 'from_port', 'protocol', 'to_port', 'to_addr'
from_port=$(echo "$forward_rule" | cut -d',' -f1 | sed "s/'//g" | tr -d ' ')
protocol=$(echo "$forward_rule" | cut -d',' -f2 | sed "s/'//g" | tr -d ' ')
to_port=$(echo "$forward_rule" | cut -d',' -f3 | sed "s/'//g" | tr -d ' ')
to_addr=$(echo "$forward_rule" | cut -d',' -f4 | sed "s/'//g" | tr -d ' ')
if [ -n "$from_port" ] && [ -n "$protocol" ] && [ -n "$to_addr" ] && [ -n "$to_port" ]; then
echo " ${from_port}/${protocol} → ${to_addr}:${to_port}"
else
echo " $forward_rule (unparsed)"
fi
fi
done
else
echo " Port FWD : none"
fi
else
echo " (Failed to get policy details)"
fi
echo
done
fi
else
echo " No policies or failed to retrieve"
fi
# Runtime info
echo "=== Runtime Information ==="
echo " nftables rules:"
rule_count=$(nft list ruleset 2>/dev/null | grep -c "^[[:space:]]*[^#]" || echo "0")
echo " Active rules: $rule_count"
table_count=$(nft list tables 2>/dev/null | wc -l || echo "0")
echo " Active tables: $table_count"
}
# Function to show usage
show_help()
{
cat << EOF
Usage: $0 [OPTIONS] COMMAND
OPTIONS:
--wait SEC Wait for reload completion signal (use with reload command)
-v, --verbose Enable verbose output for error messages and status
-h, --help Show this help message
COMMANDS:
reload Reload firewall configuration
panic OPERATION Emergency panic mode: <on | off | status>
show Show comprehensive firewall status and configuration
help Show this help message
EXAMPLES:
$0 reload Reload firewall (returns immediately)
$0 --wait 30 reload Reload firewall and wait up to 30s for completion
$0 panic on Enable panic mode (blocks ALL traffic)
$0 panic off Disable panic mode
$0 panic status Query current panic status
$0 show Display complete firewall status
This tool uses the FirewallD D-Bus API directly for reliable operation.
EOF
}
main()
{
wait_timeout=""
if ! parsed_args=$(getopt -o hv --long wait:,help,verbose -- "$@"); then
echo "Error parsing options" >&2
exit 1
fi
eval set -- "$parsed_args"
while true; do
case "$1" in
--wait)
wait_timeout="$2"
shift 2
;;
-v|--verbose)
VERBOSE=1
shift
;;
-h|--help)
show_help
exit 0
;;
--)
shift
break
;;
*)
echo "Error: Unknown option '$1'" >&2
exit 1
;;
esac
done
case "${1:-}" in
reload)
if ! check_firewalld; then
echo "Error: firewalld is not running or does not respond!" >&2
exit 1
fi
if ! call_reload; then
exit 1
fi
if [ -n "$wait_timeout" ]; then
if ! wait_for_reload "$wait_timeout"; then
echo "Firewall reload timed out" >&2
exit 1
fi
fi
;;
panic)
if ! check_firewalld; then
echo "Error: firewalld is not running or does not respond" >&2
exit 1
fi
case "${2:-}" in
on)
panic_on
;;
off)
panic_off
;;
status)
panic_status
;;
*)
echo "Error: Invalid panic operation '$2'" >&2
echo "Use: $0 panic {on|off|status}" >&2
exit 1
;;
esac
;;
show)
show_status
;;
help)
show_help
;;
*)
echo "Error: Missing or unknown command '$1'" >&2
echo "Use $0 help for usage information"
exit 1
;;
esac
}
main "$@"
+1
View File
@@ -47,6 +47,7 @@ confd_plugin_la_SOURCES = \
infix-dhcp-client.c \
infix-dhcp-server.c \
infix-factory.c \
infix-firewall.c \
infix-meta.c \
infix-services.c \
infix-system-software.c \
+3
View File
@@ -172,6 +172,9 @@ int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv)
if (rc)
goto err;
rc = ietf_hardware_init(&confd);
if (rc)
goto err;
rc = infix_firewall_init(&confd);
if (rc)
goto err;
+3
View File
@@ -256,4 +256,7 @@ int ietf_hardware_init(struct confd *confd);
/* ietf-keystore.c */
int ietf_keystore_init(struct confd *confd);
/* infix-firewall.c */
int infix_firewall_init(struct confd *confd);
#endif /* CONFD_CORE_H_ */
+740
View File
@@ -0,0 +1,740 @@
/* SPDX-License-Identifier: BSD-3-Clause */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <dirent.h>
#include <srx/common.h>
#include <srx/lyx.h>
#include <srx/srx_val.h>
#include <libyang/libyang.h>
#include "core.h"
#include "ietf-interfaces.h"
#define MODULE "infix-firewall"
#define XPATH "/infix-firewall:firewall"
#define INFER_POLICY 0
#define FIREWALLD_DIR "/etc/firewalld"
#define FIREWALLD_DIR_NEXT "/etc/firewalld+"
#define FIREWALLD_CONF FIREWALLD_DIR_NEXT "/firewalld.conf"
#define FIREWALLD_ZONES_DIR FIREWALLD_DIR_NEXT "/zones"
#define FIREWALLD_SERVICES_DIR FIREWALLD_DIR_NEXT "/services"
#define FIREWALLD_POLICIES_DIR FIREWALLD_DIR_NEXT "/policies"
static struct {
const char *yang;
const char *target;
} zone_action_map[] = {
{ "reject", "%%REJECT%%" },
{ "accept", "ACCEPT" },
{ "drop", "DROP" },
};
static struct {
const char *yang;
const char *target;
} policy_action_map[] = {
{ "continue", "CONTINUE" },
{ "accept", "ACCEPT" },
{ "reject", "REJECT" },
{ "drop", "DROP" },
};
static const char *zone_action_to_target(const char *action)
{
for (size_t i = 0; action && i < NELEMS(zone_action_map); i++) {
if (!strcmp(action, zone_action_map[i].yang))
return zone_action_map[i].target;
}
return zone_action_map[0].yang;
}
static const char *policy_action_to_target(const char *action)
{
for (size_t i = 0; action && i < NELEMS(policy_action_map); i++) {
if (!strcmp(action, policy_action_map[i].yang))
return policy_action_map[i].target;
}
return policy_action_map[0].yang;
}
static void mark_interfaces_used(struct lyd_node *cfg, char **ifaces)
{
struct lyd_node *node;
LYX_LIST_FOR_EACH(lyd_child(cfg), node, "interface") {
const char *ifname = lyd_get_value(node);
for (int i = 0; ifaces[i]; i++) {
if (!strcmp(ifaces[i], ifname)) {
ifaces[i][0] = '\0';
break;
}
}
}
}
static void log_unzoned(const char *name, char **ifaces)
{
size_t num = 0;
for (int i = 0; ifaces && ifaces[i]; i++) {
if (ifaces[i][0] != '\0')
num++;
}
if (num > 0) {
size_t sz = num * 16 + 2 * num + 1;
char buf[sz];
int hit = 0;
memset(buf, 0, sz);
for (int i = 0; ifaces[i]; i++) {
if (ifaces[i][0] == '\0')
continue;
if (hit)
strlcat(buf, ", ", sz);
strlcat(buf, ifaces[i], sz);
hit++;
}
WARN("Adding %zu unassigned interfaces to default zone '%s': %s",
num, name, buf);
}
}
static FILE *open_file(const char *dir, const char *name)
{
FILE *fp;
fp = fopenf("w", "%s/%s.xml", dir, name);
if (!fp) {
ERRNO("Failed creating %s/%s.xml: %s", dir, name, strerror(errno));
return NULL;
}
fprintf(fp, "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
return fp;
}
static int close_file(FILE *fp)
{
fclose(fp);
return SR_ERR_OK;
}
static int delete_file(const char *dir, const char *name)
{
if (erasef("%s/%s.xml", dir, name) && errno != ENOENT) {
ERRNO("Failed deleting %s/%s.xml: %s", dir, name, strerror(errno));
return SR_ERR_SYS;
}
return SR_ERR_OK;
}
static int generate_zone(struct lyd_node *cfg, const char *name, char **ifaces)
{
const char *action, *desc;
struct lyd_node *node;
FILE *fp;
fp = open_file(FIREWALLD_ZONES_DIR, name);
if (!fp)
return SR_ERR_SYS;
action = lydx_get_cattr(cfg, "action");
desc = lydx_get_cattr(cfg, "description");
fprintf(fp, "<zone target=\"%s\">\n", zone_action_to_target(action));
fprintf(fp, " <short>%s</short>\n", name);
if (desc)
fprintf(fp, " <description>%s</description>\n", desc);
LYX_LIST_FOR_EACH(lyd_child(cfg), node, "interface")
fprintf(fp, " <interface name=\"%s\"/>\n", lyd_get_value(node));
if (ifaces) {
for (int i = 0; ifaces[i]; i++) {
if (ifaces[i][0] != '\0') {
fprintf(fp, " <interface name=\"%s\"/>\n", ifaces[i]);
}
}
log_unzoned(name, ifaces);
}
LYX_LIST_FOR_EACH(lyd_child(cfg), node, "network")
fprintf(fp, " <source address=\"%s\"/>\n", lyd_get_value(node));
LYX_LIST_FOR_EACH(lyd_child(cfg), node, "service")
fprintf(fp, " <service name=\"%s\"/>\n", lyd_get_value(node));
LYX_LIST_FOR_EACH(lyd_child(cfg), node, "port-forward") {
const char *lower = lydx_get_cattr(node, "lower");
const char *upper = lydx_get_cattr(node, "upper");
const char *proto = lydx_get_cattr(node, "proto");
struct lyd_node *to = lydx_get_child(node, "to");
if (to) {
const char *to_addr = lydx_get_cattr(to, "addr");
const char *to_port = lydx_get_cattr(to, "port");
if (upper) {
/* Port range */
fprintf(fp, " <forward-port port=\"%s-%s\" protocol=\"%s\"", lower, upper, proto);
if (to_addr)
fprintf(fp, " to-addr=\"%s\"", to_addr);
if (to_port)
fprintf(fp, " to-port=\"%s\"", to_port);
fprintf(fp, "/>\n");
} else {
/* Single port */
fprintf(fp, " <forward-port port=\"%s\" protocol=\"%s\"", lower, proto);
if (to_addr)
fprintf(fp, " to-addr=\"%s\"", to_addr);
if (to_port)
fprintf(fp, " to-port=\"%s\"", to_port);
fprintf(fp, "/>\n");
}
}
}
fprintf(fp, "</zone>\n");
return close_file(fp);
}
static int generate_service(struct lyd_node *cfg, const char *name)
{
const char *desc;
const char *dest;
struct lyd_node *node;
FILE *fp;
fp = open_file(FIREWALLD_SERVICES_DIR, name);
if (!fp)
return SR_ERR_SYS;
desc = lydx_get_cattr(cfg, "description");
dest = lydx_get_cattr(cfg, "destination");
fprintf(fp, "<service>\n");
if (desc)
fprintf(fp, " <short>%s</short>\n", desc);
if (dest)
fprintf(fp, " <destination ipv%s=\"%s\"/>\n", strchr(dest, ':') ? "6" : "4", dest);
LYX_LIST_FOR_EACH(lyd_child(cfg), node, "port") {
const char *lower = lydx_get_cattr(node, "lower");
const char *upper = lydx_get_cattr(node, "upper");
const char *proto = lydx_get_cattr(node, "proto");
if (upper && strcmp(lower, upper))
fprintf(fp, " <port port=\"%s-%s\" protocol=\"%s\"/>\n", lower, upper, proto);
else
fprintf(fp, " <port port=\"%s\" protocol=\"%s\"/>\n", lower, proto);
}
fprintf(fp, "</service>\n");
return close_file(fp);
}
static int generate_policy(struct lyd_node *cfg, const char *name, int *priority)
{
const char *desc, *action;
struct lyd_node *node;
bool masquerade;
FILE *fp;
if (*priority > 0) {
ERROR("Too many policies/filters - exceeded int16 range");
return SR_ERR_SYS;
}
fp = open_file(FIREWALLD_POLICIES_DIR, name);
if (!fp)
return SR_ERR_SYS;
desc = lydx_get_cattr(cfg, "description");
action = lydx_get_cattr(cfg, "action");
masquerade = lydx_is_enabled(cfg, "masquerade");
fprintf(fp, "<policy target=\"%s\" priority=\"%d\">\n",
policy_action_to_target(action), (*priority)++);
if (desc)
fprintf(fp, " <description>%s</description>\n", desc);
LYX_LIST_FOR_EACH(lyd_child(cfg), node, "ingress")
fprintf(fp, " <ingress-zone name=\"%s\"/>\n", lyd_get_value(node));
LYX_LIST_FOR_EACH(lyd_child(cfg), node, "egress")
fprintf(fp, " <egress-zone name=\"%s\"/>\n", lyd_get_value(node));
LYX_LIST_FOR_EACH(lyd_child(cfg), node, "service")
fprintf(fp, " <service name=\"%s\"/>\n", lyd_get_value(node));
/* Handle custom filters */
node = lydx_get_descendant(cfg, "policy", "custom", NULL);
if (node) {
struct lyd_node *filter;
LYX_LIST_FOR_EACH(lyd_child(node), filter, "filter") {
const char *family = lydx_get_cattr(filter, "family");
struct lyd_node *icmp;
if (*priority > 0) {
ERROR("Too many policies/filters - exceeded int16 range");
close_file(fp);
delete_file(FIREWALLD_POLICIES_DIR, name);
return SR_ERR_SYS;
}
if (strcmp(family, "both"))
fprintf(fp, " <rule family=\"%s\" priority=\"%d\">\n",
family, (*priority)++);
else
fprintf(fp, " <rule priority=\"%d\">\n", (*priority)++);
action = lydx_get_cattr(filter, "action");
icmp = lydx_get_descendant(filter, "filter", "icmp", NULL);
if (icmp) {
const char *type = lydx_get_cattr(icmp, "type");
if (strcmp(action, "reject") == 0) {
fprintf(fp, " <icmp-block name=\"%s\"/>\n", type);
} else {
fprintf(fp, " <icmp-type name=\"%s\"/>\n", type);
fprintf(fp, " <%s/>\n", action);
}
}
fprintf(fp, " </rule>\n");
}
}
if (masquerade)
fprintf(fp, " <masquerade/>\n");
fprintf(fp, "</policy>\n");
return close_file(fp);
}
static int generate_firewalld_conf(struct lyd_node *cfg)
{
FILE *fp;
fp = fopen(FIREWALLD_CONF, "w");
if (!fp) {
ERRNO("Failed creating %s", FIREWALLD_CONF);
return SR_ERR_SYS;
}
fprintf(fp, "DefaultZone=%s\n", lydx_get_cattr(cfg, "default"));
fprintf(fp, "LogDenied=%s\n", lydx_get_cattr(cfg, "logging") ?: "off");
fprintf(fp, "FirewallBackend=nftables\n");
fprintf(fp, "IndividualCalls=no\n");
/*
* Set nftables rule set to be owned exclusively by firewalld.
* This prevents other entities from mistakenly (or maliciously)
* modifying firewalld's rule set -- e.g., 'nft flush ruleset'
* will not affect the firewalld rules.
*/
fprintf(fp, "NftablesTableOwner=yes\n");
/* TODO: add config option to enable nftables flowtable (fastpath) */
fprintf(fp, "NftablesFlowtable=off\n");
/* TODO: Add config option to enable this useful debug option. */
fprintf(fp, "NftablesCounters=no\n");
/* Drop all traffic, except established connections, while rules are updated */
fprintf(fp, "ReloadPolicy=INPUT:DROP,FORWARD:DROP,OUTPUT:DROP\n");
fprintf(fp, "FlushAllOnReload=yes\n");
/* Seamless integration with podman -- published ports are opened. */
fprintf(fp, "StrictForwardPorts=no\n");
/* Performs reverse path filtering (RPF) on IPv6 packets as per RFC 3704 */
fprintf(fp, "IPv6_rpfilter=loose-forward\n");
/*
* Filter IPv6 traffic with 6to4 destination addresses that correspond
* to IPv4 addresses that should not be routed over the public internet.
*/
fprintf(fp, "RFC3964_IPv4=yes\n");
/* Remove all firewall rules on exit */
fprintf(fp, "CleanupOnExit=yes\n");
fclose(fp);
return SR_ERR_OK;
}
static int infer_zone(sr_session_ctx_t *session, const char *name, const char *desc,
const char *action, const char *services[])
{
int rc;
DEBUG("Inferring zone %s (%s), action %s", name, desc, action);
rc = srx_set_str(session, desc, 0, XPATH "/zone[name='%s']/description", name);
if (rc)
return rc;
rc = srx_set_str(session, action, 0, XPATH "/zone[name='%s']/action", name);
if (rc)
return rc;
for (int i = 0; services && services[i]; i++) {
rc = srx_set_str(session, services[i], 0, XPATH "/zone[name='%s']/service[.='%s']",
name, services[i]);
if (rc)
return rc;
}
return SR_ERR_OK;
}
#if INFER_POLICY
static int infer_policy(sr_session_ctx_t *session, const char *name, const char *desc,
const char *action, const char *ingress[], const char *egress[],
const char *icmp_types[][4])
{
int rc;
DEBUG("Inferring policy %s (%s), action %s", name, desc, action);
rc = srx_set_str(session, desc, 0, XPATH "/policy[name='%s']/description", name);
if (rc)
return rc;
rc = srx_set_str(session, action, 0, XPATH "/policy[name='%s']/action", name);
if (rc)
return rc;
/* Set ingress zones */
for (int i = 0; ingress && ingress[i]; i++) {
rc = srx_set_str(session, ingress[i], 0, XPATH "/policy[name='%s']/ingress[.='%s']",
name, ingress[i]);
if (rc)
return rc;
}
/* Set egress zones */
for (int i = 0; egress && egress[i]; i++) {
rc = srx_set_str(session, egress[i], 0, XPATH "/policy[name='%s']/egress[.='%s']",
name, egress[i]);
if (rc)
return rc;
}
/* Set custom ICMP filters */
for (int i = 0; icmp_types && icmp_types[i][0]; i++) {
const char *family = icmp_types[i][0];
const char *filter = icmp_types[i][1];
const char *action = icmp_types[i][2];
const char *type = icmp_types[i][3];
rc = srx_set_str(session, family, 0,
XPATH "/policy[name='%s']/custom/filter[name='%s']/family",
name, filter);
if (rc)
return rc;
rc = srx_set_str(session, action, 0,
XPATH "/policy[name='%s']/custom/filter[name='%s']/action",
name, filter);
if (rc)
return rc;
rc = srx_set_str(session, type, 0,
XPATH "/policy[name='%s']/custom/filter[name='%s']/icmp/type",
name, filter);
if (rc)
return rc;
}
return SR_ERR_OK;
}
#endif
static int change(sr_session_ctx_t *session, uint32_t sub_id, const char *module,
const char *xpath, sr_event_t event, unsigned request_id, void *_confd)
{
struct lyd_node *tree, *global;
struct lyd_node *clist, *cnode;
struct lyd_node *diff = NULL;
sr_error_t err = SR_ERR_OK;
sr_data_t *cfg = NULL;
char **ifaces = NULL;
switch (event) {
case SR_EV_CHANGE:
/* Generate configuration to /etc/firewalld+ */
break;
case SR_EV_ABORT:
systemf("rm -rf " FIREWALLD_DIR_NEXT);
return SR_ERR_OK;
case SR_EV_DONE:
if (!fisdir(FIREWALLD_DIR_NEXT)) {
/* Firewall is disabled */
systemf("initctl -nbq disable firewalld");
return SR_ERR_OK;
}
/* Firewall is enabled, roll in new configuration */
systemf("rm -rf " FIREWALLD_DIR);
if (rename(FIREWALLD_DIR_NEXT, FIREWALLD_DIR)) {
ERRNO("Failed rolling in firewalld configuration");
return SR_ERR_SYS;
}
systemf("initctl -nbq touch firewalld");
systemf("initctl -nbq enable firewalld");
return SR_ERR_OK;
default:
return SR_ERR_OK;
}
err = sr_get_data(session, "//.", 0, 0, 0, &cfg);
if (err || !cfg)
return SR_ERR_INTERNAL;
tree = cfg->tree;
global = lydx_get_descendant(tree, "firewall", NULL);
/* Clean up any stale /etc/firewalld+ first */
systemf("rm -rf " FIREWALLD_DIR_NEXT);
/* If firewall is disabled or not enabled, don't generate config */
if (!global || !lydx_is_enabled(global, "enabled")) {
/* Firewall is disabled - no /etc/firewalld+ directory */
goto done;
}
/* Get L3 interfaces for default zone assignment */
if (ietf_interfaces_get_all_l3(tree, &ifaces) != 0) {
ERROR("Failed to get L3 interfaces");
ifaces = NULL;
}
err = srx_get_diff(session, &diff);
if (err)
goto err_release_data;
if (!diff)
goto err_release_data;
/* Create /etc/firewalld+ directory structure */
if (fmkpath(0755, FIREWALLD_DIR_NEXT) ||
fmkpath(0755, FIREWALLD_ZONES_DIR) ||
fmkpath(0755, FIREWALLD_SERVICES_DIR) ||
fmkpath(0755, FIREWALLD_POLICIES_DIR)) {
ERROR("Failed creating " FIREWALLD_DIR_NEXT " directory structure");
err = SR_ERR_SYS;
goto err_release_data;
}
if (lydx_get_descendant(diff, "firewall", "default", NULL) ||
lydx_get_descendant(diff, "firewall", "logging", NULL))
generate_firewalld_conf(global);
/*
* Regenerate everything if anything in firewall changed, firewalld
* handles the 'diff' for us. Starting priority for policies are at
* -14999 because at -15000 is the first "Allow host IPv6" immutable
* (default/built-in) policy from firewalld. We want the user rules
* to be between that and the default 'drop-all' implicit rule.
*/
if (lydx_get_descendant(diff, "firewall", NULL)) {
const char *default_zone = lydx_get_cattr(global, "default");
struct lyd_node *list, *node;
int priority = -14999;
/* First, handle explicit deletions by removing files */
list = lydx_get_descendant(diff, "firewall", "zone", NULL);
LYX_LIST_FOR_EACH(list, node, "zone") {
if (lydx_get_op(node) == LYDX_OP_DELETE)
delete_file(FIREWALLD_ZONES_DIR, lydx_get_cattr(node, "name"));
}
list = lydx_get_descendant(diff, "firewall", "service", NULL);
LYX_LIST_FOR_EACH(list, node, "service") {
if (lydx_get_op(node) == LYDX_OP_DELETE)
delete_file(FIREWALLD_SERVICES_DIR, lydx_get_cattr(node, "name"));
}
list = lydx_get_descendant(diff, "firewall", "policy", NULL);
LYX_LIST_FOR_EACH(list, node, "policy") {
if (lydx_get_op(node) == LYDX_OP_DELETE)
delete_file(FIREWALLD_POLICIES_DIR, lydx_get_cattr(node, "name"));
}
/* Regenerate all non-default zones first */
clist = lydx_get_descendant(tree, "firewall", "zone", NULL);
LYX_LIST_FOR_EACH(clist, cnode, "zone") {
const char *name = lydx_get_cattr(cnode, "name");
/* Skip default zone - we'll do it last */
if (!strcmp(name, default_zone))
continue;
mark_interfaces_used(cnode, ifaces);
generate_zone(cnode, name, NULL);
}
/* Generate default zone last with any unzoned interfaces */
clist = lydx_get_descendant(tree, "firewall", "zone", NULL);
LYX_LIST_FOR_EACH(clist, cnode, "zone") {
const char *name = lydx_get_cattr(cnode, "name");
if (strcmp(name, default_zone))
continue;
mark_interfaces_used(cnode, ifaces);
generate_zone(cnode, name, ifaces);
break;
}
/* Regenerate all services */
clist = lydx_get_descendant(tree, "firewall", "service", NULL);
LYX_LIST_FOR_EACH(clist, cnode, "service")
generate_service(cnode, lydx_get_cattr(cnode, "name"));
/* Regenerate all policies with sequential priority allocation */
clist = lydx_get_descendant(tree, "firewall", "policy", NULL);
LYX_LIST_FOR_EACH(clist, cnode, "policy") {
const char *name = lydx_get_cattr(cnode, "name");
if (generate_policy(cnode, name, &priority)) {
ERROR("Failed to generate policy %s", name);
goto err_release_data;
}
}
}
done:
if (ifaces) {
for (int i = 0; ifaces[i]; i++)
free(ifaces[i]);
free(ifaces);
}
if (diff)
lyd_free_tree(diff);
err_release_data:
if (cfg)
sr_release_data(cfg);
return err;
}
static int cand(sr_session_ctx_t *session, uint32_t sub_id, const char *module,
const char *path, sr_event_t event, unsigned request_id, void *priv)
{
const char *svc[] = {"ssh", "dhcpv6-client", NULL};
#if INFER_POLICY
const char *any[] = {"ANY", NULL};
const char *host[] = {"HOST", NULL};
const char *icmp_types[][4] = {
{"ipv6", "na", "accept", "neighbour-advertisement"},
{"ipv6", "ns", "accept", "neighbour-solicitation"},
{"ipv6", "ra", "accept", "router-advertisement"},
{"ipv6", "re", "accept", "redirect"},
{NULL, NULL, NULL, NULL}
};
#endif
size_t cnt = 0;
int rc;
if (event != SR_EV_UPDATE && event != SR_EV_CHANGE)
return 0;
if (!srx_enabled(session, XPATH "/enabled")) {
DEBUG("Deleted, or not enabled, not inferring anything.");
return 0;
}
/* If unset, this is the first time we're called */
if (srx_get_str(session, XPATH "/default"))
return 0;
rc = srx_nitems(session, &cnt, XPATH "/zones");
if (rc == 0 || cnt) {
WARN("firewall has %zu zone(s) defined, but no default zone! (rc %d)", cnt, rc);
return 0;
}
rc = infer_zone(session, "public", "Public, unknown network. Only SSH and DHCPv6 client allowed.",
"reject", svc);
if (rc)
return rc;
/* Set up default zone for new networks */
rc = srx_set_str(session, "public", 0, XPATH "/default");
if (rc)
return rc;
#if INFER_POLICY
/* Infer allow-host-ipv6 policy */
rc = infer_policy(session, "allow-host-ipv6",
"Allows basic IPv6 functionality for the host.",
"continue", any, host, icmp_types);
if (rc)
return rc;
#endif
return SR_ERR_OK;
}
static int lockdown(sr_session_ctx_t *session, uint32_t sub_id, const char *xpath,
const sr_val_t *input, const size_t input_cnt, sr_event_t event,
uint32_t request_id, sr_val_t **output, size_t *output_cnt, void *priv)
{
const char *operation = input->data.string_val;
int rc;
DEBUG("lockdown-mode: operation = %s", operation);
rc = systemf("firewall panic %s", strcmp(operation, "now") ? "off" : "on");
if (rc) {
ERROR("lockdown-mode: firewall command failed with exit code %d", rc);
return SR_ERR_OPERATION_FAILED;
}
return SR_ERR_OK;
}
int infix_firewall_init(struct confd *confd)
{
int rc;
REGISTER_CHANGE(confd->session, MODULE, XPATH "//.", 0, change, confd, &confd->sub);
REGISTER_CHANGE(confd->cand, MODULE, XPATH "//.", SR_SUBSCR_UPDATE, cand, confd, &confd->sub);
REGISTER_RPC(confd->session, XPATH "/lockdown-mode", lockdown, NULL, &confd->sub);
return SR_ERR_OK;
fail:
ERROR("init failed: %s", sr_strerror(rc));
return rc;
}
+3
View File
@@ -28,6 +28,9 @@ MODULES=(
"infix-dhcp-common@2025-01-29.yang"
"infix-dhcp-client@2025-01-29.yang"
"infix-dhcp-server@2025-01-29.yang"
"infix-firewall@2025-04-26.yang"
"infix-firewall-services@2025-04-26.yang"
"infix-firewall-icmp-types@2025-04-26.yang"
"infix-meta@2024-10-18.yang"
"infix-system@2025-01-25.yang"
"infix-services@2024-12-03.yang"
@@ -0,0 +1,169 @@
module infix-firewall-icmp-types {
yang-version 1.1;
namespace "urn:infix:firewall:icmp-types:ns:yang:1.0";
prefix ifw-icmp;
organization "KernelKit";
contact "kernelkit@googlegroups.com";
description "Common well-defined network services.";
revision 2025-04-26 {
description "Initial revision.";
reference "internal";
}
/* firewall-cmd --get-icmptypes */
typedef type {
description "Available ICMP/ICMPv6 types.";
type enumeration {
enum address-unreachable {
description "Error sent when a packet cannot be delivered to its IPv6 destination address.";
}
enum bad-header {
description "IPv6 error indicating there is a problem with the packet header structure or format.";
}
enum beyond-scope {
description "IPv6 error sent when transmitting a packet would cross a zone boundary of the source address scope.";
}
enum communication-prohibited {
description "Error indicating that communication with the destination has been administratively blocked.";
}
enum destination-unreachable {
description "General error sent by hosts or gateways when a destination cannot be reached.";
}
enum echo-reply {
description "Response message sent back to acknowledge receipt of an echo request (ping response/pong).";
}
enum echo-request {
description "Test message used to check if a host is reachable, commonly sent by the ping utility.";
}
enum failed-policy {
description "IPv6 error indicating the source address failed to meet ingress or egress policy requirements.";
}
enum fragmentation-needed {
description "IPv4 error sent when a packet needs fragmentation but the 'Don't Fragment' flag is set.";
}
enum host-precedence-violation {
description "IPv4 error sent when communication is administratively prohibited due to precedence rules.";
}
enum host-prohibited {
description "IPv4 error indicating that access from a specific host has been administratively blocked.";
}
enum host-redirect {
description "IPv4 message instructing to redirect packets to a different route for the specific host.";
}
enum host-unknown {
description "IPv4 error sent when the destination host cannot be identified or located.";
}
enum host-unreachable {
description "IPv4 error sent when the destination host exists but cannot be reached.";
}
enum ip-header-bad {
description "IPv4 error indicating malformed or corrupted IP header information.";
}
enum mld-listener-done {
description "IPv6 multicast message sent when a host leaves a multicast group.";
}
enum mld-listener-query {
description "IPv6 multicast router query to discover group membership information.";
}
enum mld-listener-report {
description "IPv6 multicast message sent by hosts to report group membership.";
}
enum mld2-listener-report {
description "IPv6 multicast listener report message for MLDv2 enhanced reporting.";
}
enum neighbour-advertisement {
description "IPv6 message sent in response to neighbor solicitation to propagate new network information.";
}
enum neighbour-solicitation {
description "IPv6 message used to discover link-layer addresses of neighbors and verify reachability.";
}
enum network-prohibited {
description "IPv4 error sent when access to an entire network has been administratively blocked.";
}
enum network-redirect {
description "IPv4 message instructing to redirect packets to a different route for the entire network.";
}
enum network-unknown {
description "IPv4 error sent when the destination network cannot be identified or located.";
}
enum network-unreachable {
description "IPv4 error sent when the destination network exists but cannot be reached.";
}
enum no-route {
description "IPv6 error sent when there is no routing table entry available for the destination.";
}
enum packet-too-big {
description "IPv6 error sent by routers when they cannot forward a packet because it exceeds the MTU.";
}
enum parameter-problem {
description "Error sent when IP header contains bad parameters or missing required options.";
}
enum port-unreachable {
description "Error sent when the destination port on a reachable host is not available or not listening.";
}
enum precedence-cutoff {
description "IPv4 error sent when the packet's precedence level is lower than the required minimum.";
}
enum protocol-unreachable {
description "IPv4 error sent when the specified protocol is not supported at the destination.";
}
enum redirect {
description "General message instructing a host to use a different route for future packets.";
}
enum reject-route {
description "IPv6 error sent when the routing table explicitly rejects the route to the destination.";
}
enum required-option-missing {
description "IPv4 error sent when a mandatory IP option is not present in the packet header.";
}
enum router-advertisement {
description "Message sent by routers to periodically announce their presence and network configuration.";
}
enum router-solicitation {
description "Message sent by hosts to request router advertisements and discover available routers.";
}
enum source-quench {
description "IPv4 flow control message telling a host to reduce its packet transmission rate.";
}
enum source-route-failed {
description "IPv4 error sent when source routing specified in the packet cannot be completed.";
}
enum time-exceeded {
description "Error sent when a packet's time-to-live expires during transit or reassembly.";
}
enum timestamp-reply {
description "IPv4 response message containing timestamp information for network time synchronization.";
}
enum timestamp-request {
description "IPv4 message requesting timestamp information from the destination for time synchronization.";
}
enum tos-host-redirect {
description "IPv4 message instructing to redirect packets based on both the type of service and specific host.";
}
enum tos-host-unreachable {
description "IPv4 error sent when a host is unreachable for the specific type of service requested.";
}
enum tos-network-redirect {
description "IPv4 message instructing to redirect packets based on both the type of service and network.";
}
enum tos-network-unreachable {
description "IPv4 error sent when a network is unreachable for the specific type of service requested.";
}
enum ttl-zero-during-reassembly {
description "Error sent when a host fails to completely reassemble fragmented packets within the time limit.";
}
enum ttl-zero-during-transit {
description "Error sent when a packet's time-to-live counter reaches zero while being forwarded.";
}
enum unknown-header-type {
description "IPv6 error sent when an unrecognized Next Header type is encountered in the packet.";
}
enum unknown-option {
description "IPv6 error sent when an unrecognized or unsupported IPv6 option is encountered.";
}
}
}
}
@@ -0,0 +1 @@
infix-firewall-icmp-types.yang
@@ -0,0 +1,386 @@
module infix-firewall-services {
yang-version 1.1;
namespace "urn:infix:firewall:services:ns:yang:1.0";
prefix ifw-svc;
organization "KernelKit";
contact "kernelkit@googlegroups.com";
description "Common well-defined network services.";
revision 2025-04-26 {
description "Initial revision.";
reference "internal";
}
typedef well-known-service {
description "Well-known network services, with standard port assignments from IANA.";
type enumeration {
enum "amqp" {
description "5672/tcp — Advanced Message Queuing Protocol for message-oriented middleware";
}
enum "amqps" {
description "5671/tcp — Secure Advanced Message Queuing Protocol over SSL";
}
enum "apcupsd" {
description "3551/tcp — APC uninterruptible power supply daemon protocol";
}
enum "audit" {
description "60/tcp — Linux audit subsystem for security event logging";
}
enum "bacula" {
description "9101-9103/tcp — Open source network backup tool";
}
enum "bacula-client" {
description "9102/tcp — Bacula client for backup operations";
}
enum "bgp" {
description "179/tcp — Border Gateway Protocol for internet routing";
}
enum "cockpit" {
description "9090/tcp — Web-based server administration interface";
}
enum "dhcp" {
description "67-68/udp — Dynamic Host Configuration Protocol for network configuration";
}
enum "dhcpv6" {
description "547/udp — Allow incoming DHCP for IPv6 requests from clients or relay agents.";
}
enum "dhcpv6-client" {
description "546/udp — Allow a DHCP for IPv6 client to obtain a lease.";
}
enum "dns" {
description "53/tcp+udp — Domain Name System for name resolution";
}
enum "dns-over-quic" {
description "853/udp — DNS over QUIC protocol for encrypted domain name resolution";
}
enum "dns-over-tls" {
description "853/tcp — DNS over TLS protocol for encrypted domain name resolution";
}
enum "elasticsearch" {
description "9300/tcp — Distributed search and analytics engine";
}
enum "ftp" {
description "20-21/tcp — File Transfer Protocol for file transfers";
}
enum "git" {
description "9418/tcp — Git daemon for version control repository access";
}
enum "grafana" {
description "3000/tcp — Analytics and monitoring dashboard platform";
}
enum "gpsd" {
description "2947/tcp — GPS daemon for location services";
}
enum "gre" {
description "47/ip — Generic Routing Encapsulation for tunneling";
}
enum "http" {
description "80/tcp — Hypertext Transfer Protocol for web traffic";
}
enum "https" {
description "443/tcp — Secure Hypertext Transfer Protocol for encrypted web traffic";
}
enum "http3" {
description "443/udp — HTTP/3 protocol over QUIC for faster web traffic";
}
enum "imap" {
description "143/tcp — Internet Message Access Protocol for email access";
}
enum "imaps" {
description "993/tcp — Secure Internet Message Access Protocol for encrypted email access";
}
enum "ipp" {
description "631/tcp+udp — Internet Printing Protocol (IPP) is used for distributed printing.";
}
enum "iperf2" {
description "5001/tcp — Network bandwidth measurement tool version 2";
}
enum "iperf3" {
description "5201/tcp — Network bandwidth measurement tool version 3";
}
enum "ipsec" {
description "500/udp — Internet Protocol Security for VPN connections";
}
enum "irc" {
description "6667/tcp — Internet Relay Chat for text messaging";
}
enum "ircs" {
description "6697/tcp — Secure Internet Relay Chat over SSL";
}
enum "jenkins" {
description "8080/tcp — Open source automation server for CI/CD";
}
enum "kerberos" {
description "88/tcp+udp — Network authentication protocol";
}
enum "kadmin" {
description "749/tcp — Kerberos administration server";
}
enum "kibana" {
description "5601/tcp — Data visualization dashboard for Elasticsearch";
}
enum "klogin" {
description "543/tcp — Kerberos remote login";
}
enum "kpasswd" {
description "464/tcp+udp — Kerberos password changing protocol";
}
enum "kprop" {
description "754/tcp — Kerberos database propagation";
}
enum "kshell" {
description "544/tcp — Kerberos remote shell";
}
enum "ldap" {
description "389/tcp — Lightweight Directory Access Protocol for directory services";
}
enum "ldaps" {
description "636/tcp — Lightweight Directory Access Protocol over SSL";
}
enum "libvirt" {
description "16509/tcp — Virtualization management daemon";
}
enum "libvirt-tls" {
description "16514/tcp — Secure virtualization management over TLS";
}
enum "llmnr" {
description "5355/tcp+udp — Link-Local Multicast Name Resolution";
}
enum "llmnr-client" {
description "5355/udp — LLMNR client for name resolution";
}
enum "llmnr-tcp" {
description "5355/tcp — LLMNR over TCP";
}
enum "llmnr-udp" {
description "5355/udp — LLMNR over UDP";
}
enum "matrix" {
description "8008/tcp — Matrix chat protocol server";
}
enum "mdns" {
description "5353/udp — Multicast DNS for local network service discovery";
}
enum "mongodb" {
description "27017/tcp — Document-oriented NoSQL database";
}
enum "minecraft" {
description "25565/tcp — Minecraft game server";
}
enum "minidlna" {
description "8200/tcp — Lightweight DLNA/UPnP media server";
}
enum "mndp" {
description "5678/udp — MikroTik Neighbor Discovery Protocol";
}
enum "mosh" {
description "60000-61000/udp — Mobile shell for remote terminal access";
}
enum "mpd" {
description "6600/tcp — Music Player Daemon";
}
enum "mqtt" {
description "1883/tcp — Message Queuing Telemetry Transport for IoT";
}
enum "mqtt-tls" {
description "8883/tcp — Secure MQTT over TLS for IoT";
}
enum "mssql" {
description "1433/tcp — Microsoft SQL Server database";
}
enum "mysql" {
description "3306/tcp — MySQL database server connections";
}
enum "nbd" {
description "10809/tcp — Network Block Device";
}
enum "netbios-ns" {
description "137/udp — NetBIOS Name Service for Windows networking";
}
enum "netconf" {
description "830/tcp — Network Configuration Protocol for network device management";
}
enum "nfs" {
description "2049/tcp+udp — Network File System for distributed file sharing";
}
enum "nfs3" {
description "2049/tcp+udp — Network File System version 3";
}
enum "nmea-0183" {
description "10110/tcp — Marine electronics data interface";
}
enum "nrpe" {
description "5666/tcp — Nagios Remote Plugin Executor";
}
enum "ntp" {
description "123/udp — Network Time Protocol for time synchronization";
}
enum "openvpn" {
description "1194/udp — OpenVPN secure tunnel for VPN connections";
}
enum "opentelemetry" {
description "4317/tcp — Open source observability framework";
}
enum "pop3" {
description "110/tcp — Post Office Protocol version 3 for email retrieval";
}
enum "pop3s" {
description "995/tcp — Secure Post Office Protocol version 3 for encrypted email retrieval";
}
enum "plex" {
description "32400/tcp — Plex media server";
}
enum "postgresql" {
description "5432/tcp — PostgreSQL database server connections";
}
enum "prometheus" {
description "9090/tcp — Monitoring system and time series database";
}
enum "proxy-dhcp" {
description "4011/udp — Proxy DHCP for PXE boot";
}
enum "ptp" {
description "319-320/udp — Precision Time Protocol";
}
enum "puppetmaster" {
description "8140/tcp — Puppet configuration management server";
}
enum "radius" {
description "1812-1813/tcp+udp — Remote Authentication Dial-in User Service";
}
enum "quassel" {
description "4242/tcp — Quassel IRC client-server protocol";
}
enum "radsec" {
description "2083/tcp — RADIUS over TLS";
}
enum "rdp" {
description "3389/tcp — Remote Desktop Protocol for Windows remote access";
}
enum "redis" {
description "6379/tcp — In-memory data structure store for database and cache";
}
enum "redis-sentinel" {
description "26379/tcp — Redis Sentinel for high availability";
}
enum "restconf" {
description "443/tcp — RESTful Network Configuration Protocol for HTTP-based network management";
}
enum "rsyncd" {
description "873/tcp — Rsync daemon for centralized file synchronization";
}
enum "rtsp" {
description "554/tcp — Real Time Streaming Protocol";
}
enum "samba" {
description "445/tcp — Windows file and printer sharing";
}
enum "samba-client" {
description "138/udp — Windows file and printer sharing (client-only)";
}
enum "samba-dc" {
description "389/tcp — Samba Active Directory Domain Controller";
}
enum "sane" {
description "6566/tcp — Scanner Access Now Easy network scanning";
}
enum "sip" {
description "5060/tcp+udp — Session Initiation Protocol for VoIP communications";
}
enum "sips" {
description "5061/tcp+udp — Secure Session Initiation Protocol for encrypted VoIP";
}
enum "smtp" {
description "25/tcp — Simple Mail Transfer Protocol for email transmission";
}
enum "smtps" {
description "465/tcp — Secure Simple Mail Transfer Protocol over SSL";
}
enum "slp" {
description "427/tcp+udp — Service Location Protocol";
}
enum "snmp" {
description "161/udp — Simple Network Management Protocol for network monitoring";
}
enum "snmptrap" {
description "162/udp — Simple Network Management Protocol trap notifications";
}
enum "snmptls" {
description "10161/tcp — SNMP over TLS";
}
enum "snmptls-trap" {
description "10162/tcp — SNMP trap over TLS";
}
enum "spotify-sync" {
description "57621/tcp — Spotify Connect synchronization";
}
enum "ssh" {
description "22/tcp — Secure Shell for remote login and command execution";
}
enum "stun" {
description "3478/tcp+udp — Session Traversal Utilities for NAT";
}
enum "stuns" {
description "5349/tcp — Secure STUN over TLS";
}
enum "supertuxkart" {
description "7321/tcp — SuperTuxKart racing game server";
}
enum "svn" {
description "3690/tcp — Subversion version control system";
}
enum "syslog" {
description "514/udp — System logging protocol for log message transmission";
}
enum "syslog-tls" {
description "6514/tcp — Secure system logging protocol over TLS";
}
enum "syncthing" {
description "22000/tcp — Continuous file synchronization";
}
enum "syncthing-gui" {
description "8384/tcp — Syncthing web interface";
}
enum "syncthing-relay" {
description "22067/tcp — Syncthing relay protocol";
}
enum "synergy" {
description "24800/tcp — Keyboard and mouse sharing";
}
enum "ssdp" {
description "1900/udp — Simple Service Discovery Protocol for UPnP device discovery";
}
enum "telnet" {
description "23/tcp — Telnet protocol for remote terminal access";
}
enum "tftp" {
description "69/udp — Trivial File Transfer Protocol for simple file transfers";
}
enum "turn" {
description "3478/tcp+udp — Traversal Using Relay NAT for firewall traversal";
}
enum "turns" {
description "5349/tcp — Secure TURN over TLS";
}
enum "vnc-server" {
description "5900-5906/tcp — Virtual Network Computing server for remote desktop access";
}
enum "vrrp" {
description "112/ip — Virtual Router Redundancy Protocol";
}
enum "warpinator" {
description "42000/tcp — File sharing tool by Linux Mint";
}
enum "wireguard" {
description "51820/udp — Modern VPN tunnel for secure networking";
}
enum "xdmcp" {
description "177/tcp+udp — X Display Manager Control Protocol for remote X11 sessions";
}
enum "zerotier" {
description "9993/udp — ZeroTier virtual network service";
}
}
}
}
@@ -0,0 +1 @@
infix-firewall-services.yang
+495
View File
@@ -0,0 +1,495 @@
module infix-firewall {
yang-version 1.1;
namespace "urn:infix:firewall:ns:yang:1.0";
prefix ifw;
import ietf-inet-types {
prefix inet;
reference "RFC 6991: Common YANG Data Types";
}
import ietf-interfaces {
prefix if;
reference "RFC 8343: A YANG Data Model for Interface Management";
}
import infix-firewall-icmp-types {
prefix ifw-icmp;
reference "internal";
}
import infix-firewall-services {
prefix ifw-svc;
reference "internal";
}
organization "KernelKit";
contact "kernelkit@googlegroups.com";
description "Zone-based firewall inspired by firewalld concepts.";
revision 2025-04-26 {
description "Initial revision.";
reference "internal";
}
/*
* Type definitions
*/
typedef ident {
description "Generic filesystem-safe identifier (filename).";
type string {
length "2..64";
pattern '[a-zA-Z0-9\-_]+';
}
}
typedef zone-action {
description "Default action for a zone.";
type enumeration {
enum accept {
description "Accept all connections by default.";
}
enum reject {
description "Reject all connections, except ICMP, by default.";
}
enum drop {
description "Drop all connections by default.";
}
}
}
typedef zone-ref {
description "Reference to a named zone or symbolic value: 'HOST' or 'ANY'.";
type union {
type enumeration {
enum HOST {
description "Refers to the local host/device itself";
}
enum ANY {
description "Refers to any zone";
}
}
type leafref {
path "../../zone/name";
}
}
}
typedef policy-action {
type enumeration {
enum continue {
description "Non-terminal policy. Matching traffic is accepted or allowed to proceed, and other policies continue to be evaluated.";
}
enum accept {
description "Accept matching traffic and stop evaluating further policies.";
}
enum reject {
description "Reject matching traffic (e.g., send ICMP unreachable) and stop evaluating further policies.";
}
enum drop {
description "Silently drop matching traffic and stop evaluating further policies.";
}
}
description "Action for traffic that does not match any specific service or port entry.";
}
typedef protocol-type {
description "Network protocols supported for services and port definitions.";
type enumeration {
enum tcp {
description "TCP protocol.";
}
enum udp {
description "UDP protocol.";
}
enum sctp {
description "SCTP protocol.";
}
enum dccp {
description "DCCP protocol.";
}
}
}
/*
* Main container and configuration
*/
container firewall {
description "Zone-based firewall configuration.";
presence "Activate firewall.";
leaf enabled {
description "Enable or disable the firewall.
Note, by disabling the firewall all rules are unloaded from the kernel, making
the system fully open! This can be useful when debugging firewall issues, but
remember to re-enable when done, and maybe remove connections to the Internet
before disabling.";
type boolean;
default true;
}
leaf default {
description "Default zone for interfaces.
Any interface not explicitly associated with a zone is placed in this zone.";
type leafref {
path "../zone/name";
}
mandatory true;
}
leaf logging {
description "Enable logging of denied (rejected/dropped) packets.
Add logging rules right before reject and drop rules in the INPUT, FORWARD and
OUTPUT chains for the default rules and also final reject and drop rules in
zones for the configured link-layer packet type.";
type enumeration {
enum all {
description "Log all denied packets.";
}
enum unicast {
description "Log unicast denied packets.";
}
enum broadcast {
description "Log broadcast denied packets.";
}
enum multicast {
description "Log multicast denied packets.";
}
enum off {
description "Do not log denied packets.";
}
}
default off;
}
list zone {
description "A zone defines a level of trust for network connections.";
key "name";
must "count(interface) = 0 or count(network) = 0" {
error-message "A zone cannot have both interfaces and networks - use interfaces for local traffic or networks for forwarding";
}
leaf name {
description "Name of the zone.";
type ident;
}
leaf action {
description "Default action for traffic from this zone to HOST.
When 'accept', all traffic from this zone can reach HOST services.
When 'reject' or 'drop', only explicitly listed services are allowed
to reach HOST, all other traffic is rejected/dropped. I.e., an ICMP
unreachable message or silent drop.";
type zone-action;
default reject;
}
leaf immutable {
description "Indicates if this zone is read-only/system-defined and cannot be modified.";
config false;
type boolean;
}
leaf description {
description "Free-form description of the zone.";
type string;
}
leaf-list interface {
description "List of interfaces assigned to this zone.";
type if:interface-ref;
must "count(/firewall/zone[interface = current()]) <= 1" {
error-message "An interface can only be assigned to one firewall zone";
}
}
leaf-list network {
description "IP networks assigned to this zone.";
type inet:ip-prefix;
}
leaf-list service {
description "Services allowed from this zone to HOST (INPUT chain only).
These define exceptions when action is 'reject' or 'drop'.
Ignored when action is 'accept' (all services allowed).";
type union {
type leafref {
path "../../service/name";
}
type ifw-svc:well-known-service;
}
}
list port-forward {
description "Forward traffic to another port and/or host (DNAT).
Port forwarding rules within a zone apply to traffic matching
the zone's interfaces or networks.";
key "lower proto";
leaf lower {
description "Local port to forward from.";
type inet:port-number;
mandatory true;
}
leaf upper {
description "Upper port when forwarding a range of ports.";
type inet:port-number;
must "../lower <= .";
}
leaf proto {
description "Network protocol to forward.";
type protocol-type;
mandatory true;
}
container to {
description "Destination to forward to.";
leaf addr {
description "Destination IPv4/IPv6 address to forward to.";
type inet:ip-address;
}
leaf port {
description "Destination port to forward to. Defaults to 'lower',
and the upper is then automatically calculated.";
type inet:port-number;
}
}
}
}
list policy {
description "Rules for filtering traffic forwarded between zones (inter-zone).";
ordered-by user;
key "name";
must "count(ingress) > 0 and count(egress) > 0" {
error-message "A policy must have at least one ingress and one egress zone defined";
}
leaf name {
description "Unique identifier (filename) for this policy, e.g., LAN-to-WAN.";
type ident;
}
leaf action {
description "Action for non-matching traffic.
With 'continue' non-matching traffic is allowed to continue to
be processed by the next policy rule, or in the end be dropped.";
type policy-action;
default "reject";
}
leaf immutable {
description "Indicates if this policy is read-only/system-defined and cannot be modified.";
config false;
type boolean;
}
leaf description {
description "Free-form description of this policy's purpose and scope.";
type string;
}
leaf-list ingress {
type ifw:zone-ref;
description "List of zones traffic is entering. Use symbolic 'HOST' or 'ANY' as needed.";
}
leaf-list egress {
description "List of zones traffic is exiting from. Use symbolic 'HOST' or 'ANY' as needed.";
type ifw:zone-ref;
}
leaf masquerade {
description "Enable masquerading (SNAT) for traffic matching this policy.
Matching traffic will have their source IP address changed on egress,
using the IP address of the interface the traffic egresses.";
type boolean;
}
leaf-list service {
description "Services to allow between ingress and egress zones.
Services that are not in this list follow the default policy action. When
it is 'accept', all services are accepted, regardless of this list. When
action is 'reject' or 'drop': these services are accepted, while others are
rejected/dropped. When action is 'continue', traffic not matching the list
is passed on to the next policy.";
type union {
type leafref {
path "../../service/name";
}
type ifw-svc:well-known-service;
}
}
container custom {
description "Custom filters, prioritized over other policy elements.";
list filter {
description "Custom traffic filters with specific matching criteria.
Evaluation order = list order.";
ordered-by user;
key "name";
must "action" {
error-message "Custom filters must have a valid action.";
}
must "not(icmp) or icmp/type" {
error-message "ICMP filters must specify an ICMP type.";
}
leaf name {
description "Unique identifier for this filter within the policy.";
type ident;
}
leaf family {
description "Address family selector.";
type enumeration { enum ipv4; enum ipv6; enum both; }
default both;
}
choice type {
description "Type of traffic to match for this filter.";
case icmp {
container icmp {
leaf type {
description "ICMP type to match.";
type ifw-icmp:type;
}
}
}
}
leaf action {
description "How to handle filter matches.";
// XXX: Different from similar enums because we may add 'mark' later
type enumeration { enum accept; enum drop; enum reject; }
default accept;
}
leaf priority {
// Sorting order as read from firewalld
description "Effective priority of this filter.";
config false;
type int16;
}
}
}
leaf priority {
// Sorting order as read from firewalld
description "Effective priority of this filter.";
config false;
type int16;
}
}
list service {
description "Manage services, human-friendly names of port+protocol pairs.
A service is a collection of port and protocol pairs. Used by the firewall
instead of hard-coding raw port numbers everywhere.";
key "name";
leaf name {
description "Name of the service.";
type ident;
}
leaf description {
description "Free-form description of the service.";
type string;
}
list port {
description "Port, or range of ports, and protocol to match.";
key "lower proto";
leaf lower {
description "Lower port in range.";
type inet:port-number;
}
leaf upper {
description "Upper port in range.";
type inet:port-number;
must "../lower <= .";
}
leaf proto {
description "Layer 4 protocol.";
type protocol-type;
}
}
leaf destination {
type union {
type inet:ip-address;
type inet:ip-prefix;
}
description "Destination IP address/group to match this service to.";
}
}
leaf lockdown {
description "Current state of emergency lockdown mode.";
config false;
type boolean;
}
action lockdown-mode {
description "Emergency lockdown mode blocks all network traffic.
This action is effectively a kill switch for all network
connections, immediately dropping all incoming and outgoing
packets and terminating existing sessions. It is intended for
emergency situations such as active security breaches where
immediate network isolation is required.
WARNING: Activating lockdown mode will sever all remote
connections including SSH sessions. Physical console access
will be required to deactivate lockdown mode and restore
normal network operations.
Implementation uses firewalld panic mode under the hood to
achieve complete traffic blocking at the netfilter level.";
input {
leaf operation {
description "Lockdown operation to perform";
type enumeration {
enum now {
description "Enable lockdown mode immediately - block all traffic";
}
enum cancel {
description "Cancel lockdown mode - restore normal operation";
}
}
mandatory true;
}
}
}
}
}
+1
View File
@@ -0,0 +1 @@
infix-firewall.yang
+58
View File
@@ -142,6 +142,61 @@ int infix_ifaces(kcontext_t *ctx)
return 0;
}
static int firewall_dbus_completion(const char *interface, const char *method, const char *parser)
{
return systemf("gdbus call --system --dest org.fedoraproject.FirewallD1 "
"--object-path /org/fedoraproject/FirewallD1 "
"--method org.fedoraproject.FirewallD1.%s.%s 2>/dev/null "
"| %s", interface, method, parser);
}
/*
* Completion function for firewall zones.
* D-Bus returns variant format: ({'zone1': {...}},)
* Pipeline:
* - sed removes wrapper parentheses
* - tr converts single to double quotes
* - jq extracts keys
*/
int infix_firewall_zones(kcontext_t *ctx)
{
(void)ctx;
return firewall_dbus_completion("zone", "getActiveZones",
"sed 's/^(//; s/,)$//' | sed 's/@as \\[\\]/[]/g' | tr \"'\" '\"' | jq -r 'keys[]' 2>/dev/null");
}
/*
* Completion function for firewall policies.
* D-Bus returns variant format: (['policy1', 'policy2'],)
* Pipeline:
* - sed removes wrapper parentheses
* - tr converts single to double quotes
* - jq extracts array items
*/
int infix_firewall_policies(kcontext_t *ctx)
{
(void)ctx;
return firewall_dbus_completion("policy", "getPolicies",
"sed 's/^(//; s/,)$//' | tr \"'\" '\"' | jq -r '.[]' 2>/dev/null");
}
/*
* Completion function for firewall services.
* D-Bus returns variant format: (['dhcp', 'dns', 'ssh'],)
* Pipeline:
* - sed removes wrapper parentheses
* - tr converts single to double quotes
* - jq extracts array items
*/
int infix_firewall_services(kcontext_t *ctx)
{
(void)ctx;
return systemf("gdbus call --system --dest org.fedoraproject.FirewallD1 "
"--object-path /org/fedoraproject/FirewallD1 "
"--method org.fedoraproject.FirewallD1.listServices 2>/dev/null "
"| sed 's/^(//; s/,)$//' | tr \"'\" '\"' | jq -r '.[]' 2>/dev/null");
}
int infix_copy(kcontext_t *ctx)
{
kpargv_t *pargv = kcontext_pargv(ctx);
@@ -228,6 +283,9 @@ int kplugin_infix_init(kcontext_t *ctx)
kplugin_add_syms(plugin, ksym_new("erase", infix_erase));
kplugin_add_syms(plugin, ksym_new("files", infix_files));
kplugin_add_syms(plugin, ksym_new("ifaces", infix_ifaces));
kplugin_add_syms(plugin, ksym_new("firewall_zones", infix_firewall_zones));
kplugin_add_syms(plugin, ksym_new("firewall_policies", infix_firewall_policies));
kplugin_add_syms(plugin, ksym_new("firewall_services", infix_firewall_services));
kplugin_add_syms(plugin, ksym_new("shell", infix_shell));
return 0;
+77
View File
@@ -113,6 +113,27 @@
<ACTION sym="STRING"/>
</PTYPE>
<PTYPE name="FIREWALL_ZONES">
<COMPL>
<ACTION sym="firewall_zones@infix"/>
</COMPL>
<ACTION sym="STRING"/>
</PTYPE>
<PTYPE name="FIREWALL_POLICIES">
<COMPL>
<ACTION sym="firewall_policies@infix"/>
</COMPL>
<ACTION sym="STRING"/>
</PTYPE>
<PTYPE name="FIREWALL_SERVICES">
<COMPL>
<ACTION sym="firewall_services@infix"/>
</COMPL>
<ACTION sym="STRING"/>
</PTYPE>
<VIEW name="main">
<HOTKEY key="^D" cmd="exit"/>
@@ -490,6 +511,43 @@
<COMMAND name="startup-config" help="Show startup-config">
<ACTION sym="script" in="tty" out="tty" interrupt="true">jq -C . /cfg/startup-config.cfg |pager</ACTION>
</COMMAND>
<COMMAND name="firewall" help="Show firewall status and configuration">
<SWITCH name="optional" min="0" max="1">
<COMMAND name="log" help="Show firewall log (jumps to end), alias to 'show log firewall.log'">
<ACTION sym="script" in="tty" out="tty" interrupt="true">
doas -u $USER cat /log/firewall.log |pager +G
</ACTION>
</COMMAND>
<COMMAND name="zone" help="Show firewall zones">
<SWITCH name="optional" min="0">
<PARAM name="name" ptype="/FIREWALL_ZONES" help="Zone name"/>
</SWITCH>
<ACTION sym="script" in="tty" out="tty" interrupt="true">
sysrepocfg -X -d operational -x /infix-firewall:firewall -f json -t 60 | /usr/libexec/statd/cli-pretty show-firewall-zone "$KLISH_PARAM_name" |pager
</ACTION>
</COMMAND>
<COMMAND name="policy" help="Show firewall policies">
<SWITCH name="optional" min="0">
<PARAM name="name" ptype="/FIREWALL_POLICIES" help="Policy name"/>
</SWITCH>
<ACTION sym="script" in="tty" out="tty" interrupt="true">
sysrepocfg -X -d operational -x /infix-firewall:firewall -f json -t 60 | /usr/libexec/statd/cli-pretty show-firewall-policy "$KLISH_PARAM_name" |pager
</ACTION>
</COMMAND>
<COMMAND name="service" help="Show firewall services">
<SWITCH name="optional" min="0">
<PARAM name="name" ptype="/FIREWALL_SERVICES" help="Service name"/>
</SWITCH>
<ACTION sym="script" in="tty" out="tty" interrupt="true">
sysrepocfg -X -d operational -x /infix-firewall:firewall -f json -t 60 | /usr/libexec/statd/cli-pretty show-firewall-service "$KLISH_PARAM_name" |pager
</ACTION>
</COMMAND>
</SWITCH>
<ACTION sym="script" in="tty" out="tty" interrupt="true">
sysrepocfg -X -d operational -x /infix-firewall:firewall -f json -t 60 | /usr/libexec/statd/cli-pretty show-firewall |pager
</ACTION>
</COMMAND>
</COMMAND>
<COMMAND name="factory-reset" help="Restore the system to factory default state">
@@ -499,6 +557,25 @@
<ACTION sym="srp_rpc@sysrepo">/ietf-factory-default:factory-reset</ACTION>
</COMMAND>
<COMMAND name="firewall" help="Control the firewall" mode="switch">
<COMMAND name="lockdown" help="Emergency lockdown mode">
<PARAM name="operation" ptype="/STRING" help="Lockdown commands.">
<COMPL>
<ACTION sym="printl">now</ACTION>
<ACTION sym="printl">cancel</ACTION>
</COMPL>
</PARAM>
<ACTION sym="script" in="tty" out="tty" interrupt="true">
if [ "${KLISH_PARAM_operation}" = "now" ]; then
if ! firewall panic status; then
/bin/yorn -q "WARNING: This will block ALL network traffic and sever existing connections"
fi
fi
</ACTION>
<ACTION sym="srp_rpc@sysrepo">/infix-firewall:firewall/lockdown-mode</ACTION>
</COMMAND>
</COMMAND>
<COMMAND name="follow" help="Monitor a log file, use Ctrl-C to abort">
<PARAM name="fn" ptype="/LOGFILES" help="Optional log file to monitor, default: syslog"/>
<ACTION sym="script" in="tty" out="tty" interrupt="true">
File diff suppressed because it is too large Load Diff
+3
View File
@@ -78,6 +78,9 @@ def main():
elif args.model == 'ieee802-dot1ab-lldp':
from . import infix_lldp
yang_data = infix_lldp.operational()
elif args.model == 'infix-firewall':
from . import infix_firewall
yang_data = infix_firewall.operational()
else:
common.LOG.warning("Unsupported model %s", args.model)
sys.exit(1)
+362
View File
@@ -0,0 +1,362 @@
#!/usr/bin/env python3
"""
Collect operational data for infix-firewall.yang from firewalld using D-Bus,
for the full API, see:
gdbus introspect --system --dest org.fedoraproject.FirewallD1 \
--object-path /org/fedoraproject/FirewallD1
"""
import dbus
import re
from . import common
def get_interface(interface="org.fedoraproject.FirewallD1"):
try:
bus = dbus.SystemBus()
obj = bus.get_object("org.fedoraproject.FirewallD1",
"/org/fedoraproject/FirewallD1")
return dbus.Interface(obj, dbus_interface=interface)
except dbus.exceptions.DBusException as e:
common.LOG.warning("Failed to connect to firewalld D-Bus: %s", e)
return None
def get_zone_data(fw, name):
"""
$ gdbus call --system --dest org.fedoraproject.FirewallD1 \
--object-path /org/fedoraproject/FirewallD1 \
--method org.fedoraproject.FirewallD1.zone.getForwardPorts \
external
([['443', 'tcp', '443', '192.168.2.10']],)
"""
try:
settings = fw.getZoneSettings2(name)
target = settings.get('target', 'default')
action = {
"%%REJECT%%": "reject",
"REJECT": "reject",
"ACCEPT": "accept",
"DROP": "drop",
"default": "accept"
}
short = settings.get('short', '')
immutable = False
if short and "(immutable)" in short:
# Remove (immutable), added by us to set ⚷ symbol in output
short = short.replace("(immutable)", "").strip()
immutable = True
elif not short:
short = ""
zone = {
"name": name,
"short": short,
"immutable": immutable,
"description": settings.get('description', 0),
"interface": list(settings.get('interfaces', [])),
"network": list(settings.get('sources', [])),
"action": action.get(target, "accept"),
"service": list(settings.get('services', []))
}
# Handle port forwarding from zone
port_forwards = []
forwards = settings.get('forward_ports', [])
for fwd in forwards:
try:
if len(fwd) >= 4:
port, protocol, toport, toaddr = fwd[:4] # Fixed field order!
# Handle port ranges: port can be "80" or "8000-8080"
if '-' in str(port):
port_lower, port_upper = str(port).split('-', 1)
fwd_data = {
'lower': int(port_lower),
'upper': int(port_upper),
'proto': str(protocol),
'to': {
'addr': str(toaddr)
}
}
else:
fwd_data = {
'lower': int(port),
'proto': str(protocol),
'to': {
'addr': str(toaddr)
}
}
# Handle destination port - only store lower port, upper calculated by C code
if toport and str(toport).strip():
toport_str = str(toport).strip()
# Skip if toport looks like an IP address instead of port
if '.' not in toport_str and ':' not in toport_str:
fwd_data['to']['port'] = int(toport_str)
else:
# If toport looks like IP, use the same port as source lower
fwd_data['to']['port'] = fwd_data['lower']
else:
# No destination port specified, use same as source lower
fwd_data['to']['port'] = fwd_data['lower']
port_forwards.append(fwd_data)
except (ValueError, IndexError, TypeError) as e:
common.LOG.warning("Invalid port forward rule in zone %s: %s", name, e)
continue
if port_forwards:
zone["port-forward"] = port_forwards
return zone
except Exception as e:
common.LOG.warning("Failed querying zone %s via D-Bus: %s", name, e)
return None
def get_zones(fw):
"""Get only active zones (loaded in kernel) instead of all zones"""
zones = []
try:
fwz = get_interface("org.fedoraproject.FirewallD1.zone")
if not fwz:
return zones
active_zones = fwz.getActiveZones()
for name, zone_info in active_zones.items():
zone_data = get_zone_data(fwz, name)
if zone_data:
zone_data['interface'] = list(zone_info.get('interfaces', []))
zone_data['network'] = list(zone_info.get('sources', []))
zones.append(zone_data)
except Exception as e:
common.LOG.warning("Failed querying zones: %s", e)
return zones
def get_policy_data(fw, name):
try:
settings = fw.getPolicySettings(name)
policy = {
"name": name,
"action": "reject",
"priority": 32767,
"ingress": [],
"egress": []
}
target = settings.get('target', 'CONTINUE')
action = {
"CONTINUE": "continue",
"ACCEPT": "accept",
"REJECT": "reject",
"DROP": "drop"
}
policy["action"] = action.get(target, "reject")
priority = settings.get('priority', 32767)
if isinstance(priority, int):
policy["priority"] = priority
description = settings.get('description', '')
if description:
policy["description"] = description
short = settings.get('short', '')
policy["immutable"] = bool(short and "(immutable)" in short)
ingress = settings.get('ingress_zones', [])
if ingress:
policy["ingress"] = list(ingress)
egress = settings.get('egress_zones', [])
if egress:
policy["egress"] = list(egress)
services = settings.get('services', [])
if services:
policy["service"] = list(services)
policy["masquerade"] = bool(settings.get('masquerade', 0))
# Handle custom filters from rich_rules
custom_filters = []
rich_rules = settings.get('rich_rules', [])
for rule in rich_rules:
# Extract family (default to both if not specified)
family = "both"
if 'family="ipv4"' in rule:
family = "ipv4"
elif 'family="ipv6"' in rule:
family = "ipv6"
icmp_type = None
action = None
prio = -1
if 'priority' in rule:
prio_match = re.search(r'.*priority=([^ ]+)', rule)
if prio_match:
val = prio_match.group(1)
if isinstance(val, int):
prio = val
if 'icmp-type' in rule and 'name=' in rule:
name_match = re.search(r'.*name="([^"]+)"', rule)
if name_match:
icmp_type = name_match.group(1)
action = "accept"
if ' drop' in rule:
action = "drop"
elif ' reject' in rule:
action = "reject"
elif 'icmp-block' in rule and 'name=' in rule:
name_match = re.search(r'.*name="([^"]+)"', rule)
if name_match:
icmp_type = name_match.group(1)
action = "reject"
if icmp_type and action:
filter_entry = {
"name": f"icmp-{icmp_type}",
"priority": prio,
"family": family,
"action": action,
"icmp": {
"type": icmp_type
}
}
custom_filters.append(filter_entry)
if custom_filters:
policy["custom"] = {
"filter": custom_filters
}
return policy
except Exception as e:
common.LOG.warning("Failed querying policy %s via D-Bus: %s", name, e)
return None
def get_policies(fw):
policies = []
try:
fwp = get_interface("org.fedoraproject.FirewallD1.policy")
if not fwp:
return policies
for name in fwp.getPolicies():
data = get_policy_data(fwp, name)
if data:
policies.append(data)
except Exception as e:
common.LOG.warning("Failed querying policies: %s", e)
# Add implicit drop/reject policy as the last rule
implicit_policy = {
"name": "default-drop",
"description": "Default deny rule - drops all unmatched traffic",
"action": "drop",
"priority": 32767, # Highest priority number (lowest precedence)
"ingress": ["ANY"],
"egress": ["ANY"],
"immutable": True
}
policies.append(implicit_policy)
return policies
def get_service_data(fw, name):
try:
settings = fw.getServiceSettings2(name)
service = {
"name": name,
"port": []
}
description = settings.get('description', '')
if description:
service["description"] = description
ports = settings.get('ports', [])
for port_info in ports:
if len(port_info) >= 2:
port, protocol = port_info[:2]
port_data = {'proto': protocol}
if '-' in str(port):
lower, upper = str(port).split('-', 1)
port_data['lower'] = int(lower)
port_data['upper'] = int(upper)
else:
port_data['lower'] = int(port)
service["port"].append(port_data)
return service
except Exception as e:
common.LOG.warning("Failed querying service %s via D-Bus: %s", name, e)
return None
def get_services(fw):
services = []
try:
for name in fw.listServices():
data = get_service_data(fw, name)
if data:
services.append(data)
except Exception as e:
common.LOG.warning("Failed querying services: %s", e)
return services
def operational():
try:
fw = get_interface()
if not fw:
return {}
except Exception as e:
common.LOG.warning("Failed checking firewalld state: %s", e)
return {}
data = {
"infix-firewall:firewall": {
"default": fw.getDefaultZone(),
"logging": fw.getLogDenied(),
"lockdown": bool(fw.queryPanicMode())
}
}
zones = get_zones(fw)
if zones:
data["infix-firewall:firewall"]["zone"] = zones
policies = get_policies(fw)
if policies:
data["infix-firewall:firewall"]["policy"] = policies
services = get_services(fw)
if services:
data["infix-firewall:firewall"]["service"] = services
return data
+3
View File
@@ -43,6 +43,7 @@
#define XPATH_CONTAIN_BASE "/infix-containers:containers"
#define XPATH_DHCP_SERVER_BASE "/infix-dhcp-server:dhcp-server"
#define XPATH_LLDP_BASE "/ieee802-dot1ab-lldp:lldp"
#define XPATH_FIREWALL_BASE "/infix-firewall:firewall"
TAILQ_HEAD(sub_head, sub);
@@ -356,6 +357,8 @@ static int subscribe_to_all(struct statd *statd)
#endif
if (subscribe(statd, "infix-dhcp-server", XPATH_DHCP_SERVER_BASE, sr_generic_cb))
return SR_ERR_INTERNAL;
if (subscribe(statd, "infix-firewall", XPATH_FIREWALL_BASE, sr_generic_cb))
return SR_ERR_INTERNAL;
INFO("Successfully subscribed to all models");
return SR_ERR_OK;
+2 -1
View File
@@ -1,4 +1,5 @@
USB PORTS 
──────────────────────────────
USB PORTS
NAME STATE 
USB locked
USB2 locked