Merge pull request #1114 from kernelkit/fw
Add basic zone-based firewall
@@ -41,6 +41,7 @@ jobs:
|
|||||||
pipx inject mkdocs mkdocs-callouts
|
pipx inject mkdocs mkdocs-callouts
|
||||||
pipx inject mkdocs mike
|
pipx inject mkdocs mike
|
||||||
pipx inject mkdocs mkdocs-to-pdf
|
pipx inject mkdocs mkdocs-to-pdf
|
||||||
|
pipx inject mkdocs mkdocs-glightbox
|
||||||
# Workaround, if pipx inject fails to install symlink
|
# Workaround, if pipx inject fails to install symlink
|
||||||
ln -s "$(pipx environment -V PIPX_LOCAL_VENVS)/mkdocs/bin/mike" \
|
ln -s "$(pipx environment -V PIPX_LOCAL_VENVS)/mkdocs/bin/mike" \
|
||||||
"$(pipx environment -V PIPX_BIN_DIR)/mike" || true
|
"$(pipx environment -V PIPX_BIN_DIR)/mike" || true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[![License Badge][]][License] [![GitHub Status][]][GitHub] [![Coverity Status][]][Coverity Scan] [![Discord][discord-badge]][discord-url]
|
[![License Badge][]][License] [![GitHub Status][]][GitHub] [![Coverity Status][]][Coverity Scan] [![Discord][discord-badge]][discord-url]
|
||||||
|
|
||||||
<img align="right" src="doc/logo.png" alt="Infix - Linux <3 NETCONF" width=480 border=10>
|
<img align="right" src="doc/logo.png" alt="Infix — Immutable.Friendly.Secure" width=480 border=10>
|
||||||
|
|
||||||
Turn any ARM or x86 device into a powerful, manageable network appliance
|
Turn any ARM or x86 device into a powerful, manageable network appliance
|
||||||
in minutes. From $35 Raspberry Pi boards to enterprise switches — deploy
|
in minutes. From $35 Raspberry Pi boards to enterprise switches — deploy
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,11 +1,18 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
opts="-n1"
|
||||||
|
|
||||||
|
if [ "$1" = "-q" ]; then
|
||||||
|
opts="$opts -s"
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
|
||||||
Q=$@
|
Q=$@
|
||||||
|
|
||||||
/bin/echo -n "$Q, are you sure (y/N)? "
|
/bin/echo -n "$Q, are you sure (y/N)? "
|
||||||
read -n1 yorn
|
read $opts yorn
|
||||||
echo
|
echo
|
||||||
|
|
||||||
if [ x$yorn != "xy" ] && [ x$yorn != "xY" ]; then
|
if [ "x$yorn" != "xy" ] && [ "x$yorn" != "xY" ]; then
|
||||||
echo "OK, aborting."
|
echo "OK, aborting."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ BR2_PACKAGE_CONNTRACK_TOOLS=y
|
|||||||
BR2_PACKAGE_DNSMASQ=y
|
BR2_PACKAGE_DNSMASQ=y
|
||||||
BR2_PACKAGE_ETHTOOL=y
|
BR2_PACKAGE_ETHTOOL=y
|
||||||
BR2_PACKAGE_FPING=y
|
BR2_PACKAGE_FPING=y
|
||||||
|
BR2_PACKAGE_FIREWALL=y
|
||||||
BR2_PACKAGE_FRR=y
|
BR2_PACKAGE_FRR=y
|
||||||
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
|
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
|
||||||
BR2_PACKAGE_IPERF3=y
|
BR2_PACKAGE_IPERF3=y
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ BR2_PACKAGE_AVAHI_DEFAULT_SERVICES=y
|
|||||||
BR2_PACKAGE_CHRONY=y
|
BR2_PACKAGE_CHRONY=y
|
||||||
BR2_PACKAGE_DNSMASQ=y
|
BR2_PACKAGE_DNSMASQ=y
|
||||||
BR2_PACKAGE_ETHTOOL=y
|
BR2_PACKAGE_ETHTOOL=y
|
||||||
|
BR2_PACKAGE_FIREWALL=y
|
||||||
BR2_PACKAGE_FRR=y
|
BR2_PACKAGE_FRR=y
|
||||||
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
|
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
|
||||||
BR2_PACKAGE_IPROUTE2=y
|
BR2_PACKAGE_IPROUTE2=y
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ BR2_PACKAGE_CONNTRACK_TOOLS=y
|
|||||||
BR2_PACKAGE_DNSMASQ=y
|
BR2_PACKAGE_DNSMASQ=y
|
||||||
BR2_PACKAGE_ETHTOOL=y
|
BR2_PACKAGE_ETHTOOL=y
|
||||||
BR2_PACKAGE_FPING=y
|
BR2_PACKAGE_FPING=y
|
||||||
|
BR2_PACKAGE_FIREWALL=y
|
||||||
BR2_PACKAGE_FRR=y
|
BR2_PACKAGE_FRR=y
|
||||||
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
|
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
|
||||||
BR2_PACKAGE_IPERF3=y
|
BR2_PACKAGE_IPERF3=y
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ BR2_PACKAGE_CONNTRACK_TOOLS=y
|
|||||||
BR2_PACKAGE_DNSMASQ=y
|
BR2_PACKAGE_DNSMASQ=y
|
||||||
BR2_PACKAGE_ETHTOOL=y
|
BR2_PACKAGE_ETHTOOL=y
|
||||||
BR2_PACKAGE_FPING=y
|
BR2_PACKAGE_FPING=y
|
||||||
|
BR2_PACKAGE_FIREWALL=y
|
||||||
BR2_PACKAGE_FRR=y
|
BR2_PACKAGE_FRR=y
|
||||||
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
|
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
|
||||||
BR2_PACKAGE_IPERF3=y
|
BR2_PACKAGE_IPERF3=y
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ BR2_PACKAGE_CONNTRACK_TOOLS=y
|
|||||||
BR2_PACKAGE_DNSMASQ=y
|
BR2_PACKAGE_DNSMASQ=y
|
||||||
BR2_PACKAGE_ETHTOOL=y
|
BR2_PACKAGE_ETHTOOL=y
|
||||||
BR2_PACKAGE_FPING=y
|
BR2_PACKAGE_FPING=y
|
||||||
|
BR2_PACKAGE_FIREWALL=y
|
||||||
BR2_PACKAGE_FRR=y
|
BR2_PACKAGE_FRR=y
|
||||||
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
|
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
|
||||||
BR2_PACKAGE_IPERF3=y
|
BR2_PACKAGE_IPERF3=y
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ BR2_PACKAGE_AVAHI_DEFAULT_SERVICES=y
|
|||||||
BR2_PACKAGE_CHRONY=y
|
BR2_PACKAGE_CHRONY=y
|
||||||
BR2_PACKAGE_DNSMASQ=y
|
BR2_PACKAGE_DNSMASQ=y
|
||||||
BR2_PACKAGE_ETHTOOL=y
|
BR2_PACKAGE_ETHTOOL=y
|
||||||
|
BR2_PACKAGE_FIREWALL=y
|
||||||
BR2_PACKAGE_FRR=y
|
BR2_PACKAGE_FRR=y
|
||||||
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
|
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
|
||||||
BR2_PACKAGE_IPROUTE2=y
|
BR2_PACKAGE_IPROUTE2=y
|
||||||
|
|||||||
@@ -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
|
* TODO doc: User Guide
|
||||||
|
|
||||||
- Feature set and scope, e.g.
|
- Feature set and scope, e.g.
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
line-height: 2.6rem;
|
line-height: 2.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.md-typeset code {
|
||||||
|
font-size: .75em;
|
||||||
|
}
|
||||||
|
|
||||||
[data-md-color-primary="orange"] {
|
[data-md-color-primary="orange"] {
|
||||||
--md-primary-fg-color: #ff7f2a;
|
--md-primary-fg-color: #ff7f2a;
|
||||||
--md-primary-bg-color: #5c5f5c;
|
--md-primary-bg-color: #5c5f5c;
|
||||||
@@ -10,3 +14,13 @@
|
|||||||
[data-md-color-primary="black"] {
|
[data-md-color-primary="black"] {
|
||||||
--md-primary-bg-color: #5c5f5c;
|
--md-primary-bg-color: #5c5f5c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Center Markdown Tables (requires md_in_html extension) */
|
||||||
|
.center-table {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-typeset .center-table :is(td,th):not([align]) {
|
||||||
|
/* Reset alignment for table cells */
|
||||||
|
text-align: initial;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,460 @@
|
|||||||
|
{ align=left width="60" }
|
||||||
|
|
||||||
|
# Firewall Documentation
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
A zone-based firewall aims to *simplify network security*. Instead of complex
|
||||||
|
per-interface rules, you work with **zones** and **policies**. Briefly, ^^zones
|
||||||
|
define a level of trust^^ for all interfaces or networks assigned to it, and
|
||||||
|
^^policies regulate the traffic flow^^ between zones.
|
||||||
|
|
||||||
|
{ width=600 }
|
||||||
|
/// figure-caption
|
||||||
|
Zones group interfaces, policies control traffic flows.
|
||||||
|
///
|
||||||
|
|
||||||
|
Three distinct traffic flows exist: traffic destined for the host itself,
|
||||||
|
traffic between interfaces within the same zone (intra-zone), and traffic
|
||||||
|
between different zones (inter-zones).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
The zone approach is not just more intuitive and maintainable, it allows you
|
||||||
|
to think more in terms of trust relationships:
|
||||||
|
|
||||||
|
- "internal networks can access the Internet"
|
||||||
|
- "Internet cannot access my internal network, except this port forward"
|
||||||
|
|
||||||
|
When you add new interfaces to existing zones, they automatically inherit the
|
||||||
|
established security policies. The amount of actual rules *that matter to
|
||||||
|
you* is kept to a minimum.
|
||||||
|
|
||||||
|
> [!TIP] Impatient and ready to get going?
|
||||||
|
> [Fast forward to the Examples: End Device, Home/Office Router, Enterprise Gateway](#examples)
|
||||||
|
|
||||||
|
## Visual Overview
|
||||||
|
|
||||||
|
Use the **zone matrix** to quickly audit your firewall configuration and
|
||||||
|
identify potential security gaps. It provides an overview and shows the
|
||||||
|
relationship between zones and the policies that connect them. Each cell in
|
||||||
|
the matrix represents a potential traffic flow, with rows indicating the
|
||||||
|
ingress zone and columns the egress zone.
|
||||||
|
|
||||||
|

|
||||||
|
/// figure-caption
|
||||||
|
Example output from <kbd>show firewall</kbd> command.
|
||||||
|
///
|
||||||
|
|
||||||
|
The matrix uses visual indicators to show the status of each zone and policy:
|
||||||
|
|
||||||
|
**✓ Green checkmark** — traffic is explicitly allowed by policy
|
||||||
|
**✗ Red cross** — traffic is blocked (default behavior)
|
||||||
|
**⚠ Yellow warning** — watch out! Some traffic allowed, such as port forwarding rules
|
||||||
|
|
||||||
|
This visualization helps you quickly understand your firewall's behavior and
|
||||||
|
identify any unintended gaps or overly permissive rules in your configuration.
|
||||||
|
|
||||||
|
> [!TIP] Use the ++question++ key in the CLI
|
||||||
|
> This admin-exec (top-level) CLI command has sub-commands that you can use to
|
||||||
|
> drill down on the operational data. Tap the ++question++ key once to see an
|
||||||
|
> overview after <kbd>show firewall</kbd>, or just use the classic UNIX ++tab++ key to
|
||||||
|
> complete everything until you've found your command.
|
||||||
|
|
||||||
|
## Zones
|
||||||
|
|
||||||
|
Zones are logical groupings of network interfaces or IP networks that share
|
||||||
|
the same trust level. Each zone has a *default action* that determines what
|
||||||
|
happens to traffic destined for the host itself (INPUT chain). A LAN zone may
|
||||||
|
have this set to *accept*, while a DMZ zone may be set to *reject* by default
|
||||||
|
and only allow a subset of available *services* (e.g., DHCP, DNS, SSH) that
|
||||||
|
devices in the DMZ can use to reach the host.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> Interfaces and networks are mutually exclusive in zones — attempting to
|
||||||
|
> configure both will result in a validation error. When setting up
|
||||||
|
> [*port forwarding*](#port-forwarding) from one zone to another, the
|
||||||
|
> destination network must be declared in a zone.
|
||||||
|
|
||||||
|
### Default Zone
|
||||||
|
|
||||||
|
You must specify a default zone. This serves as a safety net: any interface
|
||||||
|
not explicitly assigned to a zone automatically belongs to the default zone,
|
||||||
|
ensuring network interfaces remain protected by the firewall. This automatic
|
||||||
|
assignment is particularly useful when configuring new interfaces (e.g.,
|
||||||
|
VLANs, bridges, or hotplugged devices).
|
||||||
|
|
||||||
|
Choose your default zone carefully — it should be the most restrictive zone
|
||||||
|
appropriate for unmanaged interfaces. For routers, this is typically the
|
||||||
|
`wan` zone, but you can of course also set up a dedicated `block` zone. In the
|
||||||
|
CLI, when first enabling the firewall, a `public` zone is created. See more
|
||||||
|
about this in the [example below](#end-device-protection).
|
||||||
|
|
||||||
|
> [!IMPORTANT] Remember IP forwarding on interfaces!
|
||||||
|
> Firewall policies only control whether traffic is allowed on input, to be
|
||||||
|
> forwarded, or blocked (default). For the actual routing between interfaces
|
||||||
|
> to work, you must also enable [IP forwarding](networking.md#ipv4-forwarding)
|
||||||
|
> on the relevant interfaces.
|
||||||
|
|
||||||
|
### Intra-Zone Traffic
|
||||||
|
|
||||||
|
Traffic between different interfaces, or networks, in the same zone is not
|
||||||
|
forwarded by default. In most cases, if devices on separate interfaces need
|
||||||
|
to communicate, they should be in different zones with a policy between them.
|
||||||
|
Alternatively, if you want true LAN-like behavior, [bridge the interfaces][1]
|
||||||
|
at layer-2 instead of routing between them at layer-3.
|
||||||
|
|
||||||
|
*Intra-zone* forwarding — routing traffic within a single zone — is rarely
|
||||||
|
needed. But if you do require it, create a policy where both ingress and
|
||||||
|
egress are set to the same zone, e.g., `lan` → `lan`.
|
||||||
|
|
||||||
|
### Port Forwarding
|
||||||
|
|
||||||
|
Port forwarding, also known as destination NAT (DNAT), redirects inbound
|
||||||
|
traffic to another address and/or port. This allows external access to
|
||||||
|
internal services. See [Enterprise Gateway](#enterprise-gateway) for an
|
||||||
|
example.
|
||||||
|
|
||||||
|
Each zone can have port forwarding rules that apply to traffic arriving at
|
||||||
|
that zone's interfaces or matching its networks. The forwarded traffic must
|
||||||
|
then be allowed by appropriate policies to reach the destination zone.
|
||||||
|
|
||||||
|
The *Zone Matrix* shows a ⚠ conditional warning flag, coloring the cell
|
||||||
|
yellow, when exceptions like port forwarding are active.
|
||||||
|
|
||||||
|
## Policies
|
||||||
|
|
||||||
|
In short, policy rules control traffic **between** zones. By default all
|
||||||
|
inter-zone (and intra-zone) traffic is rejected. Meaning you must explicitly
|
||||||
|
allow the traffic flows you intend.
|
||||||
|
|
||||||
|
{ width=600 }
|
||||||
|
/// figure-caption
|
||||||
|
Example of common traffic flows (policies) between zones.
|
||||||
|
///
|
||||||
|
|
||||||
|
IP masquerading (SNAT) is a policy setting that applies to traffic egressing
|
||||||
|
a target zone. (Essential for Internet access from private networks.)
|
||||||
|
|
||||||
|
A policy, like zones, have a default action. If it is *not* set to `accept`
|
||||||
|
you must specify which services on the host any zone interface and network are
|
||||||
|
allowed access to.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Policy rules apply in-order, the first matching rule with action `drop` will
|
||||||
|
> terminate the traffic flow. Use action `continue` to allow processing to go
|
||||||
|
> to the next rule, until the last (implicit) default-drop rule at the end.
|
||||||
|
>
|
||||||
|
> The CLI currently does not support reordering rules. As a workaround, save
|
||||||
|
> your `running-config` to `startup-config`, then exit to the shell and edit
|
||||||
|
> the file with `edit /cfg/startup-config.cfg`.
|
||||||
|
|
||||||
|
See the [examples below](#enterprise-gateway) for how to set up a policy. The
|
||||||
|
built-in help system can also be useful:
|
||||||
|
<code><pre>admin@example:/config/firewall/policy/lan-to-dmz/> <b>help masquerade</b>
|
||||||
|
<b>NAME</b>
|
||||||
|
masquerade <true/false><br/>
|
||||||
|
<b>DESCRIPTION</b>
|
||||||
|
Enable masquerading (SNAT) for traffic matching this policy.<br/>
|
||||||
|
Matching traffic will have their source IP address changed on egress,
|
||||||
|
using the IP address of the interface the traffic egresses.";<br/>
|
||||||
|
admin@example:/config/firewall/policy/lan-to-dmz/>
|
||||||
|
</pre></code>
|
||||||
|
|
||||||
|
### Symbolic Names
|
||||||
|
|
||||||
|
The symbolic names `HOST` and `ANY` are available for use in both `ingress`
|
||||||
|
and `egress` zones. In fact, the CLI uses inference when first enabling the
|
||||||
|
firewall to inject a default policy to allow automatic IPv6 address
|
||||||
|
assignment.
|
||||||
|
|
||||||
|
- `HOST` → `ANY`: Control device to any outbound connection (default: allowed)
|
||||||
|
- `ANY` → `HOST`: Control what can reach device services (uncommon, usually per-zone)
|
||||||
|
- Zone → `HOST`: Allow specific zone to access device services
|
||||||
|
|
||||||
|
### Custom Filters
|
||||||
|
|
||||||
|
For more advanced firewall scenarios *custom filters* can be used. Currently
|
||||||
|
only various ICMP type traffic control is supported. Enough to support the
|
||||||
|
built-in `allow-host-ipv6` policy and allow certain ICMP types on input or
|
||||||
|
forward.
|
||||||
|
|
||||||
|
You can inspect this built-in (locked) policy from admin-exec level with the
|
||||||
|
command: <kbd>show firewall policy allow-host-ipv6</kbd>.
|
||||||
|
|
||||||
|
### Default Behavior
|
||||||
|
|
||||||
|
ICMP messages (particularly `echo-request` and `echo-reply`) can be used to
|
||||||
|
reveal network information for malicious purposes. Therefore, the firewall
|
||||||
|
blocks ICMP requests by default. This applies unless the zone's default
|
||||||
|
action is `accept`.
|
||||||
|
|
||||||
|
To enable `echo-request` (IPv4) for any interface, or per zone when action is
|
||||||
|
set to drop or reject, set up a dedicated policy with `ingress ANY` and `egress
|
||||||
|
HOST` that use a custom filter to accept that ICMP type. Make this policy the
|
||||||
|
first rule in your list of policies, and remember to use `continue` for
|
||||||
|
non-matching traffic.
|
||||||
|
|
||||||
|
Another built-in behavior is automatically allowing "established,related"
|
||||||
|
return traffic flows. This uses connection tracking - the firewall remembers
|
||||||
|
outbound connections you initiate and automatically allows the corresponding
|
||||||
|
inbound response traffic. This means you only need to configure rules for
|
||||||
|
new connections; the firewall handles return traffic automatically without
|
||||||
|
additional rules.
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
Several pre-defined services exist, that cover most use-cases, but you can
|
||||||
|
also define custom services for applications not covered by the built-in ones.
|
||||||
|
|
||||||
|
The firewall includes over 100 pre-defined services, such as:
|
||||||
|
|
||||||
|
- **`ssh`** — Secure Shell (port 22/tcp)
|
||||||
|
- **`http`** — Web traffic (port 80/tcp)
|
||||||
|
- **`https`** — Secure web traffic (port 443/tcp)
|
||||||
|
- **`dns`** — Domain Name System (port 53/tcp and 53/udp)
|
||||||
|
- **`dhcp`** — DHCP server (port 67/udp)
|
||||||
|
- **`dhcpv6-client`** — DHCPv6 client traffic
|
||||||
|
- **`netconf`** — Network Configuration Protocol (port 830/tcp)
|
||||||
|
- **`restconf`** — REST-based Network Configuration Protocol (port 443/tcp)
|
||||||
|
|
||||||
|
> [!TIP] Use the ++question++ key in the CLI
|
||||||
|
> See the YANG model for the full list, or tap the ++question++ key
|
||||||
|
> when setting up an allowed host service in a zone `set service`
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### End Device Protection
|
||||||
|
|
||||||
|
This is the default firewall setup, useful for end devices on untrusted
|
||||||
|
networks. It provides maximum protection while allowing essential
|
||||||
|
connectivity.
|
||||||
|
<code><pre>admin@example:/> <b>configure</b>
|
||||||
|
admin@example:/config/> <b>edit firewall</b>
|
||||||
|
admin@example:/config/firewall/> <b>show</b>
|
||||||
|
default public;
|
||||||
|
zone public {
|
||||||
|
action reject;
|
||||||
|
description "Public, unknown network. Only SSH and DHCPv6 client allowed.";
|
||||||
|
service dhcpv6-client;
|
||||||
|
service ssh;
|
||||||
|
}
|
||||||
|
admin@example:/config/firewall/> <b>leave</b>
|
||||||
|
</pre></code>
|
||||||
|
|
||||||
|
The `reject` action differs from `drop` in that it responds to ICMP messages,
|
||||||
|
although maybe not how you may think. Pinging the device we may[^1] see this:
|
||||||
|
<code><pre>
|
||||||
|
<b>$</b> ping 192.168.122.161
|
||||||
|
From 192.168.122.161 icmp_seq=1 <u>Packet filtered</u>
|
||||||
|
</pre></code>
|
||||||
|
|
||||||
|
If we run `tcpdump` it shows us why:
|
||||||
|
<code><pre>
|
||||||
|
<b>$</b> tcpdump -lni eth0
|
||||||
|
20:10:40.245707 IP 192.168.122.1 > 192.168.122.161: ICMP echo request, id 56838, seq 1, length 64
|
||||||
|
20:10:40.245961 IP 192.168.122.161 > 192.168.122.1: ICMP <u>host 192.168.122.161 unreachable - admin prohibited filter</u>, length 92
|
||||||
|
</pre></code>
|
||||||
|
|
||||||
|
The key here is that, yes the device responds, but not with `ICMP reply` but
|
||||||
|
`ICMP unreachable`, and a little helpful message.
|
||||||
|
|
||||||
|
The default zone is `public`, so all interfaces that are not explicitly
|
||||||
|
assigned to another zone will be operationally placed in this zone as a
|
||||||
|
safeguard. Inspect this from admin-exec context with <kbd>show firewall</kbd>, as can
|
||||||
|
be seen in the below screenshot, the only interface `e1` has been assigned
|
||||||
|
automatically to the public zone. This information is also saved to the
|
||||||
|
system log.
|
||||||
|
|
||||||
|

|
||||||
|
/// figure-caption
|
||||||
|
Zone matrix and firewall overview from <kbd>show firewall</kbd>.
|
||||||
|
///
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> These defaults are *inferred* for interactive CLI users. Enabling the
|
||||||
|
> firewall using NETCONF/RESTCONF will not yield the same results.
|
||||||
|
|
||||||
|
[^1]: The output from ping clients differ A LOT. Some do not consider ICMP
|
||||||
|
unreachable to be a proper response and it will appear as if the device is
|
||||||
|
not responding at all. Use `tcpdump` or `wireshark` to get to the bottom
|
||||||
|
of network mysteries.
|
||||||
|
|
||||||
|
### Home/Office Router
|
||||||
|
|
||||||
|
For typical routers that need to protect internal devices while providing
|
||||||
|
internet access. The LAN zone trusts internal devices, while the WAN zone
|
||||||
|
blocks external threats.
|
||||||
|
<code><pre>admin@example:/> <b>configure</b>
|
||||||
|
admin@example:/config/> <b>edit firewall</b>
|
||||||
|
admin@example:/config/firewall/> <b>set default wan</b>
|
||||||
|
admin@example:/config/firewall/> <b>edit zone lan</b>
|
||||||
|
admin@example:/config/firewall/zone/lan/> <b>set description "Internal LAN network - trusted"</b>
|
||||||
|
admin@example:/config/firewall/zone/lan/> <b>set action accept</b>
|
||||||
|
admin@example:/config/firewall/zone/lan/> <b>set interface eth1</b>
|
||||||
|
admin@example:/config/firewall/zone/lan/> <b>set service ssh</b>
|
||||||
|
admin@example:/config/firewall/zone/lan/> <b>set service dhcp</b>
|
||||||
|
admin@example:/config/firewall/zone/lan/> <b>set service dns</b>
|
||||||
|
admin@example:/config/firewall/zone/lan/> <b>end</b>
|
||||||
|
admin@example:/config/firewall/> <b>edit zone wan</b>
|
||||||
|
admin@example:/config/firewall/zone/wan/> <b>set description "External WAN interface - untrusted"</b>
|
||||||
|
admin@example:/config/firewall/zone/wan/> <b>set action drop</b>
|
||||||
|
admin@example:/config/firewall/zone/wan/> <b>set interface eth0</b>
|
||||||
|
admin@example:/config/firewall/zone/wan/> <b>end</b>
|
||||||
|
admin@example:/config/firewall/> <b>edit policy loc-to-wan</b>
|
||||||
|
admin@example:/config/firewall/policy/loc-to-wan/> <b>set description "Allow LAN traffic to WAN with SNAT"</b>
|
||||||
|
admin@example:/config/firewall/policy/loc-to-wan/> <b>set ingress lan</b>
|
||||||
|
admin@example:/config/firewall/policy/loc-to-wan/> <b>set egress wan</b>
|
||||||
|
admin@example:/config/firewall/policy/loc-to-wan/> <b>set action accept</b>
|
||||||
|
admin@example:/config/firewall/policy/loc-to-wan/> <b>set masquerade</b>
|
||||||
|
admin@example:/config/firewall/policy/loc-to-wan/> <b>leave</b>
|
||||||
|
</pre></code>
|
||||||
|
|
||||||
|
### Enterprise Gateway
|
||||||
|
|
||||||
|
For businesses that need to host public services while protecting internal
|
||||||
|
resources. We can build upon the Home/Office Router example above and add
|
||||||
|
a DMZ zone with additional policies for controlled access.
|
||||||
|
<code><pre>admin@example:/> <b>configure</b>
|
||||||
|
admin@example:/config/> <b>edit firewall zone dmz</b>
|
||||||
|
admin@example:/config/firewall/zone/dmz/> <b>set description "Semi-trusted public services"</b>
|
||||||
|
admin@example:/config/firewall/zone/dmz/> <b>set action drop</b>
|
||||||
|
admin@example:/config/firewall/zone/dmz/> <b>set interface eth1</b>
|
||||||
|
admin@example:/config/firewall/zone/dmz/> <b>set service ssh</b>
|
||||||
|
admin@example:/config/firewall/zone/dmz/> <b>end</b>
|
||||||
|
admin@example:/config/firewall/> <b>edit policy loc-to-wan</b>
|
||||||
|
admin@example:/config/firewall/policy/loc-to-wan/> <b>set description "Allow local networks (LAN+DMZ) to WAN with SNAT"</b>
|
||||||
|
admin@example:/config/firewall/policy/loc-to-wan/> <b>set ingress dmz</b>
|
||||||
|
admin@example:/config/firewall/policy/loc-to-wan/> <b>set egress wan</b>
|
||||||
|
admin@example:/config/firewall/policy/loc-to-wan/> <b>set action accept</b>
|
||||||
|
admin@example:/config/firewall/policy/loc-to-wan/> <b>set masquerade</b>
|
||||||
|
admin@example:/config/firewall/policy/loc-to-wan/> <b>end</b>
|
||||||
|
admin@example:/config/firewall/> <b>edit policy lan-to-dmz</b>
|
||||||
|
admin@example:/config/firewall/policy/lan-to-dmz/> <b>set description "Allow LAN to manage DMZ services"</b>
|
||||||
|
admin@example:/config/firewall/policy/lan-to-dmz/> <b>set ingress lan</b>
|
||||||
|
admin@example:/config/firewall/policy/lan-to-dmz/> <b>set egress dmz</b>
|
||||||
|
admin@example:/config/firewall/policy/lan-to-dmz/> <b>set action accept</b>
|
||||||
|
admin@example:/config/firewall/policy/lan-to-dmz/> <b>end</b>
|
||||||
|
admin@example:/config/firewall/> <b>edit zone wan port-forward 8080 tcp</b>
|
||||||
|
admin@example:/config/firewall/zone/wan/port-forward/8080/tcp/> <b>set to addr 192.168.2.10</b>
|
||||||
|
admin@example:/config/firewall/zone/wan/port-forward/8080/tcp/> <b>set to port 80</b>
|
||||||
|
admin@example:/config/firewall/zone/wan/port-forward/8080/tcp/> <b>leave</b>
|
||||||
|
</pre></code>
|
||||||
|
|
||||||
|
This adds a DMZ zone for public services, updates the internet access policy
|
||||||
|
to include DMZ traffic, allows LAN management of DMZ services, and forwards
|
||||||
|
external web traffic to the DMZ server.
|
||||||
|
|
||||||
|
## Logging and Monitoring
|
||||||
|
|
||||||
|
Different log levels are available to monitor and debug firewall behavior.
|
||||||
|
Configure logging using the CLI:
|
||||||
|
<code><pre>
|
||||||
|
admin@example:/> <b>configure</b>
|
||||||
|
admin@example:/config/> <b>edit firewall</b>
|
||||||
|
admin@example:/config/firewall/> <b>set logging all</b>
|
||||||
|
admin@example:/config/firewall/> <b>leave</b>
|
||||||
|
</pre></code>
|
||||||
|
|
||||||
|
Firewall logs help you understand traffic patterns and security events. The
|
||||||
|
CLI admin-exec command <kbd>show firewall</kbd> shows the last 10 log messages in the
|
||||||
|
overview:
|
||||||
|
|
||||||
|
{width=100%}
|
||||||
|
/// figure-caption
|
||||||
|
Summary of recent logs at the end of <kbd>show firewall</kbd>.
|
||||||
|
///
|
||||||
|
|
||||||
|
Use the command <kbd>show log firewall.log</kbd> to display the full logfile
|
||||||
|
(remember, the syslog daemon rotates and zips too big log files). You can
|
||||||
|
also use the <kbd>follow firewall.log</kbd> command to continuously monitor
|
||||||
|
firewall log messages.
|
||||||
|
|
||||||
|
## Netfilter Integration
|
||||||
|
|
||||||
|
The Infix firewall operates through Linux netfilter hooks. Understanding how
|
||||||
|
the *zones* and *policy* concepts map to these hooks will hopefully help you
|
||||||
|
understand the firewall's behavior and ease troubleshooting.
|
||||||
|
|
||||||
|
### Packet Flow
|
||||||
|
|
||||||
|
{width=750}
|
||||||
|
/// figure-caption
|
||||||
|
Linux netfilter hooks in layer-3 traffic flow.
|
||||||
|
///
|
||||||
|
|
||||||
|
| **Netfilter Hook** | **Function** | **Description** |
|
||||||
|
|--------------------|--------------|--------------------------------------------------------------------------|
|
||||||
|
| `prerouting` | ZONE | Classification of incoming traffic, match interfaces/networks with zones |
|
||||||
|
| `prerouting` | ZONE | Port forwarding (DNAT) from zone configuration |
|
||||||
|
| `input` | ZONE | Host input filtering (`services`) |
|
||||||
|
| `input` | ZONE | Default action for non-matching services (`action`) |
|
||||||
|
| `forward` | POLICY | Allow traffic between zones (inter-zone rules) |
|
||||||
|
| `postrouting` | POLICY | Masquerade (SNAT) when traffic egresses a zone |
|
||||||
|
|
||||||
|
#### PREROUTING Hook
|
||||||
|
|
||||||
|
- **Zone Classification**: Traffic is tagged based on ingress interface or
|
||||||
|
source network
|
||||||
|
- **Port Forwarding**: DNAT from zone configuration occurs before routing decisions
|
||||||
|
- **Connection Tracking**: Early state establishment for stateful filtering
|
||||||
|
|
||||||
|
#### INPUT Hook
|
||||||
|
|
||||||
|
- **ANY-to-HOST Policies**: Enforces policy rules for traffic destined to the
|
||||||
|
host itself
|
||||||
|
- **Zone Services**: Allows configured services (SSH, HTTP, etc.) based on
|
||||||
|
zone trust level
|
||||||
|
- **Zone Action**: Applies a default action (accept/reject/drop) for
|
||||||
|
unmatched traffic
|
||||||
|
|
||||||
|
#### FORWARD Hook
|
||||||
|
|
||||||
|
- **Policy Enforcement**: Primary location for inter-zone traffic filtering
|
||||||
|
- **Custom Filters**: ICMP and other protocol-specific rules within policies
|
||||||
|
- **Service Matching**: Allows or denies services based on policy configuration
|
||||||
|
|
||||||
|
#### POSTROUTING Hook
|
||||||
|
|
||||||
|
- **Masquerading**: Source NAT for outbound traffic when policies enable masquerading
|
||||||
|
|
||||||
|
|
||||||
|
## Emergency Lockdown
|
||||||
|
|
||||||
|
For security emergencies (active breaches, suspicious activity), the firewall
|
||||||
|
supports an immediate lockdown mode that blocks ALL traffic.
|
||||||
|
|
||||||
|
> [!DANGER]
|
||||||
|
> This will immediately terminate all network connections, including SSH.
|
||||||
|
> Physical console access is required to restore normal operation. It is
|
||||||
|
> also possible to restore normal operation by power-cycling the device.
|
||||||
|
|
||||||
|
To activate emergency lockdown:
|
||||||
|
|
||||||
|
```json
|
||||||
|
~$ curl -kX POST -u admin:admin -H "Content-Type: application/yang-data+json" \
|
||||||
|
-d '{"infix-firewall:input": {"operation": "now"}}' \
|
||||||
|
https://example.local/restconf/operations/infix-firewall:firewall/lockdown-mode
|
||||||
|
```
|
||||||
|
|
||||||
|
To cancel lockdown mode (requires console access):
|
||||||
|
|
||||||
|
```json
|
||||||
|
~$ curl -kX POST -u admin:admin -H "Content-Type: application/yang-data+json" \
|
||||||
|
-d '{"infix-firewall:input": {"operation": "cancel"}}' \
|
||||||
|
https://example.local/restconf/operations/infix-firewall:firewall/lockdown-mode
|
||||||
|
```
|
||||||
|
|
||||||
|
You can check the current lockdown state:
|
||||||
|
|
||||||
|
```json
|
||||||
|
~$ curl -kX GET -u admin:admin -H 'Accept: application/yang-data+json' \
|
||||||
|
https://example.local/restconf/data/infix-firewall:firewall/lockdown
|
||||||
|
{
|
||||||
|
"infix-firewall:firewall": {
|
||||||
|
"lockdown": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
[1]: networking.md#bridging
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250.9 169.3" width="350" xmlns:v="https://vecta.io/nano"><path d="M128.1 92l-2.3 1.4-2.3-1.4c-.6-.4-7.6-4.7-16-13.2h-.2-2.7-10.2-.7H91h-2-9-19.1v21.5h19.2 9 4.7 13.7 35.3 13.7 9.8 3.9 1.3V78.8h-1.3-3.9-7.1-2.7-3.7-.8-7.9c-8.3 8.5-15.3 12.9-16 13.2zM17.5 143.7h49.7v22H17.5zm60.7 0h49.7v22H78.2zm-39.8-10.4h49.8v-22h-32-17.8zm60.2-22h49.7v22H98.6zM.7 79.3h49.7v21.5H.7zm175.8-32.4h-6.6-4.2c-3.7 8.6-8.2 15.9-12.8 22h8.7 44.5 3.2v-22h-32.8zm-138.1 0v22H99c-4.6-6.1-9.2-13.4-12.8-22H38.4z" fill="#aaa"/><g fill="#00be3b"><path d="M163 25.7c1.3-6 1.7-10.8 1.7-13.8v-.1-.9-.1c-7-3.5-14.1-6.2-21.2-7.8-5.4-1.3-11-2-16.6-2.1h-.1-1-1-.1c-5.6.1-11.2.8-16.6 2-7 1.7-14.1 4.4-21.1 7.9v1c.1 3.1.4 7.9 1.7 13.8 6.7 33.1 29.8 52 37.1 57.3 7.4-5.3 30.5-24.2 37.2-57.2z"/><circle cx="127.6" cy="38" r="26.9"/></g><path d="M122.2 51.2c-1 0-1.9-.4-2.6-1.2l-6.8-7.8c-1.2-1.4-1.1-3.6.3-4.8s3.6-1.1 4.8.3l4.3 4.9 15.1-16.7c1.3-1.4 3.4-1.5 4.8-.2s1.5 3.4.2 4.8L124.7 50c-.6.8-1.5 1.2-2.5 1.2z" fill="#fff"/><path d="M236.7 91.8l-6 28.2L218 73.5l-.1.3h0l-10.3 37.5-2.4 8.8-1.9-8.8-4.2-19.4c-2.5 5.2-5.4 12-8 19.4-2.4 7.1-4.4 14.7-5.2 22-.4 3.6-.5 7.1-.2 10.5.3 4.1 1.3 8 3 11.4.3.6.6 1.1.9 1.6 4.8 8 14.4 12.1 28.3 12.2l.1 2.6V169c13.9 0 23.4-4.1 28.3-12.2 4.6-7.6 5.2-18.7 2-33.1-2.7-11.7-7.4-23-11.6-31.9z" fill="#db9300"/><path d="M180.5 143.7h-42.1v22h50.3v-1.6c-1.4-1.4-2.6-3-3.6-4.7-2.6-4.4-4.2-9.6-4.6-15.7zm5.1-32.4h-8.4-18.3v22h21.9c.3-3.4.9-7 1.8-10.8.7-3.9 1.8-7.6 3-11.2z" fill="#aaa"/><path d="M217.4 117.4s-21.9 47.2.7 47.2c22.5 0-.7-47.2-.7-47.2" fill="#bd0001"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 281 KiB |
@@ -23,12 +23,13 @@ nav:
|
|||||||
- Keybindings: cli/keybindings.md
|
- Keybindings: cli/keybindings.md
|
||||||
- Network Calculator: cli/netcalc.md
|
- Network Calculator: cli/netcalc.md
|
||||||
- Network Monitoring: cli/tcpdump.md
|
- Network Monitoring: cli/tcpdump.md
|
||||||
- Quickstart Guide: cli/quick.md
|
- Quickstart Guide: cli/quick.md
|
||||||
- Text Editor: cli/text-editor.md
|
- Text Editor: cli/text-editor.md
|
||||||
- Upgrading: cli/upgrade.md
|
- Upgrading: cli/upgrade.md
|
||||||
- Docker Containers: container.md
|
- Docker Containers: container.md
|
||||||
- Networking:
|
- Networking:
|
||||||
- Network Configuration: networking.md
|
- Network Configuration: networking.md
|
||||||
|
- Firewall Configuration: firewall.md
|
||||||
- Quality of Service: qos.md
|
- Quality of Service: qos.md
|
||||||
- RMON Counters: eth-counters.md
|
- RMON Counters: eth-counters.md
|
||||||
- Tunneling (L2/L3): tunnels.md
|
- Tunneling (L2/L3): tunnels.md
|
||||||
@@ -98,10 +99,18 @@ theme:
|
|||||||
icon: material/weather-sunny
|
icon: material/weather-sunny
|
||||||
name: Switch to light mode
|
name: Switch to light mode
|
||||||
|
|
||||||
|
# https://squidfunk.github.io/mkdocs-material/reference/formatting/
|
||||||
markdown_extensions:
|
markdown_extensions:
|
||||||
- admonition
|
- admonition
|
||||||
- attr_list
|
- attr_list
|
||||||
- footnotes
|
- footnotes
|
||||||
|
- md_in_html
|
||||||
|
- pymdownx.blocks.caption
|
||||||
|
- pymdownx.critic
|
||||||
|
- pymdownx.caret
|
||||||
|
- pymdownx.keys
|
||||||
|
- pymdownx.mark
|
||||||
|
- pymdownx.tilde
|
||||||
- pymdownx.details
|
- pymdownx.details
|
||||||
- pymdownx.superfences
|
- pymdownx.superfences
|
||||||
- pymdownx.highlight:
|
- pymdownx.highlight:
|
||||||
@@ -118,6 +127,23 @@ plugins:
|
|||||||
- search
|
- search
|
||||||
- callouts
|
- callouts
|
||||||
- mike
|
- mike
|
||||||
|
- glightbox:
|
||||||
|
touchNavigation: true
|
||||||
|
loop: false
|
||||||
|
effect: zoom
|
||||||
|
slide_effect: slide
|
||||||
|
width: 100%
|
||||||
|
height: auto
|
||||||
|
zoomable: true
|
||||||
|
draggable: true
|
||||||
|
skip_classes:
|
||||||
|
- custom-skip-class-name
|
||||||
|
auto_themed: true
|
||||||
|
auto_caption: false
|
||||||
|
caption_position: bottom
|
||||||
|
background: black
|
||||||
|
shadow: false
|
||||||
|
manual: false
|
||||||
- to-pdf:
|
- to-pdf:
|
||||||
cover: true
|
cover: true
|
||||||
enabled_if_env: PDF_EXPORT
|
enabled_if_env: PDF_EXPORT
|
||||||
|
|||||||
@@ -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/factory/Config.in"
|
||||||
source "$BR2_EXTERNAL_INFIX_PATH/package/faux/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/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/greenpak-programmer/Config.in"
|
||||||
source "$BR2_EXTERNAL_INFIX_PATH/package/ifupdown-ng/Config.in"
|
source "$BR2_EXTERNAL_INFIX_PATH/package/ifupdown-ng/Config.in"
|
||||||
source "$BR2_EXTERNAL_INFIX_PATH/package/iito/Config.in"
|
source "$BR2_EXTERNAL_INFIX_PATH/package/iito/Config.in"
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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))
|
||||||
@@ -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>
|
||||||
@@ -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, "
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
From 03f273fc540082d1eaa23bd9b5847e695afd8283 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Joachim Wiberg <troglobit@gmail.com>
|
||||||
|
Date: Thu, 25 Sep 2025 15:00:54 +0200
|
||||||
|
Subject: [PATCH] Silence warnings about old backends
|
||||||
|
Organization: Wires
|
||||||
|
|
||||||
|
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
|
||||||
|
---
|
||||||
|
src/firewall/core/fw.py | 6 +++---
|
||||||
|
1 file changed, 3 insertions(+), 3 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/src/firewall/core/fw.py b/src/firewall/core/fw.py
|
||||||
|
index bf7c109a..90d556de 100644
|
||||||
|
--- a/src/firewall/core/fw.py
|
||||||
|
+++ b/src/firewall/core/fw.py
|
||||||
|
@@ -281,7 +281,7 @@ class Firewall:
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if self.nftables_enabled:
|
||||||
|
- log.info1(
|
||||||
|
+ log.debug1(
|
||||||
|
"iptables-restore and iptables are missing, "
|
||||||
|
"IPv4 direct rules won't be usable."
|
||||||
|
)
|
||||||
|
@@ -311,7 +311,7 @@ class Firewall:
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if self.nftables_enabled:
|
||||||
|
- log.info1(
|
||||||
|
+ log.debug1(
|
||||||
|
"ip6tables-restore and ip6tables are missing, "
|
||||||
|
"IPv6 direct rules won't be usable."
|
||||||
|
)
|
||||||
|
@@ -341,7 +341,7 @@ class Firewall:
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if self.nftables_enabled:
|
||||||
|
- log.info1(
|
||||||
|
+ log.debug1(
|
||||||
|
"ebtables-restore and ebtables are missing, "
|
||||||
|
"eb direct rules won't be usable."
|
||||||
|
)
|
||||||
|
--
|
||||||
|
2.43.0
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
pkglibexec_SCRIPTS = bootstrap error load gen-service gen-hostname \
|
pkglibexec_SCRIPTS = bootstrap error load gen-service gen-hostname \
|
||||||
gen-interfaces gen-motd gen-hardware gen-version \
|
gen-interfaces gen-motd gen-hardware gen-version \
|
||||||
mstpd-wait-online wait-interface
|
mstpd-wait-online wait-interface
|
||||||
sbin_SCRIPTS = dagger migrate
|
sbin_SCRIPTS = dagger migrate firewall
|
||||||
|
|||||||
@@ -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 "$@"
|
||||||
@@ -47,6 +47,7 @@ confd_plugin_la_SOURCES = \
|
|||||||
infix-dhcp-client.c \
|
infix-dhcp-client.c \
|
||||||
infix-dhcp-server.c \
|
infix-dhcp-server.c \
|
||||||
infix-factory.c \
|
infix-factory.c \
|
||||||
|
infix-firewall.c \
|
||||||
infix-meta.c \
|
infix-meta.c \
|
||||||
infix-services.c \
|
infix-services.c \
|
||||||
infix-system-software.c \
|
infix-system-software.c \
|
||||||
|
|||||||
@@ -172,6 +172,9 @@ int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv)
|
|||||||
if (rc)
|
if (rc)
|
||||||
goto err;
|
goto err;
|
||||||
rc = ietf_hardware_init(&confd);
|
rc = ietf_hardware_init(&confd);
|
||||||
|
if (rc)
|
||||||
|
goto err;
|
||||||
|
rc = infix_firewall_init(&confd);
|
||||||
if (rc)
|
if (rc)
|
||||||
goto err;
|
goto err;
|
||||||
|
|
||||||
|
|||||||
@@ -256,4 +256,7 @@ int ietf_hardware_init(struct confd *confd);
|
|||||||
/* ietf-keystore.c */
|
/* ietf-keystore.c */
|
||||||
int ietf_keystore_init(struct confd *confd);
|
int ietf_keystore_init(struct confd *confd);
|
||||||
|
|
||||||
|
/* infix-firewall.c */
|
||||||
|
int infix_firewall_init(struct confd *confd);
|
||||||
|
|
||||||
#endif /* CONFD_CORE_H_ */
|
#endif /* CONFD_CORE_H_ */
|
||||||
|
|||||||
@@ -908,6 +908,72 @@ cleanup:
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int ietf_interfaces_get_all_l3(const struct lyd_node *tree, char ***ifaces)
|
||||||
|
{
|
||||||
|
struct lyd_node *interfaces, *cif;
|
||||||
|
char **names = NULL;
|
||||||
|
size_t capacity = 0;
|
||||||
|
size_t num = 0;
|
||||||
|
const char *ifname;
|
||||||
|
|
||||||
|
if (!tree || !ifaces)
|
||||||
|
return -EINVAL;
|
||||||
|
|
||||||
|
*ifaces = NULL;
|
||||||
|
|
||||||
|
interfaces = lydx_get_descendant((struct lyd_node *)tree, "interfaces", "interface", NULL);
|
||||||
|
if (!interfaces) {
|
||||||
|
*ifaces = calloc(1, sizeof(char *));
|
||||||
|
return *ifaces ? 0 : -ENOMEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
LYX_LIST_FOR_EACH(interfaces, cif, "interface") {
|
||||||
|
ifname = lydx_get_cattr(cif, "name");
|
||||||
|
if (!ifname)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (is_member_port(cif))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (iftype_from_iface(cif) == IFT_LO)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (lydx_get_child(cif, "container-network"))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (num + 1 >= capacity) {
|
||||||
|
capacity = capacity ? capacity * 2 : 8;
|
||||||
|
char **new_names = realloc(names, capacity * sizeof(char *));
|
||||||
|
if (!new_names) {
|
||||||
|
for (size_t i = 0; i < num; i++)
|
||||||
|
free(names[i]);
|
||||||
|
free(names);
|
||||||
|
return -ENOMEM;
|
||||||
|
}
|
||||||
|
names = new_names;
|
||||||
|
}
|
||||||
|
|
||||||
|
names[num] = strdup(ifname);
|
||||||
|
if (!names[num]) {
|
||||||
|
for (size_t i = 0; i < num; i++)
|
||||||
|
free(names[i]);
|
||||||
|
free(names);
|
||||||
|
return -ENOMEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
num++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (num == 0) {
|
||||||
|
*ifaces = calloc(1, sizeof(char *));
|
||||||
|
return *ifaces ? 0 : -ENOMEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
names[num] = NULL;
|
||||||
|
*ifaces = names;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int ietf_interfaces_init(struct confd *confd)
|
int ietf_interfaces_init(struct confd *confd)
|
||||||
{
|
{
|
||||||
int rc;
|
int rc;
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ int netdag_gen_ethtool(struct dagger *net, struct lyd_node *cif, struct lyd_node
|
|||||||
/* ietf-interfaces.c */
|
/* ietf-interfaces.c */
|
||||||
const char *get_chassis_addr(void);
|
const char *get_chassis_addr(void);
|
||||||
int link_gen_address(struct lyd_node *cif, FILE *ip);
|
int link_gen_address(struct lyd_node *cif, FILE *ip);
|
||||||
|
int ietf_interfaces_get_all_l3(const struct lyd_node *tree, char ***ifaces);
|
||||||
|
|
||||||
/* ietf-ip.c */
|
/* ietf-ip.c */
|
||||||
int netdag_gen_ipv6_autoconf(struct dagger *net, struct lyd_node *cif,
|
int netdag_gen_ipv6_autoconf(struct dagger *net, struct lyd_node *cif,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -28,6 +28,9 @@ MODULES=(
|
|||||||
"infix-dhcp-common@2025-01-29.yang"
|
"infix-dhcp-common@2025-01-29.yang"
|
||||||
"infix-dhcp-client@2025-01-29.yang"
|
"infix-dhcp-client@2025-01-29.yang"
|
||||||
"infix-dhcp-server@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-meta@2024-10-18.yang"
|
||||||
"infix-system@2025-01-25.yang"
|
"infix-system@2025-01-25.yang"
|
||||||
"infix-services@2024-12-03.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
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
infix-firewall.yang
|
||||||
@@ -142,6 +142,61 @@ int infix_ifaces(kcontext_t *ctx)
|
|||||||
return 0;
|
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)
|
int infix_copy(kcontext_t *ctx)
|
||||||
{
|
{
|
||||||
kpargv_t *pargv = kcontext_pargv(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("erase", infix_erase));
|
||||||
kplugin_add_syms(plugin, ksym_new("files", infix_files));
|
kplugin_add_syms(plugin, ksym_new("files", infix_files));
|
||||||
kplugin_add_syms(plugin, ksym_new("ifaces", infix_ifaces));
|
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));
|
kplugin_add_syms(plugin, ksym_new("shell", infix_shell));
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -113,6 +113,27 @@
|
|||||||
<ACTION sym="STRING"/>
|
<ACTION sym="STRING"/>
|
||||||
</PTYPE>
|
</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">
|
<VIEW name="main">
|
||||||
<HOTKEY key="^D" cmd="exit"/>
|
<HOTKEY key="^D" cmd="exit"/>
|
||||||
|
|
||||||
@@ -490,6 +511,43 @@
|
|||||||
<COMMAND name="startup-config" help="Show startup-config">
|
<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>
|
<ACTION sym="script" in="tty" out="tty" interrupt="true">jq -C . /cfg/startup-config.cfg |pager</ACTION>
|
||||||
</COMMAND>
|
</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>
|
||||||
|
|
||||||
<COMMAND name="factory-reset" help="Restore the system to factory default state">
|
<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>
|
<ACTION sym="srp_rpc@sysrepo">/ietf-factory-default:factory-reset</ACTION>
|
||||||
</COMMAND>
|
</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">
|
<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"/>
|
<PARAM name="fn" ptype="/LOGFILES" help="Optional log file to monitor, default: syslog"/>
|
||||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
||||||
|
|||||||
@@ -149,28 +149,59 @@ bool srx_isset(sr_session_ctx_t *session, const char *fmt, ...)
|
|||||||
return isset;
|
return isset;
|
||||||
}
|
}
|
||||||
|
|
||||||
int srx_set_item(sr_session_ctx_t *session, const sr_val_t *val, sr_edit_options_t opts,
|
static int set_vaitem(sr_session_ctx_t *session, const sr_val_t *val, sr_edit_options_t opts,
|
||||||
const char *fmt, ...)
|
const char *fmt, va_list ap)
|
||||||
{
|
{
|
||||||
|
va_list apdup;
|
||||||
char *xpath;
|
char *xpath;
|
||||||
va_list ap;
|
|
||||||
size_t len;
|
size_t len;
|
||||||
|
|
||||||
va_start(ap, fmt);
|
va_copy(apdup, ap);
|
||||||
len = vsnprintf(NULL, 0, fmt, ap) + 1;
|
len = vsnprintf(NULL, 0, fmt, apdup) + 1;
|
||||||
va_end(ap);
|
va_end(apdup);
|
||||||
|
|
||||||
xpath = alloca(len);
|
xpath = alloca(len);
|
||||||
if (!xpath)
|
if (!xpath)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
va_start(ap, fmt);
|
va_copy(apdup, ap);
|
||||||
vsnprintf(xpath, len, fmt, ap);
|
vsnprintf(xpath, len, fmt, apdup);
|
||||||
va_end(ap);
|
va_end(apdup);
|
||||||
|
|
||||||
return sr_set_item(session, xpath, val, opts);
|
return sr_set_item(session, xpath, val, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int srx_set_item(sr_session_ctx_t *session, const sr_val_t *val, sr_edit_options_t opts,
|
||||||
|
const char *fmt, ...)
|
||||||
|
{
|
||||||
|
va_list ap;
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
va_start(ap, fmt);
|
||||||
|
rc = set_vaitem(session, val, opts, fmt, ap);
|
||||||
|
va_end(ap);
|
||||||
|
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
int srx_set_bool(sr_session_ctx_t *session, bool ena, sr_edit_options_t opts,
|
||||||
|
const char *fmt, ...)
|
||||||
|
{
|
||||||
|
sr_val_t val = {
|
||||||
|
.type = SR_BOOL_T,
|
||||||
|
.data.bool_val = ena
|
||||||
|
};
|
||||||
|
va_list ap;
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
va_start(ap, fmt);
|
||||||
|
rc = set_vaitem(session, &val, opts, fmt, ap);
|
||||||
|
va_end(ap);
|
||||||
|
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
int srx_set_str(sr_session_ctx_t *session, const char *str, sr_edit_options_t opts,
|
int srx_set_str(sr_session_ctx_t *session, const char *str, sr_edit_options_t opts,
|
||||||
const char *fmt, ...)
|
const char *fmt, ...)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ int srx_set_item(sr_session_ctx_t *, const sr_val_t *, sr_edit_options_t, const
|
|||||||
__attribute__ ((format (printf, 4, 5)));
|
__attribute__ ((format (printf, 4, 5)));
|
||||||
int srx_set_str(sr_session_ctx_t *, const char *, sr_edit_options_t, const char *fmt, ...)
|
int srx_set_str(sr_session_ctx_t *, const char *, sr_edit_options_t, const char *fmt, ...)
|
||||||
__attribute__ ((format (printf, 4, 5)));
|
__attribute__ ((format (printf, 4, 5)));
|
||||||
|
int srx_set_bool(sr_session_ctx_t *session, bool ena, sr_edit_options_t opts, const char *fmt, ...)
|
||||||
|
__attribute__ ((format (printf, 4, 5)));
|
||||||
|
|
||||||
char *srx_get_str (sr_session_ctx_t *session, const char *fmt, ...)
|
char *srx_get_str (sr_session_ctx_t *session, const char *fmt, ...)
|
||||||
__attribute__ ((format (printf, 2, 3)));
|
__attribute__ ((format (printf, 2, 3)));
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ def main():
|
|||||||
elif args.model == 'ieee802-dot1ab-lldp':
|
elif args.model == 'ieee802-dot1ab-lldp':
|
||||||
from . import infix_lldp
|
from . import infix_lldp
|
||||||
yang_data = infix_lldp.operational()
|
yang_data = infix_lldp.operational()
|
||||||
|
elif args.model == 'infix-firewall':
|
||||||
|
from . import infix_firewall
|
||||||
|
yang_data = infix_firewall.operational()
|
||||||
else:
|
else:
|
||||||
common.LOG.warning("Unsupported model %s", args.model)
|
common.LOG.warning("Unsupported model %s", args.model)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -43,6 +43,7 @@
|
|||||||
#define XPATH_CONTAIN_BASE "/infix-containers:containers"
|
#define XPATH_CONTAIN_BASE "/infix-containers:containers"
|
||||||
#define XPATH_DHCP_SERVER_BASE "/infix-dhcp-server:dhcp-server"
|
#define XPATH_DHCP_SERVER_BASE "/infix-dhcp-server:dhcp-server"
|
||||||
#define XPATH_LLDP_BASE "/ieee802-dot1ab-lldp:lldp"
|
#define XPATH_LLDP_BASE "/ieee802-dot1ab-lldp:lldp"
|
||||||
|
#define XPATH_FIREWALL_BASE "/infix-firewall:firewall"
|
||||||
|
|
||||||
TAILQ_HEAD(sub_head, sub);
|
TAILQ_HEAD(sub_head, sub);
|
||||||
|
|
||||||
@@ -356,6 +357,8 @@ static int subscribe_to_all(struct statd *statd)
|
|||||||
#endif
|
#endif
|
||||||
if (subscribe(statd, "infix-dhcp-server", XPATH_DHCP_SERVER_BASE, sr_generic_cb))
|
if (subscribe(statd, "infix-dhcp-server", XPATH_DHCP_SERVER_BASE, sr_generic_cb))
|
||||||
return SR_ERR_INTERNAL;
|
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");
|
INFO("Successfully subscribed to all models");
|
||||||
return SR_ERR_OK;
|
return SR_ERR_OK;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# shellcheck disable=SC2034,SC2154
|
# shellcheck disable=SC2034,SC2154
|
||||||
|
|
||||||
# Current container image
|
# Current container image
|
||||||
INFIX_TEST=ghcr.io/kernelkit/infix-test:2.4
|
INFIX_TEST=ghcr.io/kernelkit/infix-test:2.5
|
||||||
|
|
||||||
ixdir=$(readlink -f "$testdir/..")
|
ixdir=$(readlink -f "$testdir/..")
|
||||||
logdir=$(readlink -f "$testdir/.log")
|
logdir=$(readlink -f "$testdir/.log")
|
||||||
|
|||||||
@@ -34,6 +34,9 @@
|
|||||||
- name: "IETF Routing"
|
- name: "IETF Routing"
|
||||||
suite: ietf_routing/all.yaml
|
suite: ietf_routing/all.yaml
|
||||||
|
|
||||||
|
- name: "Infix Firewall"
|
||||||
|
suite: infix_firewall/all.yaml
|
||||||
|
|
||||||
- name: "Infix Containers"
|
- name: "Infix Containers"
|
||||||
suite: infix_containers/all.yaml
|
suite: infix_containers/all.yaml
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
:testgroup:
|
||||||
|
== infix-firewall
|
||||||
|
|
||||||
|
<<<
|
||||||
|
|
||||||
|
include::basic/Readme.adoc[]
|
||||||
|
|
||||||
|
<<<
|
||||||
|
|
||||||
|
include::lan-wan/Readme.adoc[]
|
||||||
|
|
||||||
|
<<<
|
||||||
|
|
||||||
|
include::wan-dmz-lan/Readme.adoc[]
|
||||||
|
|
||||||
|
<<<
|
||||||
|
|
||||||
|
include::ipv6-lan-wan/Readme.adoc[]
|
||||||
|
|
||||||
|
<<<
|
||||||
|
|
||||||
|
include::ipv6-zone-migration/Readme.adoc[]
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
- name: Basic Firewall for End Devices
|
||||||
|
case: basic/test.py
|
||||||
|
|
||||||
|
- name: LAN-WAN Firewall with Masquerading
|
||||||
|
case: lan-wan/test.py
|
||||||
|
|
||||||
|
- name: WAN-DMZ-LAN Firewall with Port Forwarding
|
||||||
|
case: wan-dmz-lan/test.py
|
||||||
|
|
||||||
|
- name: IPv6 LAN-WAN Firewall
|
||||||
|
case: ipv6-lan-wan/test.py
|
||||||
|
|
||||||
|
- name: IPv6 Zone Migration with Custom Services
|
||||||
|
case: ipv6-zone-migration/test.py
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
test.adoc
|
||||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,31 @@
|
|||||||
|
=== Basic Firewall for End Devices
|
||||||
|
|
||||||
|
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_firewall/basic]
|
||||||
|
|
||||||
|
==== Description
|
||||||
|
|
||||||
|
Firewall configuration suitable for end devices on untrusted networks.
|
||||||
|
|
||||||
|
image::basic.svg[align=center, scaledwidth=50%]
|
||||||
|
|
||||||
|
- Single zone configuration, "public", with action=drop
|
||||||
|
- Allowed services: SSH (port 22), DHCPv6-client, mySSH (custom, port 222)
|
||||||
|
- All other ports (HTTP, HTTPS, Telnet, etc.) blocked
|
||||||
|
- Verifies unused interfaces automatically assigned to default zone
|
||||||
|
|
||||||
|
==== Topology
|
||||||
|
|
||||||
|
image::topology.svg[Basic Firewall for End Devices topology, align=center, scaledwidth=75%]
|
||||||
|
|
||||||
|
==== Sequence
|
||||||
|
|
||||||
|
. Set up topology and attach to target
|
||||||
|
. Configure basic end-device firewall
|
||||||
|
. Verify unused interface assigned to default zone
|
||||||
|
. Verify ICMP is dropped
|
||||||
|
. Verify ICMPv6 is dropped
|
||||||
|
. Verify SSH service is allowed
|
||||||
|
. Verify custom mySSH service is allowed
|
||||||
|
. Verify other ports are blocked
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Basic Firewall for End Devices
|
||||||
|
|
||||||
|
Firewall configuration suitable for end devices on untrusted networks.
|
||||||
|
|
||||||
|
image::basic.svg[align=center, scaledwidth=50%]
|
||||||
|
|
||||||
|
- Single zone configuration, "public", with action=drop
|
||||||
|
- Allowed services: SSH (port 22), DHCPv6-client, mySSH (custom, port 222)
|
||||||
|
- All other ports (HTTP, HTTPS, Telnet, etc.) blocked
|
||||||
|
- Check that unused interfaces are automatically assigned to default zone
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import infamy
|
||||||
|
from infamy.util import until
|
||||||
|
|
||||||
|
|
||||||
|
with infamy.Test() as test:
|
||||||
|
with test.step("Set up topology and attach to target"):
|
||||||
|
env = infamy.Env()
|
||||||
|
target = env.attach("target", "mgmt")
|
||||||
|
_, data_if = env.ltop.xlate("target", "data")
|
||||||
|
_, mgmt_if = env.ltop.xlate("target", "mgmt")
|
||||||
|
_, unused_if = env.ltop.xlate("target", "unused")
|
||||||
|
_, host_data = env.ltop.xlate("host", "data")
|
||||||
|
TARGET_IP = "192.168.1.1"
|
||||||
|
HOST_IP = "192.168.1.42"
|
||||||
|
|
||||||
|
with test.step("Configure basic end-device firewall"):
|
||||||
|
target.put_config_dict("ietf-interfaces", {
|
||||||
|
"interfaces": {
|
||||||
|
"interface": [
|
||||||
|
{
|
||||||
|
"name": data_if,
|
||||||
|
"enabled": True,
|
||||||
|
"ipv4": {
|
||||||
|
"address": [{
|
||||||
|
"ip": TARGET_IP,
|
||||||
|
"prefix-length": 24
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
target.put_config_dict("infix-firewall", {
|
||||||
|
"firewall": {
|
||||||
|
"default": "public",
|
||||||
|
"logging": "all",
|
||||||
|
"service": [{
|
||||||
|
"name": "mySSH",
|
||||||
|
"port": [{
|
||||||
|
"lower": 222,
|
||||||
|
"proto": "tcp"
|
||||||
|
}]
|
||||||
|
}, {
|
||||||
|
"name": "http",
|
||||||
|
"port": [{
|
||||||
|
"lower": 8080,
|
||||||
|
"proto": "tcp"
|
||||||
|
}]
|
||||||
|
}],
|
||||||
|
"zone": [{
|
||||||
|
"name": "mgmt",
|
||||||
|
"description": "Management network - for test automation",
|
||||||
|
"action": "accept",
|
||||||
|
"interface": [mgmt_if],
|
||||||
|
"service": ["ssh", "netconf", "restconf"]
|
||||||
|
}, {
|
||||||
|
"name": "public",
|
||||||
|
"description": "Public untrusted network",
|
||||||
|
"action": "drop",
|
||||||
|
"interface": [data_if],
|
||||||
|
"service": ["ssh", "dhcpv6-client", "mySSH", "http"]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Wait for configuration to be activated
|
||||||
|
infamy.Firewall.wait_for_operational(target, {
|
||||||
|
"public": {"action": "drop"},
|
||||||
|
"mgmt": {"action": "accept"}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Verify firewall operational state
|
||||||
|
data = target.get_data("/infix-firewall:firewall")
|
||||||
|
fw = data["firewall"]
|
||||||
|
|
||||||
|
assert fw["default"] == "public"
|
||||||
|
|
||||||
|
services = {svc["name"]: svc for svc in fw.get("service", [])}
|
||||||
|
assert "mySSH" in services, "Custom service mySSH not found"
|
||||||
|
custom_service = services["mySSH"]
|
||||||
|
assert len(custom_service["port"]) == 1
|
||||||
|
port_entry = next(iter(custom_service["port"]))
|
||||||
|
assert port_entry["proto"] == "tcp"
|
||||||
|
assert int(port_entry["lower"]) == 222
|
||||||
|
|
||||||
|
assert "http" in services, "HTTP service override not found"
|
||||||
|
http_service = services["http"]
|
||||||
|
assert len(http_service["port"]) == 1
|
||||||
|
port_entry = next(iter(http_service["port"]))
|
||||||
|
assert port_entry["proto"] == "tcp"
|
||||||
|
assert int(port_entry["lower"]) == 8080
|
||||||
|
|
||||||
|
zones = {zone["name"]: zone for zone in fw["zone"]}
|
||||||
|
assert "public" in zones, "Public zone not found in configuration"
|
||||||
|
public_zone = zones["public"]
|
||||||
|
assert public_zone["action"] == "drop"
|
||||||
|
assert data_if in public_zone["interface"]
|
||||||
|
assert "ssh" in public_zone["service"]
|
||||||
|
assert "dhcpv6-client" in public_zone["service"]
|
||||||
|
assert "mySSH" in public_zone["service"]
|
||||||
|
assert "http" in public_zone["service"]
|
||||||
|
|
||||||
|
with test.step("Verify unused interface assigned to default zone"):
|
||||||
|
data = target.get_data("/infix-firewall:firewall")
|
||||||
|
fw = data["firewall"]
|
||||||
|
|
||||||
|
assert fw["default"] == "public", "Default zone should be 'public'"
|
||||||
|
|
||||||
|
zones = {zone["name"]: zone for zone in fw["zone"]}
|
||||||
|
public_zone = zones["public"]
|
||||||
|
|
||||||
|
assert unused_if in public_zone["interface"], \
|
||||||
|
f"Unused interface {unused_if} should be in default zone 'public', got interfaces: {public_zone['interface']}"
|
||||||
|
|
||||||
|
with infamy.IsolatedMacVlan(host_data) as ns:
|
||||||
|
ns.addip(HOST_IP)
|
||||||
|
|
||||||
|
with test.step("Verify ICMP is dropped"):
|
||||||
|
ns.must_not_reach(TARGET_IP, timeout=2)
|
||||||
|
|
||||||
|
with test.step("Verify ICMPv6 is dropped"):
|
||||||
|
ns.must_not_reach("fe80::1%iface", timeout=2)
|
||||||
|
|
||||||
|
with test.step("Verify SSH service is allowed"):
|
||||||
|
scanner = infamy.PortScanner(ns)
|
||||||
|
ssh_result = scanner.scan_port(TARGET_IP, 22, timeout=2)
|
||||||
|
assert ssh_result["status"] in ["open", "closed"], \
|
||||||
|
f"SSH port should be allowed, got: {ssh_result['status']}"
|
||||||
|
|
||||||
|
with test.step("Verify custom mySSH service is allowed"):
|
||||||
|
scanner = infamy.PortScanner(ns)
|
||||||
|
myssh_result = scanner.scan_port(TARGET_IP, 222, timeout=2)
|
||||||
|
assert myssh_result["status"] in ["open", "closed"], \
|
||||||
|
f"mySSH port 222 should be allowed, got: {myssh_result['status']}"
|
||||||
|
|
||||||
|
with test.step("Verify HTTP service override (8080 allowed, 80 blocked)"):
|
||||||
|
scanner = infamy.PortScanner(ns)
|
||||||
|
|
||||||
|
# Custom HTTP on port 8080 should be allowed
|
||||||
|
http_custom_result = scanner.scan_port(TARGET_IP, 8080, timeout=2)
|
||||||
|
assert http_custom_result["status"] in ["open", "closed"], \
|
||||||
|
f"Custom HTTP port 8080 should be allowed, got: {http_custom_result['status']}"
|
||||||
|
|
||||||
|
# Built-in HTTP on port 80 should be blocked (filtered)
|
||||||
|
http_builtin_result = scanner.scan_port(TARGET_IP, 80, timeout=2)
|
||||||
|
assert http_builtin_result["status"] == "filtered", \
|
||||||
|
f"Built-in HTTP port 80 should be blocked, got: {http_builtin_result['status']}"
|
||||||
|
|
||||||
|
with test.step("Verify other ports are blocked"):
|
||||||
|
firewall = infamy.Firewall(ns, None)
|
||||||
|
allowed = [22, 222, 8080]
|
||||||
|
|
||||||
|
ok, open_ports, filtered = \
|
||||||
|
firewall.verify_blocked(TARGET_IP, exempt=allowed)
|
||||||
|
if not ok:
|
||||||
|
if open_ports:
|
||||||
|
print(f"Unexpected open ports: {', '.join(open_ports)}")
|
||||||
|
if filtered:
|
||||||
|
print(f"Unexpected, filtered ports: {', '.join(filtered)}")
|
||||||
|
test.fail()
|
||||||
|
|
||||||
|
test.succeed()
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
graph "1x3" {
|
||||||
|
layout = "neato";
|
||||||
|
overlap = false;
|
||||||
|
esep = "+80";
|
||||||
|
|
||||||
|
node [shape=record, fontname="DejaVu Sans Mono, Book"];
|
||||||
|
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
|
||||||
|
|
||||||
|
host [
|
||||||
|
label="host | { <mgmt> mgmt | <data> data }",
|
||||||
|
pos="1,1!",
|
||||||
|
requires="controller"
|
||||||
|
];
|
||||||
|
|
||||||
|
target [
|
||||||
|
label="{ <mgmt> mgmt | <data> data | <unused> unused } | target",
|
||||||
|
pos="3,1!",
|
||||||
|
requires="infix",
|
||||||
|
];
|
||||||
|
|
||||||
|
dummy [
|
||||||
|
label="{ <link> link } | dummy",
|
||||||
|
pos="5,1!",
|
||||||
|
requires="infix",
|
||||||
|
];
|
||||||
|
|
||||||
|
host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"]
|
||||||
|
host:data -- target:data [color=black, fontcolor=black, taillabel="192.168.1.42/24"]
|
||||||
|
target:unused -- dummy:link [color="gray", style="dashed"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
|
||||||
|
<!-- Title: 1x3 Pages: 1 -->
|
||||||
|
<svg width="440pt" height="78pt"
|
||||||
|
viewBox="0.00 0.00 440.03 78.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 74)">
|
||||||
|
<title>1x3</title>
|
||||||
|
<polygon fill="white" stroke="transparent" points="-4,4 -4,-74 436.03,-74 436.03,4 -4,4"/>
|
||||||
|
<!-- host -->
|
||||||
|
<g id="node1" class="node">
|
||||||
|
<title>host</title>
|
||||||
|
<polygon fill="none" stroke="black" points="0,-12 0,-58 100,-58 100,-12 0,-12"/>
|
||||||
|
<text text-anchor="middle" x="25" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
|
||||||
|
<polyline fill="none" stroke="black" points="50,-12 50,-58 "/>
|
||||||
|
<text text-anchor="middle" x="75" y="-42.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||||
|
<polyline fill="none" stroke="black" points="50,-35 100,-35 "/>
|
||||||
|
<text text-anchor="middle" x="75" y="-19.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
|
||||||
|
</g>
|
||||||
|
<!-- target -->
|
||||||
|
<g id="node2" class="node">
|
||||||
|
<title>target</title>
|
||||||
|
<polygon fill="none" stroke="black" points="300.03,-0.5 300.03,-69.5 432.03,-69.5 432.03,-0.5 300.03,-0.5"/>
|
||||||
|
<text text-anchor="middle" x="333.03" y="-54.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||||
|
<polyline fill="none" stroke="black" points="300.03,-46.5 366.03,-46.5 "/>
|
||||||
|
<text text-anchor="middle" x="333.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data</text>
|
||||||
|
<polyline fill="none" stroke="black" points="300.03,-23.5 366.03,-23.5 "/>
|
||||||
|
<text text-anchor="middle" x="333.03" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">unused</text>
|
||||||
|
<polyline fill="none" stroke="black" points="366.03,-0.5 366.03,-69.5 "/>
|
||||||
|
<text text-anchor="middle" x="399.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
|
||||||
|
</g>
|
||||||
|
<!-- host--target -->
|
||||||
|
<g id="edge1" class="edge">
|
||||||
|
<title>host:mgmt--target:mgmt</title>
|
||||||
|
<path fill="none" stroke="lightgray" stroke-width="2" d="M100,-47C100,-47 300.03,-58 300.03,-58"/>
|
||||||
|
</g>
|
||||||
|
<!-- host--target -->
|
||||||
|
<g id="edge2" class="edge">
|
||||||
|
<title>host:data--target:data</title>
|
||||||
|
<path fill="none" stroke="black" stroke-width="2" d="M100,-23C100,-23 300.03,-35 300.03,-35"/>
|
||||||
|
<text text-anchor="middle" x="159" y="-26.8" font-family="DejaVu Serif, Book" font-size="14.00">192.168.1.42/24</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1 @@
|
|||||||
|
test.adoc
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
../lan-wan/lan-wan.svg
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
=== IPv6 LAN-WAN Firewall
|
||||||
|
|
||||||
|
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_firewall/ipv6-lan-wan]
|
||||||
|
|
||||||
|
==== Description
|
||||||
|
|
||||||
|
IPv6 version of the typical home/office router scenario where the DUT acts as
|
||||||
|
a gateway with LAN-to-WAN traffic forwarding and IPv6 prefix delegation.
|
||||||
|
|
||||||
|
image::lan-wan.svg[align=center, scaledwidth=50%]
|
||||||
|
|
||||||
|
- DUT/Gateway with IPv6 firewall and forwarding
|
||||||
|
- Test host has two interfaces: a LAN-side and a WAN-side (Internet)
|
||||||
|
- Test host's LAN interface acts as an IPv6 client behind the router
|
||||||
|
- Test host's WAN interface acts as an IPv6 Internet server/destination
|
||||||
|
- Demonstrates IPv6 policy-based forwarding between zones
|
||||||
|
|
||||||
|
==== Topology
|
||||||
|
|
||||||
|
image::topology.svg[IPv6 LAN-WAN Firewall topology, align=center, scaledwidth=75%]
|
||||||
|
|
||||||
|
==== Sequence
|
||||||
|
|
||||||
|
. Set up topology and attach to gateway
|
||||||
|
. Configure gateway with firewall and forwarding
|
||||||
|
. Test connectivity to gateway
|
||||||
|
. Test LAN-to-WAN forwarding
|
||||||
|
. Test WAN-to-LAN blocking
|
||||||
|
. Verify LAN services accessibility
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""IPv6 LAN-WAN Firewall
|
||||||
|
|
||||||
|
IPv6 version of the typical home/office router scenario where the DUT acts as
|
||||||
|
a gateway with LAN-to-WAN traffic forwarding and IPv6 prefix delegation.
|
||||||
|
|
||||||
|
image::lan-wan.svg[align=center, scaledwidth=50%]
|
||||||
|
|
||||||
|
- DUT/Gateway with IPv6 firewall and forwarding
|
||||||
|
- Test host has two interfaces: a LAN-side and a WAN-side (Internet)
|
||||||
|
- Test host's LAN interface acts as an IPv6 client behind the router
|
||||||
|
- Test host's WAN interface acts as an IPv6 Internet server/destination
|
||||||
|
- Demonstrates IPv6 policy-based forwarding between zones
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import infamy
|
||||||
|
from infamy.util import until
|
||||||
|
|
||||||
|
|
||||||
|
with infamy.Test() as test:
|
||||||
|
with test.step("Set up topology and attach to gateway"):
|
||||||
|
env = infamy.Env()
|
||||||
|
gateway = env.attach("gateway", "mgmt")
|
||||||
|
_, lan_if = env.ltop.xlate("gateway", "lan")
|
||||||
|
_, wan_if = env.ltop.xlate("gateway", "wan")
|
||||||
|
_, mgmt_if = env.ltop.xlate("gateway", "mgmt")
|
||||||
|
_, host_lan = env.ltop.xlate("host", "lan") # Host LAN-side interface
|
||||||
|
_, host_wan = env.ltop.xlate("host", "wan") # Host WAN-side interface
|
||||||
|
|
||||||
|
LAN_NET = "fd01:db8:1::/64"
|
||||||
|
LAN_ROUTER_IP = "fd01:db8:1::1" # Router's LAN interface
|
||||||
|
LAN_CLIENT_IP = "fd01:db8:1::100" # Client on LAN side
|
||||||
|
|
||||||
|
WAN_NET = "2001:db8:2::/64" # RFC 3849 documentation prefix
|
||||||
|
WAN_ROUTER_IP = "2001:db8:2::1" # Router's WAN interface
|
||||||
|
WAN_SERVER_IP = "2001:db8:2::100" # Server on WAN side
|
||||||
|
|
||||||
|
with test.step("Configure gateway with firewall and forwarding"):
|
||||||
|
gateway.put_config_dicts({
|
||||||
|
"ietf-interfaces": {
|
||||||
|
"interfaces": {
|
||||||
|
"interface": [{
|
||||||
|
"name": lan_if,
|
||||||
|
"enabled": True,
|
||||||
|
"ipv6": {
|
||||||
|
"enabled": True,
|
||||||
|
"forwarding": True,
|
||||||
|
"address": [{
|
||||||
|
"ip": LAN_ROUTER_IP,
|
||||||
|
"prefix-length": 64
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"name": wan_if,
|
||||||
|
"enabled": True,
|
||||||
|
"ipv6": {
|
||||||
|
"enabled": True,
|
||||||
|
"forwarding": True,
|
||||||
|
"address": [{
|
||||||
|
"ip": WAN_ROUTER_IP,
|
||||||
|
"prefix-length": 64
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"infix-firewall": {
|
||||||
|
"firewall": {
|
||||||
|
"default": "wan",
|
||||||
|
"logging": "all",
|
||||||
|
"zone": [{
|
||||||
|
"name": "lan",
|
||||||
|
"description": "Internal LAN network - trusted",
|
||||||
|
"action": "accept",
|
||||||
|
"interface": [lan_if, mgmt_if],
|
||||||
|
"service": ["ssh", "dhcpv6", "dns"]
|
||||||
|
}, {
|
||||||
|
"name": "wan",
|
||||||
|
"description": "External WAN interface - untrusted",
|
||||||
|
"action": "drop",
|
||||||
|
"interface": [wan_if]
|
||||||
|
}],
|
||||||
|
"policy": [{
|
||||||
|
"name": "lan-to-wan",
|
||||||
|
"description": "Allow LAN to WAN traffic",
|
||||||
|
"ingress": ["lan"],
|
||||||
|
"egress": ["wan"],
|
||||||
|
"action": "accept"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Wait for configuration to be activated
|
||||||
|
infamy.Firewall.wait_for_operational(gateway, {
|
||||||
|
"lan": {"action": "accept"},
|
||||||
|
"wan": {"action": "drop"}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Verify firewall operational state
|
||||||
|
data = gateway.get_data("/infix-firewall:firewall")
|
||||||
|
fw = data["firewall"]
|
||||||
|
zones = {z["name"]: z for z in fw["zone"]}
|
||||||
|
|
||||||
|
# Verify LAN zone
|
||||||
|
lan_zone = zones["lan"]
|
||||||
|
assert lan_zone["action"] == "accept"
|
||||||
|
assert lan_if in lan_zone["interface"]
|
||||||
|
|
||||||
|
# Verify WAN zone
|
||||||
|
wan_zone = zones["wan"]
|
||||||
|
assert wan_zone["action"] == "drop"
|
||||||
|
assert wan_if in wan_zone["interface"]
|
||||||
|
|
||||||
|
# Verify policy exists
|
||||||
|
policies = {p["name"]: p for p in fw.get("policy", [])}
|
||||||
|
assert "lan-to-wan" in policies
|
||||||
|
policy = policies["lan-to-wan"]
|
||||||
|
assert "lan" in policy["ingress"]
|
||||||
|
assert "wan" in policy["egress"]
|
||||||
|
assert policy["action"] == "accept"
|
||||||
|
|
||||||
|
with infamy.IsolatedMacVlan(host_lan) as lan_ns:
|
||||||
|
lan_ns.addip(LAN_CLIENT_IP, prefix_length=64, proto="ipv6")
|
||||||
|
lan_ns.addroute("default", LAN_ROUTER_IP, proto="ipv6")
|
||||||
|
|
||||||
|
with infamy.IsolatedMacVlan(host_wan) as wan_ns:
|
||||||
|
wan_ns.addip(WAN_SERVER_IP, prefix_length=64, proto="ipv6")
|
||||||
|
wan_ns.addroute("default", WAN_ROUTER_IP, proto="ipv6")
|
||||||
|
|
||||||
|
with test.step("Test connectivity to gateway"):
|
||||||
|
lan_ns.must_reach(LAN_ROUTER_IP, timeout=5)
|
||||||
|
wan_ns.must_not_reach(WAN_ROUTER_IP, timeout=5)
|
||||||
|
|
||||||
|
with test.step("Test LAN-to-WAN forwarding"):
|
||||||
|
lan_ns.must_reach(WAN_SERVER_IP, timeout=10)
|
||||||
|
|
||||||
|
with test.step("Test WAN-to-LAN blocking"):
|
||||||
|
wan_ns.must_not_reach(LAN_CLIENT_IP, timeout=5)
|
||||||
|
|
||||||
|
with test.step("Verify LAN services accessibility"):
|
||||||
|
firewall_lan = infamy.Firewall(lan_ns, None)
|
||||||
|
svc = [
|
||||||
|
(22, "tcp", "ssh"),
|
||||||
|
(53, "tcp", "dns")
|
||||||
|
]
|
||||||
|
|
||||||
|
ok, ports = firewall_lan.verify_allowed(LAN_ROUTER_IP, svc)
|
||||||
|
if not ok:
|
||||||
|
print(f" ⚠ Some LAN services are filtered: {', '.join(ports)}")
|
||||||
|
test.fail()
|
||||||
|
|
||||||
|
test.succeed()
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
graph "lan-wan-v6" {
|
||||||
|
layout = "neato";
|
||||||
|
overlap = false;
|
||||||
|
esep = "+80";
|
||||||
|
|
||||||
|
node [shape=record, fontname="DejaVu Sans Mono, Book"];
|
||||||
|
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
|
||||||
|
|
||||||
|
host [
|
||||||
|
label="host | { <mgmt> mgmt | <lan> lan | <wan> wan }",
|
||||||
|
pos="1,1!",
|
||||||
|
requires="controller"
|
||||||
|
];
|
||||||
|
|
||||||
|
gateway [
|
||||||
|
label="{ <mgmt> mgmt | <lan> lan | <wan> wan } | gateway",
|
||||||
|
pos="3,1!",
|
||||||
|
requires="infix",
|
||||||
|
];
|
||||||
|
|
||||||
|
host:mgmt -- gateway:mgmt [requires="mgmt", color="lightgray"]
|
||||||
|
host:lan -- gateway:lan [color=green, fontcolor=green, taillabel="fd01:db8:1::/64"]
|
||||||
|
host:wan -- gateway:wan [color=red, fontcolor=red, taillabel="2001:db8:2::/64"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
|
||||||
|
<!-- Title: lan-wan-v6 Pages: 1 -->
|
||||||
|
<svg width="432pt" height="78pt"
|
||||||
|
viewBox="0.00 0.00 432.03 78.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 74)">
|
||||||
|
<title>lan-wan-v6</title>
|
||||||
|
<polygon fill="white" stroke="transparent" points="-4,4 -4,-74 428.03,-74 428.03,4 -4,4"/>
|
||||||
|
<!-- host -->
|
||||||
|
<g id="node1" class="node">
|
||||||
|
<title>host</title>
|
||||||
|
<polygon fill="none" stroke="black" points="0,-0.5 0,-69.5 100,-69.5 100,-0.5 0,-0.5"/>
|
||||||
|
<text text-anchor="middle" x="25" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
|
||||||
|
<polyline fill="none" stroke="black" points="50,-0.5 50,-69.5 "/>
|
||||||
|
<text text-anchor="middle" x="75" y="-54.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||||
|
<polyline fill="none" stroke="black" points="50,-46.5 100,-46.5 "/>
|
||||||
|
<text text-anchor="middle" x="75" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">lan</text>
|
||||||
|
<polyline fill="none" stroke="black" points="50,-23.5 100,-23.5 "/>
|
||||||
|
<text text-anchor="middle" x="75" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">wan</text>
|
||||||
|
</g>
|
||||||
|
<!-- gateway -->
|
||||||
|
<g id="node2" class="node">
|
||||||
|
<title>gateway</title>
|
||||||
|
<polygon fill="none" stroke="black" points="300.03,-0.5 300.03,-69.5 424.03,-69.5 424.03,-0.5 300.03,-0.5"/>
|
||||||
|
<text text-anchor="middle" x="325.03" y="-54.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||||
|
<polyline fill="none" stroke="black" points="300.03,-46.5 350.03,-46.5 "/>
|
||||||
|
<text text-anchor="middle" x="325.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">lan</text>
|
||||||
|
<polyline fill="none" stroke="black" points="300.03,-23.5 350.03,-23.5 "/>
|
||||||
|
<text text-anchor="middle" x="325.03" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">wan</text>
|
||||||
|
<polyline fill="none" stroke="black" points="350.03,-0.5 350.03,-69.5 "/>
|
||||||
|
<text text-anchor="middle" x="387.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">gateway</text>
|
||||||
|
</g>
|
||||||
|
<!-- host--gateway -->
|
||||||
|
<g id="edge1" class="edge">
|
||||||
|
<title>host:mgmt--gateway:mgmt</title>
|
||||||
|
<path fill="none" stroke="lightgray" stroke-width="2" d="M100,-58C100,-58 300.03,-58 300.03,-58"/>
|
||||||
|
</g>
|
||||||
|
<!-- host--gateway -->
|
||||||
|
<g id="edge2" class="edge">
|
||||||
|
<title>host:lan--gateway:lan</title>
|
||||||
|
<path fill="none" stroke="green" stroke-width="2" d="M100,-35C100,-35 300.03,-35 300.03,-35"/>
|
||||||
|
<text text-anchor="middle" x="154.5" y="-38.8" font-family="DejaVu Serif, Book" font-size="14.00" fill="green">fd01:db8:1::/64</text>
|
||||||
|
</g>
|
||||||
|
<!-- host--gateway -->
|
||||||
|
<g id="edge3" class="edge">
|
||||||
|
<title>host:wan--gateway:wan</title>
|
||||||
|
<path fill="none" stroke="red" stroke-width="2" d="M100,-12C100,-12 300.03,-12 300.03,-12"/>
|
||||||
|
<text text-anchor="middle" x="156.5" y="-15.8" font-family="DejaVu Serif, Book" font-size="14.00" fill="red">2001:db8:2::/64</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1 @@
|
|||||||
|
test.adoc
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
=== IPv6 Zone Migration with Custom Services
|
||||||
|
|
||||||
|
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_firewall/ipv6-zone-migration]
|
||||||
|
|
||||||
|
==== Description
|
||||||
|
|
||||||
|
This test verifies that firewall rules work consistently across IPv4/IPv6
|
||||||
|
protocols and that interfaces can be moved between zones without breaking
|
||||||
|
active connections.
|
||||||
|
|
||||||
|
- Requires DUT with at least 2 data interfaces supporting IPv6
|
||||||
|
- Test host must support dual-stack IPv4/IPv6 configuration
|
||||||
|
- Custom service ports (8080/tcp) should be available for testing
|
||||||
|
|
||||||
|
==== Topology
|
||||||
|
|
||||||
|
image::topology.svg[IPv6 Zone Migration with Custom Services topology, align=center, scaledwidth=75%]
|
||||||
|
|
||||||
|
==== Sequence
|
||||||
|
|
||||||
|
. Set up topology and attach to target
|
||||||
|
. Configure dual-stack interfaces and initial firewall
|
||||||
|
. Verify initial zone configuration and custom service
|
||||||
|
. Verify IPv4/IPv6 connectivity and custom service restrictions
|
||||||
|
. Verify IPv6 custom service functionality
|
||||||
|
. Perform dynamic zone migration
|
||||||
|
. Verify connectivity after zone migration
|
||||||
|
. Verify custom service from migrated interface
|
||||||
|
. Verify operational state reflects zone changes
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""IPv6 Zone Migration with Custom Service
|
||||||
|
|
||||||
|
This test verifies that firewall rules work consistently across IPv4/IPv6
|
||||||
|
protocols and that interfaces can be moved between zones without breaking
|
||||||
|
active connections.
|
||||||
|
|
||||||
|
- Requires DUT with at least 2 data interfaces supporting IPv6
|
||||||
|
- Test host must support dual-stack IPv4/IPv6 configuration
|
||||||
|
- Custom service ports (8080/tcp) should be available for testing
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import infamy
|
||||||
|
from infamy.util import until
|
||||||
|
|
||||||
|
|
||||||
|
with infamy.Test() as test:
|
||||||
|
with test.step("Set up topology and attach to target"):
|
||||||
|
env = infamy.Env()
|
||||||
|
target = env.attach("target", "mgmt")
|
||||||
|
_, data1_if = env.ltop.xlate("target", "data1")
|
||||||
|
_, data2_if = env.ltop.xlate("target", "data2")
|
||||||
|
_, mgmt_if = env.ltop.xlate("target", "mgmt")
|
||||||
|
_, host_data1 = env.ltop.xlate("host", "data1")
|
||||||
|
_, host_data2 = env.ltop.xlate("host", "data2")
|
||||||
|
|
||||||
|
# IPv4 addressing
|
||||||
|
DATA1_NET_V4 = "10.1.1.0/24"
|
||||||
|
DATA1_TARGET_V4 = "10.1.1.1"
|
||||||
|
DATA1_HOST_V4 = "10.1.1.100"
|
||||||
|
|
||||||
|
DATA2_NET_V4 = "10.2.2.0/24"
|
||||||
|
DATA2_TARGET_V4 = "10.2.2.1"
|
||||||
|
DATA2_HOST_V4 = "10.2.2.100"
|
||||||
|
|
||||||
|
# IPv6 addressing
|
||||||
|
DATA1_NET_V6 = "fd01:1:1::/64"
|
||||||
|
DATA1_TARGET_V6 = "fd01:1:1::1"
|
||||||
|
DATA1_HOST_V6 = "fd01:1:1::100"
|
||||||
|
|
||||||
|
DATA2_NET_V6 = "fd02:2:2::/64"
|
||||||
|
DATA2_TARGET_V6 = "fd02:2:2::1"
|
||||||
|
DATA2_HOST_V6 = "fd02:2:2::100"
|
||||||
|
|
||||||
|
# Custom service port
|
||||||
|
CUSTOM_PORT = 8080
|
||||||
|
|
||||||
|
with test.step("Configure dual-stack interfaces and initial firewall"):
|
||||||
|
target.put_config_dicts({
|
||||||
|
"ietf-interfaces": {
|
||||||
|
"interfaces": {
|
||||||
|
"interface": [{
|
||||||
|
"name": data1_if,
|
||||||
|
"enabled": True,
|
||||||
|
"ipv4": {
|
||||||
|
"address": [{
|
||||||
|
"ip": DATA1_TARGET_V4,
|
||||||
|
"prefix-length": 24
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
"ipv6": {
|
||||||
|
"enabled": True,
|
||||||
|
"address": [{
|
||||||
|
"ip": DATA1_TARGET_V6,
|
||||||
|
"prefix-length": 64
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"name": data2_if,
|
||||||
|
"enabled": True,
|
||||||
|
"ipv4": {
|
||||||
|
"address": [{
|
||||||
|
"ip": DATA2_TARGET_V4,
|
||||||
|
"prefix-length": 24
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
"ipv6": {
|
||||||
|
"enabled": True,
|
||||||
|
"address": [{
|
||||||
|
"ip": DATA2_TARGET_V6,
|
||||||
|
"prefix-length": 64
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"infix-firewall": {
|
||||||
|
"firewall": {
|
||||||
|
"default": "untrusted",
|
||||||
|
"logging": "all",
|
||||||
|
"service": [{
|
||||||
|
"name": "myapp",
|
||||||
|
"port": [{
|
||||||
|
"lower": CUSTOM_PORT,
|
||||||
|
"proto": "tcp"
|
||||||
|
}]
|
||||||
|
}],
|
||||||
|
"zone": [{
|
||||||
|
"name": "mgmt",
|
||||||
|
"description": "Management network",
|
||||||
|
"action": "accept",
|
||||||
|
"interface": [mgmt_if],
|
||||||
|
"service": ["ssh", "netconf", "restconf"]
|
||||||
|
}, {
|
||||||
|
"name": "untrusted",
|
||||||
|
"description": "Untrusted zone",
|
||||||
|
"action": "accept",
|
||||||
|
"interface": [data1_if]
|
||||||
|
}, {
|
||||||
|
"name": "trusted",
|
||||||
|
"description": "Trusted zone",
|
||||||
|
"action": "accept",
|
||||||
|
"interface": [data2_if],
|
||||||
|
"service": ["ssh", "myapp"]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Wait for configuration to be activated
|
||||||
|
infamy.Firewall.wait_for_operational(target, {
|
||||||
|
"untrusted": {"action": "accept"},
|
||||||
|
"trusted": {"action": "accept"},
|
||||||
|
"mgmt": {"action": "accept"}
|
||||||
|
})
|
||||||
|
|
||||||
|
with test.step("Verify initial zone configuration and custom service"):
|
||||||
|
# Verify operational state matches expected configuration
|
||||||
|
data = target.get_data("/infix-firewall:firewall")
|
||||||
|
fw = data["firewall"]
|
||||||
|
|
||||||
|
assert fw["default"] == "untrusted"
|
||||||
|
|
||||||
|
zones = {zone["name"]: zone for zone in fw["zone"]}
|
||||||
|
services = {svc["name"]: svc for svc in fw.get("service", [])}
|
||||||
|
|
||||||
|
# Verify custom service exists
|
||||||
|
assert "myapp" in services, "Custom service myapp not found"
|
||||||
|
custom_service = services["myapp"]
|
||||||
|
assert len(custom_service["port"]) == 1
|
||||||
|
port_entry = next(iter(custom_service["port"]))
|
||||||
|
assert port_entry["proto"] == "tcp"
|
||||||
|
assert int(port_entry["lower"]) == CUSTOM_PORT
|
||||||
|
|
||||||
|
# Verify zone assignments
|
||||||
|
untrusted_zone = zones["untrusted"]
|
||||||
|
trusted_zone = zones["trusted"]
|
||||||
|
|
||||||
|
assert data1_if in untrusted_zone["interface"]
|
||||||
|
assert data2_if in trusted_zone["interface"]
|
||||||
|
|
||||||
|
# Check services safely - they may not exist in operational data if empty
|
||||||
|
trusted_services = trusted_zone.get("service", [])
|
||||||
|
untrusted_services = untrusted_zone.get("service", [])
|
||||||
|
|
||||||
|
assert "myapp" in trusted_services, f"Custom service should be in trusted zone, got: {trusted_services}"
|
||||||
|
assert "myapp" not in untrusted_services, f"Custom service should not be in untrusted zone, got: {untrusted_services}"
|
||||||
|
|
||||||
|
with infamy.IsolatedMacVlan(host_data1) as ns1:
|
||||||
|
ns1.addip(DATA1_HOST_V4, prefix_length=24, proto="ipv4")
|
||||||
|
ns1.addip(DATA1_HOST_V6, prefix_length=64, proto="ipv6")
|
||||||
|
|
||||||
|
with infamy.IsolatedMacVlan(host_data2) as ns2:
|
||||||
|
ns2.addip(DATA2_HOST_V4, prefix_length=24, proto="ipv4")
|
||||||
|
ns2.addip(DATA2_HOST_V6, prefix_length=64, proto="ipv6")
|
||||||
|
|
||||||
|
with test.step("Verify IPv4/IPv6 connectivity and custom service restrictions"):
|
||||||
|
# print(f"Testing IPv4 connectivity: {DATA1_HOST_V4} -> {DATA1_TARGET_V4}")
|
||||||
|
# print(f"Testing IPv4 connectivity: {DATA2_HOST_V4} -> {DATA2_TARGET_V4}")
|
||||||
|
ns1.must_reach(DATA1_TARGET_V4, timeout=5)
|
||||||
|
ns2.must_reach(DATA2_TARGET_V4, timeout=5)
|
||||||
|
|
||||||
|
# print(f"Testing IPv6 connectivity: {DATA1_HOST_V6} -> {DATA1_TARGET_V6}")
|
||||||
|
# print(f"Testing IPv6 connectivity: {DATA2_HOST_V6} -> {DATA2_TARGET_V6}")
|
||||||
|
ns1.must_reach(DATA1_TARGET_V6, timeout=5)
|
||||||
|
ns2.must_reach(DATA2_TARGET_V6, timeout=5)
|
||||||
|
|
||||||
|
firewall_ns1 = infamy.Firewall(ns1, None)
|
||||||
|
firewall_ns2 = infamy.Firewall(ns2, None)
|
||||||
|
|
||||||
|
ok, ports = firewall_ns1.verify_allowed(DATA1_TARGET_V4,
|
||||||
|
[(CUSTOM_PORT, "tcp", "myapp")])
|
||||||
|
ok, ports = firewall_ns2.verify_allowed(DATA2_TARGET_V4,
|
||||||
|
[(CUSTOM_PORT, "tcp", "myapp")])
|
||||||
|
|
||||||
|
with test.step("Verify IPv6 custom service functionality"):
|
||||||
|
ok, ports = firewall_ns1.verify_allowed(DATA1_TARGET_V6,
|
||||||
|
[(CUSTOM_PORT, "tcp", "myapp")])
|
||||||
|
ok, ports = firewall_ns2.verify_allowed(DATA2_TARGET_V6,
|
||||||
|
[(CUSTOM_PORT, "tcp", "myapp")])
|
||||||
|
|
||||||
|
with test.step("Perform dynamic zone migration"):
|
||||||
|
target.delete_xpath(f"/infix-firewall:firewall/zone[name='untrusted']/interface[.='{data1_if}']")
|
||||||
|
target.put_config_dict("infix-firewall", {
|
||||||
|
"firewall": {
|
||||||
|
"zone": [{
|
||||||
|
"name": "trusted",
|
||||||
|
"interface": [data1_if]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
infamy.Firewall.wait_for_operational(target, {
|
||||||
|
"untrusted": {"action": "accept"},
|
||||||
|
"trusted": {"action": "accept"}
|
||||||
|
})
|
||||||
|
|
||||||
|
with test.step("Verify connectivity after zone migration"):
|
||||||
|
ns1.must_reach(DATA1_TARGET_V4, timeout=3)
|
||||||
|
ns1.must_reach(DATA1_TARGET_V6, timeout=3)
|
||||||
|
ns2.must_reach(DATA2_TARGET_V4, timeout=3)
|
||||||
|
ns2.must_reach(DATA2_TARGET_V6, timeout=3)
|
||||||
|
|
||||||
|
with test.step("Verify custom service from migrated interface"):
|
||||||
|
firewall_migrated = infamy.Firewall(ns1, None)
|
||||||
|
ok, ports = firewall_migrated.verify_allowed(DATA1_TARGET_V4,
|
||||||
|
[(CUSTOM_PORT, "tcp", "myapp")])
|
||||||
|
assert ok, f"Custom service should work on IPv4 after zone migration"
|
||||||
|
|
||||||
|
ok, ports = firewall_migrated.verify_allowed(DATA1_TARGET_V6,
|
||||||
|
[(CUSTOM_PORT, "tcp", "myapp")])
|
||||||
|
assert ok, f"Custom service should work on IPv6 after zone migration"
|
||||||
|
|
||||||
|
with test.step("Verify operational state reflects zone changes"):
|
||||||
|
data = target.get_data("/infix-firewall:firewall")
|
||||||
|
fw = data["firewall"]
|
||||||
|
zones = {zone["name"]: zone for zone in fw["zone"]}
|
||||||
|
|
||||||
|
trusted_zone = zones["trusted"]
|
||||||
|
untrusted_zone = zones["untrusted"]
|
||||||
|
|
||||||
|
assert data1_if in trusted_zone["interface"], "data1_if should be in trusted zone"
|
||||||
|
assert data2_if in trusted_zone["interface"], "data2_if should be in trusted zone"
|
||||||
|
assert data1_if not in untrusted_zone.get("interface", []), "data1_if should no longer be in untrusted zone"
|
||||||
|
|
||||||
|
trusted_services = trusted_zone.get("service", [])
|
||||||
|
assert "myapp" in trusted_services, f"Custom service should be available in trusted zone, got: {trusted_services}"
|
||||||
|
|
||||||
|
test.succeed()
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
graph "1x3" {
|
||||||
|
layout = "neato";
|
||||||
|
overlap = false;
|
||||||
|
esep = "+80";
|
||||||
|
|
||||||
|
node [shape=record, fontname="DejaVu Sans Mono, Book"];
|
||||||
|
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
|
||||||
|
|
||||||
|
host [
|
||||||
|
label="host | { <mgmt> mgmt | <data1> data1 | <data2> data2 }",
|
||||||
|
pos="1,1!",
|
||||||
|
requires="controller"
|
||||||
|
];
|
||||||
|
|
||||||
|
target [
|
||||||
|
label="{ <mgmt> mgmt | <data1> data1 | <data2> data2 } | target",
|
||||||
|
pos="3,1!",
|
||||||
|
requires="infix",
|
||||||
|
];
|
||||||
|
|
||||||
|
host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"]
|
||||||
|
host:data1 -- target:data1 [color=red, fontcolor=red, taillabel="10.1.1.0/24, fd01:1:1::/64"]
|
||||||
|
host:data2 -- target:data2 [color=green, fontcolor=green, taillabel="10.2.2.0/24, fd02:2:2::/64"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
|
||||||
|
<!-- Title: 1x3 Pages: 1 -->
|
||||||
|
<svg width="440pt" height="78pt"
|
||||||
|
viewBox="0.00 0.00 440.03 78.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 74)">
|
||||||
|
<title>1x3</title>
|
||||||
|
<polygon fill="white" stroke="transparent" points="-4,4 -4,-74 436.03,-74 436.03,4 -4,4"/>
|
||||||
|
<!-- host -->
|
||||||
|
<g id="node1" class="node">
|
||||||
|
<title>host</title>
|
||||||
|
<polygon fill="none" stroke="black" points="0,-0.5 0,-69.5 108,-69.5 108,-0.5 0,-0.5"/>
|
||||||
|
<text text-anchor="middle" x="25" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
|
||||||
|
<polyline fill="none" stroke="black" points="50,-0.5 50,-69.5 "/>
|
||||||
|
<text text-anchor="middle" x="79" y="-54.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||||
|
<polyline fill="none" stroke="black" points="50,-46.5 108,-46.5 "/>
|
||||||
|
<text text-anchor="middle" x="79" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data1</text>
|
||||||
|
<polyline fill="none" stroke="black" points="50,-23.5 108,-23.5 "/>
|
||||||
|
<text text-anchor="middle" x="79" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data2</text>
|
||||||
|
</g>
|
||||||
|
<!-- target -->
|
||||||
|
<g id="node2" class="node">
|
||||||
|
<title>target</title>
|
||||||
|
<polygon fill="none" stroke="black" points="308.03,-0.5 308.03,-69.5 432.03,-69.5 432.03,-0.5 308.03,-0.5"/>
|
||||||
|
<text text-anchor="middle" x="337.03" y="-54.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||||
|
<polyline fill="none" stroke="black" points="308.03,-46.5 366.03,-46.5 "/>
|
||||||
|
<text text-anchor="middle" x="337.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data1</text>
|
||||||
|
<polyline fill="none" stroke="black" points="308.03,-23.5 366.03,-23.5 "/>
|
||||||
|
<text text-anchor="middle" x="337.03" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">data2</text>
|
||||||
|
<polyline fill="none" stroke="black" points="366.03,-0.5 366.03,-69.5 "/>
|
||||||
|
<text text-anchor="middle" x="399.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
|
||||||
|
</g>
|
||||||
|
<!-- host--target -->
|
||||||
|
<g id="edge1" class="edge">
|
||||||
|
<title>host:mgmt--target:mgmt</title>
|
||||||
|
<path fill="none" stroke="lightgray" stroke-width="2" d="M108,-58C108,-58 308.03,-58 308.03,-58"/>
|
||||||
|
</g>
|
||||||
|
<!-- host--target -->
|
||||||
|
<g id="edge2" class="edge">
|
||||||
|
<title>host:data1--target:data1</title>
|
||||||
|
<path fill="none" stroke="red" stroke-width="2" d="M108,-35C108,-35 308.03,-35 308.03,-35"/>
|
||||||
|
<text text-anchor="middle" x="198.5" y="-38.8" font-family="DejaVu Serif, Book" font-size="14.00" fill="red">10.1.1.0/24, fd01:1:1::/64</text>
|
||||||
|
</g>
|
||||||
|
<!-- host--target -->
|
||||||
|
<g id="edge3" class="edge">
|
||||||
|
<title>host:data2--target:data2</title>
|
||||||
|
<path fill="none" stroke="green" stroke-width="2" d="M108,-12C108,-12 308.03,-12 308.03,-12"/>
|
||||||
|
<text text-anchor="middle" x="198.5" y="-15.8" font-family="DejaVu Serif, Book" font-size="14.00" fill="green">10.2.2.0/24, fd02:2:2::/64</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1 @@
|
|||||||
|
test.adoc
|
||||||
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,33 @@
|
|||||||
|
=== LAN-WAN Firewall with Masquerading
|
||||||
|
|
||||||
|
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_firewall/lan-wan]
|
||||||
|
|
||||||
|
==== Description
|
||||||
|
|
||||||
|
Typical home/office router scenario where the DUT acts as a gateway with
|
||||||
|
LAN-to-WAN traffic forwarding and masquerading (SNAT).
|
||||||
|
|
||||||
|
image::lan-wan.svg[align=center, scaledwidth=50%]
|
||||||
|
|
||||||
|
- DUT/Gateway with firewall and NAT
|
||||||
|
- Test host has two interfaces: a LAN-side and a WAN-side (Internet)
|
||||||
|
- Test host's LAN interface acts as a client behind the router
|
||||||
|
- Test host's WAN interface acts as an Internet server/destination
|
||||||
|
|
||||||
|
==== Topology
|
||||||
|
|
||||||
|
image::topology.svg[LAN-WAN Firewall with Masquerading topology, align=center, scaledwidth=75%]
|
||||||
|
|
||||||
|
==== Sequence
|
||||||
|
|
||||||
|
. Set up topology and attach to gateway
|
||||||
|
. Configure gateway with firewall and SNAT
|
||||||
|
. Verify LAN access to router
|
||||||
|
. Verify LAN services accessibility
|
||||||
|
. Verify WAN access to router is blocked
|
||||||
|
. Verify WAN blocks all well-known ports
|
||||||
|
. Verify LAN-to-WAN connectivity (outbound)
|
||||||
|
. Verify LAN-to-WAN masquerading
|
||||||
|
. Verify WAN-to-LAN blocking (inbound)
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""LAN-WAN Firewall with Masquerading
|
||||||
|
|
||||||
|
Typical home/office router scenario where the DUT acts as a gateway with
|
||||||
|
LAN-to-WAN traffic forwarding and masquerading (SNAT).
|
||||||
|
|
||||||
|
image::lan-wan.svg[align=center, scaledwidth=50%]
|
||||||
|
|
||||||
|
- DUT/Gateway with firewall and NAT
|
||||||
|
- Test host has two interfaces: a LAN-side and a WAN-side (Internet)
|
||||||
|
- Test host's LAN interface acts as a client behind the router
|
||||||
|
- Test host's WAN interface acts as an Internet server/destination
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import infamy
|
||||||
|
from infamy.util import until
|
||||||
|
|
||||||
|
|
||||||
|
with infamy.Test() as test:
|
||||||
|
with test.step("Set up topology and attach to gateway"):
|
||||||
|
env = infamy.Env()
|
||||||
|
gateway = env.attach("gateway", "mgmt")
|
||||||
|
_, lan_if = env.ltop.xlate("gateway", "lan")
|
||||||
|
_, wan_if = env.ltop.xlate("gateway", "wan")
|
||||||
|
_, mgmt_if = env.ltop.xlate("gateway", "mgmt")
|
||||||
|
_, host_lan = env.ltop.xlate("host", "lan") # Host LAN-side interface
|
||||||
|
_, host_wan = env.ltop.xlate("host", "wan") # Host WAN-side interface
|
||||||
|
|
||||||
|
LAN_NET = "192.168.1.0/24"
|
||||||
|
LAN_ROUTER_IP = "192.168.1.1" # Router's LAN interface
|
||||||
|
LAN_CLIENT_IP = "192.168.1.100" # Client on LAN side
|
||||||
|
|
||||||
|
WAN_NET = "203.0.113.0/24" # RFC 5737 test network
|
||||||
|
WAN_ROUTER_IP = "203.0.113.1" # Router's WAN interface
|
||||||
|
WAN_SERVER_IP = "203.0.113.100" # Server on WAN side
|
||||||
|
|
||||||
|
with test.step("Configure gateway with firewall and SNAT"):
|
||||||
|
gateway.put_config_dict("ietf-interfaces", {
|
||||||
|
"interfaces": {
|
||||||
|
"interface": [
|
||||||
|
{
|
||||||
|
"name": lan_if,
|
||||||
|
"enabled": True,
|
||||||
|
"ipv4": {
|
||||||
|
"forwarding": True,
|
||||||
|
"address": [{
|
||||||
|
"ip": LAN_ROUTER_IP,
|
||||||
|
"prefix-length": 24
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": wan_if,
|
||||||
|
"enabled": True,
|
||||||
|
"ipv4": {
|
||||||
|
"forwarding": True,
|
||||||
|
"address": [{
|
||||||
|
"ip": WAN_ROUTER_IP,
|
||||||
|
"prefix-length": 24
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
gateway.put_config_dict("infix-firewall", {
|
||||||
|
"firewall": {
|
||||||
|
"default": "wan",
|
||||||
|
"logging": "all",
|
||||||
|
"zone": [
|
||||||
|
{
|
||||||
|
"name": "lan",
|
||||||
|
"description": "Internal LAN network - trusted",
|
||||||
|
"action": "accept",
|
||||||
|
"interface": [lan_if, mgmt_if],
|
||||||
|
"service": ["ssh", "dhcp", "dns"]
|
||||||
|
}, {
|
||||||
|
"name": "wan",
|
||||||
|
"description": "External WAN interface - untrusted",
|
||||||
|
"action": "drop",
|
||||||
|
"interface": [wan_if]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"policy": [
|
||||||
|
{
|
||||||
|
"name": "lan-to-wan",
|
||||||
|
"description": "Allow LAN to WAN traffic with SNAT",
|
||||||
|
"ingress": ["lan"],
|
||||||
|
"egress": ["wan"],
|
||||||
|
"action": "accept",
|
||||||
|
"masquerade": True
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Wait for configuration to be activated
|
||||||
|
infamy.Firewall.wait_for_operational(gateway, {
|
||||||
|
"lan": {"action": "accept"},
|
||||||
|
"wan": {"action": "drop"}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Verify firewall operational state
|
||||||
|
data = gateway.get_data("/infix-firewall:firewall")
|
||||||
|
fw = data["firewall"]
|
||||||
|
zones = {z["name"]: z for z in fw["zone"]}
|
||||||
|
|
||||||
|
# Verify LAN zone
|
||||||
|
lan_zone = zones["lan"]
|
||||||
|
assert lan_zone["action"] == "accept"
|
||||||
|
assert lan_if in lan_zone["interface"]
|
||||||
|
|
||||||
|
# Verify WAN zone
|
||||||
|
wan_zone = zones["wan"]
|
||||||
|
assert wan_zone["action"] == "drop"
|
||||||
|
assert wan_if in wan_zone["interface"]
|
||||||
|
|
||||||
|
# Verify policy
|
||||||
|
policies = {p["name"]: p for p in fw["policy"]}
|
||||||
|
lan_wan_policy = policies["lan-to-wan"]
|
||||||
|
assert lan_wan_policy["ingress"] == ["lan"]
|
||||||
|
assert lan_wan_policy["egress"] == ["wan"]
|
||||||
|
assert lan_wan_policy["action"] == "accept"
|
||||||
|
assert lan_wan_policy["masquerade"] is True
|
||||||
|
|
||||||
|
with infamy.IsolatedMacVlan(host_lan) as lan_client:
|
||||||
|
lan_client.addip(LAN_CLIENT_IP)
|
||||||
|
lan_client.addroute("0.0.0.0", LAN_ROUTER_IP, prefix_length="0")
|
||||||
|
|
||||||
|
with infamy.IsolatedMacVlan(host_wan) as wan_server:
|
||||||
|
wan_server.addip(WAN_SERVER_IP)
|
||||||
|
|
||||||
|
with test.step("Verify LAN access to router"):
|
||||||
|
lan_client.must_reach(LAN_ROUTER_IP, timeout=3)
|
||||||
|
|
||||||
|
with test.step("Verify LAN services accessibility"):
|
||||||
|
firewall = infamy.Firewall(lan_client, None)
|
||||||
|
svc = [
|
||||||
|
(22, "tcp", "ssh"),
|
||||||
|
(53, "udp", "dns"),
|
||||||
|
(67, "udp", "dhcp"),
|
||||||
|
]
|
||||||
|
|
||||||
|
ok, ports = firewall.verify_allowed(LAN_ROUTER_IP, svc)
|
||||||
|
if not ok:
|
||||||
|
print(f" ⚠ Some LAN services are filtered: {', '.join(ports)}")
|
||||||
|
test.fail()
|
||||||
|
|
||||||
|
with test.step("Verify WAN access to router is blocked"):
|
||||||
|
wan_server.must_not_reach(WAN_ROUTER_IP, timeout=3)
|
||||||
|
|
||||||
|
with test.step("Verify WAN blocks all well-known ports"):
|
||||||
|
firewall = infamy.Firewall(wan_server, None)
|
||||||
|
|
||||||
|
ok, ports, _ = firewall.verify_blocked(WAN_ROUTER_IP)
|
||||||
|
if not ok:
|
||||||
|
print(f" ⚠ Some ports are unexpectedly open from WAN: {', '.join(ports)}")
|
||||||
|
test.fail()
|
||||||
|
|
||||||
|
with test.step("Verify LAN-to-WAN connectivity (outbound)"):
|
||||||
|
lan_client.must_reach(WAN_SERVER_IP, timeout=3)
|
||||||
|
|
||||||
|
with test.step("Verify LAN-to-WAN masquerading"):
|
||||||
|
firewall = infamy.Firewall(lan_client, wan_server)
|
||||||
|
|
||||||
|
ok, info = firewall.verify_snat(WAN_SERVER_IP, WAN_ROUTER_IP)
|
||||||
|
if not ok:
|
||||||
|
print(f" ⚠ {info}")
|
||||||
|
test.fail()
|
||||||
|
|
||||||
|
with test.step("Verify WAN-to-LAN blocking (inbound)"):
|
||||||
|
wan_server.must_not_reach(LAN_CLIENT_IP, timeout=3)
|
||||||
|
|
||||||
|
test.succeed()
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
graph "1x3" {
|
||||||
|
layout = "neato";
|
||||||
|
overlap = false;
|
||||||
|
esep = "+80";
|
||||||
|
|
||||||
|
node [shape=record, fontname="DejaVu Sans Mono, Book"];
|
||||||
|
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
|
||||||
|
|
||||||
|
host [
|
||||||
|
label="host | { <mgmt> mgmt | <lan> lan | <wan> wan }",
|
||||||
|
pos="1,1!",
|
||||||
|
requires="controller"
|
||||||
|
];
|
||||||
|
|
||||||
|
gateway [
|
||||||
|
label="{ <mgmt> mgmt | <lan> lan | <wan> wan } | gateway",
|
||||||
|
pos="3,1!",
|
||||||
|
requires="infix",
|
||||||
|
];
|
||||||
|
|
||||||
|
host:mgmt -- gateway:mgmt [requires="mgmt", color="lightgray"]
|
||||||
|
host:lan -- gateway:lan [color=black, fontcolor=black, taillabel="192.168.1.0/24"]
|
||||||
|
host:wan -- gateway:wan [color=red, fontcolor=red, taillabel="203.0.113.0/24"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
|
||||||
|
<!-- Title: 1x3 Pages: 1 -->
|
||||||
|
<svg width="432pt" height="78pt"
|
||||||
|
viewBox="0.00 0.00 432.03 78.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 74)">
|
||||||
|
<title>1x3</title>
|
||||||
|
<polygon fill="white" stroke="transparent" points="-4,4 -4,-74 428.03,-74 428.03,4 -4,4"/>
|
||||||
|
<!-- host -->
|
||||||
|
<g id="node1" class="node">
|
||||||
|
<title>host</title>
|
||||||
|
<polygon fill="none" stroke="black" points="0,-0.5 0,-69.5 100,-69.5 100,-0.5 0,-0.5"/>
|
||||||
|
<text text-anchor="middle" x="25" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
|
||||||
|
<polyline fill="none" stroke="black" points="50,-0.5 50,-69.5 "/>
|
||||||
|
<text text-anchor="middle" x="75" y="-54.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||||
|
<polyline fill="none" stroke="black" points="50,-46.5 100,-46.5 "/>
|
||||||
|
<text text-anchor="middle" x="75" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">lan</text>
|
||||||
|
<polyline fill="none" stroke="black" points="50,-23.5 100,-23.5 "/>
|
||||||
|
<text text-anchor="middle" x="75" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">wan</text>
|
||||||
|
</g>
|
||||||
|
<!-- gateway -->
|
||||||
|
<g id="node2" class="node">
|
||||||
|
<title>gateway</title>
|
||||||
|
<polygon fill="none" stroke="black" points="300.03,-0.5 300.03,-69.5 424.03,-69.5 424.03,-0.5 300.03,-0.5"/>
|
||||||
|
<text text-anchor="middle" x="325.03" y="-54.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||||
|
<polyline fill="none" stroke="black" points="300.03,-46.5 350.03,-46.5 "/>
|
||||||
|
<text text-anchor="middle" x="325.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">lan</text>
|
||||||
|
<polyline fill="none" stroke="black" points="300.03,-23.5 350.03,-23.5 "/>
|
||||||
|
<text text-anchor="middle" x="325.03" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">wan</text>
|
||||||
|
<polyline fill="none" stroke="black" points="350.03,-0.5 350.03,-69.5 "/>
|
||||||
|
<text text-anchor="middle" x="387.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">gateway</text>
|
||||||
|
</g>
|
||||||
|
<!-- host--gateway -->
|
||||||
|
<g id="edge1" class="edge">
|
||||||
|
<title>host:mgmt--gateway:mgmt</title>
|
||||||
|
<path fill="none" stroke="lightgray" stroke-width="2" d="M100,-58C100,-58 300.03,-58 300.03,-58"/>
|
||||||
|
</g>
|
||||||
|
<!-- host--gateway -->
|
||||||
|
<g id="edge2" class="edge">
|
||||||
|
<title>host:lan--gateway:lan</title>
|
||||||
|
<path fill="none" stroke="black" stroke-width="2" d="M100,-35C100,-35 300.03,-35 300.03,-35"/>
|
||||||
|
<text text-anchor="middle" x="154.5" y="-38.8" font-family="DejaVu Serif, Book" font-size="14.00">192.168.1.0/24</text>
|
||||||
|
</g>
|
||||||
|
<!-- host--gateway -->
|
||||||
|
<g id="edge3" class="edge">
|
||||||
|
<title>host:wan--gateway:wan</title>
|
||||||
|
<path fill="none" stroke="red" stroke-width="2" d="M100,-12C100,-12 300.03,-12 300.03,-12"/>
|
||||||
|
<text text-anchor="middle" x="154.5" y="-15.8" font-family="DejaVu Serif, Book" font-size="14.00" fill="red">203.0.113.0/24</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1 @@
|
|||||||
|
test.adoc
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
=== WAN-DMZ-LAN Firewall with Port Forwarding
|
||||||
|
|
||||||
|
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/infix_firewall/wan-dmz-lan]
|
||||||
|
|
||||||
|
==== Description
|
||||||
|
|
||||||
|
Multi-zone firewall setup with port forwarding (DNAT) to a DMZ server,
|
||||||
|
and masquerading (SNAT) of WAN-bound traffic.
|
||||||
|
|
||||||
|
image::wan-dmz-lan.svg[align=center, scaledwidth=50%]
|
||||||
|
|
||||||
|
- DUT/Gateway with WAN/DMZ/LAN zones and NAT
|
||||||
|
- Test host's WAN interface acts as external Internet client
|
||||||
|
- Test host's DMZ interface acts as internal server (HTTP on port 80)
|
||||||
|
- Test host's LAN interface acts as internal LAN client
|
||||||
|
|
||||||
|
==== Topology
|
||||||
|
|
||||||
|
image::topology.svg[WAN-DMZ-LAN Firewall with Port Forwarding topology, align=center, scaledwidth=75%]
|
||||||
|
|
||||||
|
==== Sequence
|
||||||
|
|
||||||
|
. Set up topology and attach to gateway
|
||||||
|
. Configure gateway with multi-zone firewall and NAT
|
||||||
|
. Verify basic connectivity within zones
|
||||||
|
. Verify WAN to DMZ port forwarding (DNAT)
|
||||||
|
. Verify LAN to DMZ connectivity
|
||||||
|
. Verify DMZ to LAN blocking
|
||||||
|
. Verify WAN isolation
|
||||||
|
. Verify LAN to WAN connectivity with SNAT
|
||||||
|
. Verify DMZ to WAN connectivity with SNAT
|
||||||
|
. Verify zone default actions/services
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""WAN-DMZ-LAN Firewall with Port Forwarding
|
||||||
|
|
||||||
|
Multi-zone firewall setup with port forwarding (DNAT) to a DMZ server,
|
||||||
|
and masquerading (SNAT) of WAN-bound traffic.
|
||||||
|
|
||||||
|
image::wan-dmz-lan.svg[align=center, scaledwidth=50%]
|
||||||
|
|
||||||
|
- DUT/Gateway with WAN/DMZ/LAN zones and NAT
|
||||||
|
- Test host's WAN interface acts as external Internet client
|
||||||
|
- Test host's DMZ interface acts as internal server (HTTP on port 80)
|
||||||
|
- Test host's LAN interface acts as internal LAN client
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import infamy
|
||||||
|
from infamy.util import until
|
||||||
|
|
||||||
|
|
||||||
|
with infamy.Test() as test:
|
||||||
|
with test.step("Set up topology and attach to gateway"):
|
||||||
|
env = infamy.Env()
|
||||||
|
gateway = env.attach("gateway", "mgmt")
|
||||||
|
_, wan_if = env.ltop.xlate("gateway", "wan")
|
||||||
|
_, dmz_if = env.ltop.xlate("gateway", "dmz")
|
||||||
|
_, lan_if = env.ltop.xlate("gateway", "lan")
|
||||||
|
_, mgmt_if = env.ltop.xlate("gateway", "mgmt")
|
||||||
|
_, host_wan = env.ltop.xlate("host", "wan")
|
||||||
|
_, host_dmz = env.ltop.xlate("host", "dmz")
|
||||||
|
_, host_lan = env.ltop.xlate("host", "lan")
|
||||||
|
|
||||||
|
WAN_NET = "203.0.113.0/24" # RFC 5737 test network
|
||||||
|
WAN_ROUTER_IP = "203.0.113.1" # Gateway WAN interface
|
||||||
|
WAN_CLIENT_IP = "203.0.113.100" # Host WAN interface
|
||||||
|
|
||||||
|
DMZ_NET = "10.0.1.0/24"
|
||||||
|
DMZ_ROUTER_IP = "10.0.1.1" # Gateway DMZ interface
|
||||||
|
DMZ_SERVER_IP = "10.0.1.100" # Host DMZ interface
|
||||||
|
|
||||||
|
LAN_NET = "192.168.1.0/24"
|
||||||
|
LAN_ROUTER_IP = "192.168.1.1" # Gateway LAN interface
|
||||||
|
LAN_CLIENT_IP = "192.168.1.100" # Host LAN interface
|
||||||
|
|
||||||
|
with test.step("Configure gateway with multi-zone firewall and NAT"):
|
||||||
|
gateway.put_config_dict("ietf-interfaces", {
|
||||||
|
"interfaces": {
|
||||||
|
"interface": [
|
||||||
|
{
|
||||||
|
"name": wan_if,
|
||||||
|
"enabled": True,
|
||||||
|
"ipv4": {
|
||||||
|
"forwarding": True,
|
||||||
|
"address": [{
|
||||||
|
"ip": WAN_ROUTER_IP,
|
||||||
|
"prefix-length": 24
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": dmz_if,
|
||||||
|
"enabled": True,
|
||||||
|
"ipv4": {
|
||||||
|
"forwarding": True,
|
||||||
|
"address": [{
|
||||||
|
"ip": DMZ_ROUTER_IP,
|
||||||
|
"prefix-length": 24
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": lan_if,
|
||||||
|
"enabled": True,
|
||||||
|
"ipv4": {
|
||||||
|
"forwarding": True,
|
||||||
|
"address": [{
|
||||||
|
"ip": LAN_ROUTER_IP,
|
||||||
|
"prefix-length": 24
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
gateway.put_config_dict("infix-firewall", {
|
||||||
|
"firewall": {
|
||||||
|
"default": "wan",
|
||||||
|
"logging": "all",
|
||||||
|
"zone": [
|
||||||
|
{
|
||||||
|
"name": "wan",
|
||||||
|
"description": "External WAN interface - untrusted",
|
||||||
|
"action": "drop",
|
||||||
|
"interface": [wan_if],
|
||||||
|
"port-forward": [{
|
||||||
|
"lower": 8080,
|
||||||
|
"proto": "tcp",
|
||||||
|
"to": {
|
||||||
|
"addr": DMZ_SERVER_IP,
|
||||||
|
"port": 80
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "dmz",
|
||||||
|
"description": "DMZ network - limited trust",
|
||||||
|
"action": "reject",
|
||||||
|
"network": [DMZ_NET],
|
||||||
|
"service": ["http"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lan",
|
||||||
|
"description": "Internal LAN network - trusted",
|
||||||
|
"action": "accept",
|
||||||
|
"interface": [lan_if, mgmt_if]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"policy": [
|
||||||
|
{
|
||||||
|
"name": "loc-to-wan",
|
||||||
|
"description": "Allow local networks to WAN with SNAT",
|
||||||
|
"ingress": ["lan", "dmz"],
|
||||||
|
"egress": ["wan"],
|
||||||
|
"action": "accept",
|
||||||
|
"masquerade": True
|
||||||
|
}, {
|
||||||
|
"name": "lan-to-dmz",
|
||||||
|
"description": "Allow LAN access to DMZ services",
|
||||||
|
"ingress": ["lan"],
|
||||||
|
"egress": ["dmz"],
|
||||||
|
"action": "accept",
|
||||||
|
"service": ["ssh", "http"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Wait for configuration to be activated
|
||||||
|
infamy.Firewall.wait_for_operational(gateway, {
|
||||||
|
"wan": {"action": "drop"},
|
||||||
|
"dmz": {"action": "reject"},
|
||||||
|
"lan": {"action": "accept"}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Verify firewall operational state
|
||||||
|
data = gateway.get_data("/infix-firewall:firewall")
|
||||||
|
fw = data["firewall"]
|
||||||
|
zones = {z["name"]: z for z in fw["zone"]}
|
||||||
|
|
||||||
|
# Verify WAN zone with port forwarding
|
||||||
|
wan_zone = zones["wan"]
|
||||||
|
assert wan_zone["action"] == "drop"
|
||||||
|
assert wan_if in wan_zone["interface"]
|
||||||
|
assert len(wan_zone["port-forward"]) == 1
|
||||||
|
pf = next(iter(wan_zone["port-forward"]))
|
||||||
|
assert pf["lower"] == 8080
|
||||||
|
assert pf["to"]["addr"] == DMZ_SERVER_IP
|
||||||
|
assert pf["to"]["port"] == 80
|
||||||
|
|
||||||
|
# Verify DMZ zone
|
||||||
|
dmz_zone = zones["dmz"]
|
||||||
|
assert dmz_zone["action"] == "reject"
|
||||||
|
assert DMZ_NET in dmz_zone["network"]
|
||||||
|
assert "http" in dmz_zone["service"]
|
||||||
|
|
||||||
|
# Verify LAN zone
|
||||||
|
lan_zone = zones["lan"]
|
||||||
|
assert lan_zone["action"] == "accept"
|
||||||
|
assert lan_if in lan_zone["interface"]
|
||||||
|
|
||||||
|
# Check policies
|
||||||
|
policies = {p["name"]: p for p in fw["policy"]}
|
||||||
|
|
||||||
|
# Verify loc-to-wan policy
|
||||||
|
loc_wan_policy = policies["loc-to-wan"]
|
||||||
|
assert set(loc_wan_policy["ingress"]) == {"lan", "dmz"}
|
||||||
|
assert loc_wan_policy["egress"] == ["wan"]
|
||||||
|
assert loc_wan_policy["masquerade"] is True
|
||||||
|
|
||||||
|
# Verify lan-to-dmz policy
|
||||||
|
lan_dmz_policy = policies["lan-to-dmz"]
|
||||||
|
assert lan_dmz_policy["ingress"] == ["lan"]
|
||||||
|
assert lan_dmz_policy["egress"] == ["dmz"]
|
||||||
|
assert "ssh" in lan_dmz_policy["service"]
|
||||||
|
assert "http" in lan_dmz_policy["service"]
|
||||||
|
|
||||||
|
with infamy.IsolatedMacVlan(host_wan) as wan_client:
|
||||||
|
wan_client.addip(WAN_CLIENT_IP)
|
||||||
|
|
||||||
|
with infamy.IsolatedMacVlan(host_dmz) as dmz_server:
|
||||||
|
dmz_server.addip(DMZ_SERVER_IP)
|
||||||
|
dmz_server.addroute("0.0.0.0", DMZ_ROUTER_IP, prefix_length="0")
|
||||||
|
|
||||||
|
with infamy.IsolatedMacVlan(host_lan) as lan_client:
|
||||||
|
lan_client.addip(LAN_CLIENT_IP)
|
||||||
|
lan_client.addroute("0.0.0.0", LAN_ROUTER_IP, prefix_length="0")
|
||||||
|
|
||||||
|
with test.step("Verify basic connectivity within zones"):
|
||||||
|
lan_client.must_reach(LAN_ROUTER_IP, timeout=3)
|
||||||
|
dmz_server.must_not_reach(DMZ_ROUTER_IP, timeout=3)
|
||||||
|
|
||||||
|
with test.step("Verify WAN to DMZ port forwarding (DNAT)"):
|
||||||
|
firewall = infamy.Firewall(wan_client, dmz_server)
|
||||||
|
|
||||||
|
# Test port forwarding: WAN:8080 → DMZ:80
|
||||||
|
ok, info = firewall.verify_dnat(
|
||||||
|
WAN_ROUTER_IP, forward_port=8080, target_port=80)
|
||||||
|
|
||||||
|
if not ok:
|
||||||
|
print(f" ⚠ {info}")
|
||||||
|
test.fail()
|
||||||
|
|
||||||
|
with test.step("Verify LAN to DMZ connectivity"):
|
||||||
|
lan_client.must_reach(DMZ_SERVER_IP, timeout=3)
|
||||||
|
firewall = infamy.Firewall(lan_client, None)
|
||||||
|
svc = [
|
||||||
|
(22, "tcp", "ssh"),
|
||||||
|
(80, "tcp", "http"),
|
||||||
|
]
|
||||||
|
|
||||||
|
ok, ports = firewall.verify_allowed(DMZ_SERVER_IP, svc)
|
||||||
|
if not ok:
|
||||||
|
print(f" ⚠ Some DMZ services filtered from LAN: {', '.join(ports)}")
|
||||||
|
test.fail()
|
||||||
|
|
||||||
|
with test.step("Verify DMZ to LAN blocking"):
|
||||||
|
dmz_server.must_not_reach(LAN_CLIENT_IP, timeout=3)
|
||||||
|
|
||||||
|
with test.step("Verify WAN isolation"):
|
||||||
|
firewall = infamy.Firewall(wan_client, None)
|
||||||
|
|
||||||
|
ok, ports, _ = firewall.verify_blocked(LAN_ROUTER_IP)
|
||||||
|
if not ok:
|
||||||
|
print(f" ⚠ WAN can access LAN ports: {', '.join(ports)}")
|
||||||
|
test.fail()
|
||||||
|
|
||||||
|
ok, ports, _ = firewall.verify_blocked(DMZ_ROUTER_IP)
|
||||||
|
if not ok:
|
||||||
|
print(f" ⚠ WAN can access DMZ ports: {', '.join(ports)}")
|
||||||
|
|
||||||
|
with test.step("Verify LAN to WAN connectivity with SNAT"):
|
||||||
|
firewall = infamy.Firewall(lan_client, wan_client)
|
||||||
|
|
||||||
|
lan_client.must_reach(WAN_CLIENT_IP, timeout=3)
|
||||||
|
|
||||||
|
ok, info = firewall.verify_snat(WAN_CLIENT_IP, WAN_ROUTER_IP)
|
||||||
|
if not ok:
|
||||||
|
print(f" ⚠ LAN to WAN SNAT: {info}")
|
||||||
|
test.fail()
|
||||||
|
|
||||||
|
with test.step("Verify DMZ to WAN connectivity with SNAT"):
|
||||||
|
firewall = infamy.Firewall(dmz_server, wan_client)
|
||||||
|
|
||||||
|
dmz_server.must_reach(WAN_CLIENT_IP, timeout=3)
|
||||||
|
|
||||||
|
ok, info = firewall.verify_snat(WAN_CLIENT_IP, WAN_ROUTER_IP)
|
||||||
|
if not ok:
|
||||||
|
print(f" ⚠ DMZ to WAN SNAT: {info}")
|
||||||
|
test.fail()
|
||||||
|
|
||||||
|
with test.step("Verify zone default actions/services"):
|
||||||
|
firewall_lan = infamy.Firewall(lan_client, None)
|
||||||
|
firewall_dmz = infamy.Firewall(dmz_server, None)
|
||||||
|
firewall_wan = infamy.Firewall(wan_client, None)
|
||||||
|
|
||||||
|
svc = [
|
||||||
|
(22, "tcp", "ssh"),
|
||||||
|
(53, "udp", "dns"),
|
||||||
|
(67, "udp", "dhcp")
|
||||||
|
]
|
||||||
|
ok, ports = firewall_lan.verify_allowed(LAN_ROUTER_IP, svc)
|
||||||
|
if not ok:
|
||||||
|
print(f" ⚠ LAN services not properly accessible: {', '.join(ports)}")
|
||||||
|
|
||||||
|
svc = [(80, "tcp", "http")]
|
||||||
|
ok, ports = firewall_dmz.verify_allowed(DMZ_ROUTER_IP, svc)
|
||||||
|
if not ok:
|
||||||
|
print(f" ⚠ DMZ HTTP service not accessible: {', '.join(ports)}")
|
||||||
|
|
||||||
|
ok, ports, _ = firewall_wan.verify_blocked(WAN_ROUTER_IP)
|
||||||
|
if not ok:
|
||||||
|
print(f" ⚠ WAN has unexpected open ports: {', '.join(ports)}")
|
||||||
|
|
||||||
|
test.succeed()
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
graph "1x4" {
|
||||||
|
layout = "neato";
|
||||||
|
overlap = false;
|
||||||
|
esep = "+80";
|
||||||
|
|
||||||
|
node [shape=record, fontname="DejaVu Sans Mono, Book"];
|
||||||
|
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
|
||||||
|
|
||||||
|
host [
|
||||||
|
label="host | { <mgmt> mgmt | <wan> wan | <dmz> dmz | <lan> lan }",
|
||||||
|
pos="1,1!",
|
||||||
|
requires="controller"
|
||||||
|
];
|
||||||
|
|
||||||
|
gateway [
|
||||||
|
label="{ <mgmt> mgmt | <wan> wan | <dmz> dmz | <lan> lan } | gateway",
|
||||||
|
pos="3,1!",
|
||||||
|
requires="infix",
|
||||||
|
];
|
||||||
|
|
||||||
|
host:mgmt -- gateway:mgmt [requires="mgmt", color="lightgray"]
|
||||||
|
host:wan -- gateway:wan [color=red, fontcolor=red, taillabel="203.0.113.0/24"]
|
||||||
|
host:dmz -- gateway:dmz [color=orange, fontcolor=orange, taillabel="10.0.1.0/24"]
|
||||||
|
host:lan -- gateway:lan [color=black, fontcolor=black, taillabel="192.168.1.0/24"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||||
|
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
|
||||||
|
<!-- Title: 1x4 Pages: 1 -->
|
||||||
|
<svg width="432pt" height="101pt"
|
||||||
|
viewBox="0.00 0.00 432.03 101.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 97)">
|
||||||
|
<title>1x4</title>
|
||||||
|
<polygon fill="white" stroke="transparent" points="-4,4 -4,-97 428.03,-97 428.03,4 -4,4"/>
|
||||||
|
<!-- host -->
|
||||||
|
<g id="node1" class="node">
|
||||||
|
<title>host</title>
|
||||||
|
<polygon fill="none" stroke="black" points="0,-0.5 0,-92.5 100,-92.5 100,-0.5 0,-0.5"/>
|
||||||
|
<text text-anchor="middle" x="25" y="-42.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
|
||||||
|
<polyline fill="none" stroke="black" points="50,-0.5 50,-92.5 "/>
|
||||||
|
<text text-anchor="middle" x="75" y="-77.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||||
|
<polyline fill="none" stroke="black" points="50,-69.5 100,-69.5 "/>
|
||||||
|
<text text-anchor="middle" x="75" y="-54.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">wan</text>
|
||||||
|
<polyline fill="none" stroke="black" points="50,-46.5 100,-46.5 "/>
|
||||||
|
<text text-anchor="middle" x="75" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">dmz</text>
|
||||||
|
<polyline fill="none" stroke="black" points="50,-23.5 100,-23.5 "/>
|
||||||
|
<text text-anchor="middle" x="75" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">lan</text>
|
||||||
|
</g>
|
||||||
|
<!-- gateway -->
|
||||||
|
<g id="node2" class="node">
|
||||||
|
<title>gateway</title>
|
||||||
|
<polygon fill="none" stroke="black" points="300.03,-0.5 300.03,-92.5 424.03,-92.5 424.03,-0.5 300.03,-0.5"/>
|
||||||
|
<text text-anchor="middle" x="325.03" y="-77.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
|
||||||
|
<polyline fill="none" stroke="black" points="300.03,-69.5 350.03,-69.5 "/>
|
||||||
|
<text text-anchor="middle" x="325.03" y="-54.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">wan</text>
|
||||||
|
<polyline fill="none" stroke="black" points="300.03,-46.5 350.03,-46.5 "/>
|
||||||
|
<text text-anchor="middle" x="325.03" y="-31.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">dmz</text>
|
||||||
|
<polyline fill="none" stroke="black" points="300.03,-23.5 350.03,-23.5 "/>
|
||||||
|
<text text-anchor="middle" x="325.03" y="-8.3" font-family="DejaVu Sans Mono, Book" font-size="14.00">lan</text>
|
||||||
|
<polyline fill="none" stroke="black" points="350.03,-0.5 350.03,-92.5 "/>
|
||||||
|
<text text-anchor="middle" x="387.03" y="-42.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">gateway</text>
|
||||||
|
</g>
|
||||||
|
<!-- host--gateway -->
|
||||||
|
<g id="edge1" class="edge">
|
||||||
|
<title>host:mgmt--gateway:mgmt</title>
|
||||||
|
<path fill="none" stroke="lightgray" stroke-width="2" d="M100,-81.5C100,-81.5 300.03,-81.5 300.03,-81.5"/>
|
||||||
|
</g>
|
||||||
|
<!-- host--gateway -->
|
||||||
|
<g id="edge2" class="edge">
|
||||||
|
<title>host:wan--gateway:wan</title>
|
||||||
|
<path fill="none" stroke="red" stroke-width="2" d="M100,-58.5C100,-58.5 300.03,-58.5 300.03,-58.5"/>
|
||||||
|
<text text-anchor="middle" x="154.5" y="-62.3" font-family="DejaVu Serif, Book" font-size="14.00" fill="red">203.0.113.0/24</text>
|
||||||
|
</g>
|
||||||
|
<!-- host--gateway -->
|
||||||
|
<g id="edge3" class="edge">
|
||||||
|
<title>host:dmz--gateway:dmz</title>
|
||||||
|
<path fill="none" stroke="orange" stroke-width="2" d="M100,-34.5C100,-34.5 300.03,-34.5 300.03,-34.5"/>
|
||||||
|
<text text-anchor="middle" x="141" y="-38.3" font-family="DejaVu Serif, Book" font-size="14.00" fill="orange">10.0.1.0/24</text>
|
||||||
|
</g>
|
||||||
|
<!-- host--gateway -->
|
||||||
|
<g id="edge4" class="edge">
|
||||||
|
<title>host:lan--gateway:lan</title>
|
||||||
|
<path fill="none" stroke="black" stroke-width="2" d="M100,-11.5C100,-11.5 300.03,-11.5 300.03,-11.5"/>
|
||||||
|
<text text-anchor="middle" x="154.5" y="-15.3" font-family="DejaVu Serif, Book" font-size="14.00">192.168.1.0/24</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -1,4 +1,5 @@
|
|||||||
[7mUSB PORTS [0m
|
──────────────────────────────
|
||||||
|
[1mUSB PORTS[0m
|
||||||
[7mNAME STATE [0m
|
[7mNAME STATE [0m
|
||||||
USB locked
|
USB locked
|
||||||
USB2 locked
|
USB2 locked
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
FROM alpine:3.18.0
|
FROM alpine:3.18.0
|
||||||
|
|
||||||
|
# NOTE: please add packages alphabetically!
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
busybox-extras \
|
busybox-extras \
|
||||||
|
curl \
|
||||||
e2fsprogs \
|
e2fsprogs \
|
||||||
|
e2tools \
|
||||||
|
ethtool \
|
||||||
fakeroot \
|
fakeroot \
|
||||||
gcc \
|
gcc \
|
||||||
git \
|
git \
|
||||||
@@ -13,7 +17,10 @@ RUN apk add --no-cache \
|
|||||||
libc-dev \
|
libc-dev \
|
||||||
libyang-dev \
|
libyang-dev \
|
||||||
linux-headers \
|
linux-headers \
|
||||||
|
make \
|
||||||
|
nmap \
|
||||||
openssh-client \
|
openssh-client \
|
||||||
|
openssl \
|
||||||
python3-dev \
|
python3-dev \
|
||||||
qemu-img \
|
qemu-img \
|
||||||
qemu-system-x86_64 \
|
qemu-system-x86_64 \
|
||||||
@@ -22,12 +29,7 @@ RUN apk add --no-cache \
|
|||||||
squashfs-tools \
|
squashfs-tools \
|
||||||
sshpass \
|
sshpass \
|
||||||
tcpdump \
|
tcpdump \
|
||||||
tshark \
|
tshark
|
||||||
openssl \
|
|
||||||
curl \
|
|
||||||
e2tools \
|
|
||||||
make \
|
|
||||||
ethtool
|
|
||||||
|
|
||||||
ARG MTOOL_VERSION="3.0"
|
ARG MTOOL_VERSION="3.0"
|
||||||
RUN wget https://github.com/troglobit/mtools/releases/download/v3.0/mtools-$MTOOL_VERSION.tar.gz -O /tmp/mtools-$MTOOL_VERSION.tar.gz
|
RUN wget https://github.com/troglobit/mtools/releases/download/v3.0/mtools-$MTOOL_VERSION.tar.gz -O /tmp/mtools-$MTOOL_VERSION.tar.gz
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ from .container import Container
|
|||||||
from .env import Env
|
from .env import Env
|
||||||
from .env import ArgumentParser
|
from .env import ArgumentParser
|
||||||
from .env import test_argument
|
from .env import test_argument
|
||||||
|
from .firewall import Firewall
|
||||||
from .furl import Furl
|
from .furl import Furl
|
||||||
from .netns import IsolatedMacVlan,IsolatedMacVlans
|
from .netns import IsolatedMacVlan,IsolatedMacVlans
|
||||||
|
from .portscanner import PortScanner
|
||||||
from .sniffer import Sniffer
|
from .sniffer import Sniffer
|
||||||
from .tap import Test
|
from .tap import Test
|
||||||
from .util import parallel, until
|
from .util import parallel, until
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
"""
|
||||||
|
Firewall testing utilities
|
||||||
|
|
||||||
|
Provides a helper class and supporting tools to validate firewall
|
||||||
|
behavior in automated tests. Supports:
|
||||||
|
|
||||||
|
- SNAT verification by inspecting captured ICMP traffic
|
||||||
|
- Zone policy checks using targeted port scans
|
||||||
|
- Positive and negative policy validation (allowed vs. blocked ports)
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from typing import Tuple, List
|
||||||
|
from .sniffer import Sniffer
|
||||||
|
from .portscanner import PortScanner
|
||||||
|
from .util import until
|
||||||
|
|
||||||
|
|
||||||
|
class Firewall:
|
||||||
|
"""Specialized utilities for testing firewall functionality"""
|
||||||
|
|
||||||
|
def __init__(self, source=None, dest=None):
|
||||||
|
"""
|
||||||
|
Initialize firewall tester
|
||||||
|
Args:
|
||||||
|
source: Source network namespace (for traffic generation)
|
||||||
|
dest: Destination network namespace (for traffic capture)
|
||||||
|
"""
|
||||||
|
self.srcns = source
|
||||||
|
self.dstns = dest
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def wait_for_operational(target, expected_zones, timeout=30):
|
||||||
|
"""Wait for firewall config to be activated/available in operational"""
|
||||||
|
def check_operational():
|
||||||
|
try:
|
||||||
|
oper = target.get_data("/infix-firewall:firewall")
|
||||||
|
if not oper or "firewall" not in oper:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if "zone" not in oper["firewall"]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
zones = {z["name"]: z for z in oper["firewall"]["zone"]}
|
||||||
|
|
||||||
|
for zone_name, expected in expected_zones.items():
|
||||||
|
if zone_name not in zones:
|
||||||
|
return False
|
||||||
|
for key, value in expected.items():
|
||||||
|
if zones[zone_name].get(key) != value:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
until(check_operational, attempts=timeout)
|
||||||
|
|
||||||
|
def verify_snat(self, dest_ip: str, snat_ip: str,
|
||||||
|
timeout: int = 3) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Verify SNAT (masquerading) by analyzing source IP of ICMP traffic
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dest_ip: Destination IP address to ping
|
||||||
|
snat_ip: Expected source IP after SNAT (router's WAN IP)
|
||||||
|
timeout: Test timeout in seconds
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (snat_working: bool, details: str)
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
sniffer = Sniffer(self.dstns, "icmp")
|
||||||
|
with sniffer:
|
||||||
|
time.sleep(0.5)
|
||||||
|
self.srcns.runsh(f"ping -c3 -W{timeout} {dest_ip}")
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
rc = sniffer.output()
|
||||||
|
packets = rc.stdout
|
||||||
|
if rc.returncode or not packets.strip():
|
||||||
|
return False, "No packets captured — routing may be broken"
|
||||||
|
|
||||||
|
lines = packets.strip().split('\n')
|
||||||
|
snat_ip_found = False
|
||||||
|
lan_ip_found = False
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if we see the expected SNAT IP as source
|
||||||
|
if f"{snat_ip} > {dest_ip}" in line:
|
||||||
|
snat_ip_found = True
|
||||||
|
|
||||||
|
# Check if we see any other source IP (SNAT not working)
|
||||||
|
if f"> {dest_ip}" in line and snat_ip not in line:
|
||||||
|
parts = line.split()
|
||||||
|
for part in parts:
|
||||||
|
if f"> {dest_ip}" in part:
|
||||||
|
src_ip = part.split('>')[0].strip()
|
||||||
|
if '.' in src_ip and src_ip != snat_ip:
|
||||||
|
lan_ip_found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if snat_ip_found and not lan_ip_found:
|
||||||
|
return True, f"SNAT working: only traffic from {snat_ip}"
|
||||||
|
if lan_ip_found and not snat_ip_found:
|
||||||
|
return False, f"SNAT broken: LAN IPs visible, no {snat_ip}"
|
||||||
|
if snat_ip_found and lan_ip_found:
|
||||||
|
return False, f"SNAT broken: both {snat_ip} and LAN IPs on WAN"
|
||||||
|
|
||||||
|
return False, f"Unclear SNAT status, see capture:\n{packets}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"SNAT verification failed with error: {e}"
|
||||||
|
|
||||||
|
def verify_blocked(self, dest_ip: str, ports: List[Tuple[int, str, str]] = None,
|
||||||
|
exempt: List[int] = None, timeout: int = 3) -> Tuple[bool, List[str], List[str]]:
|
||||||
|
"""
|
||||||
|
Verify specified ports are blocked, with optional exceptions
|
||||||
|
Args:
|
||||||
|
dest_ip: Target hostname or IP address
|
||||||
|
ports: List of port tuples, defaults to
|
||||||
|
PortScanner.WELL_KNOWN_PORTS
|
||||||
|
exempt: List of ports that should be excempt
|
||||||
|
timeout: Connection timeout per port
|
||||||
|
Returns:
|
||||||
|
When exempt=None: Tuple of (all_blocked: bool, open_ports: List[str], [])
|
||||||
|
When exempt=[...]: Tuple of (policy_correct: bool, unexpected_open: List[str],
|
||||||
|
unexpected_filtered_allowed: List[str])
|
||||||
|
"""
|
||||||
|
if ports is None:
|
||||||
|
ports = PortScanner.WELL_KNOWN_PORTS
|
||||||
|
|
||||||
|
scanner = PortScanner(self.srcns)
|
||||||
|
results = scanner.scan_ports(dest_ip, ports, timeout)
|
||||||
|
|
||||||
|
if exempt is None:
|
||||||
|
# Simple "all blocked" behavior - only "open" is bad
|
||||||
|
open_ports = []
|
||||||
|
for port, name, result in results:
|
||||||
|
if result["status"] == "open":
|
||||||
|
open_ports.append(f"{name}({port})")
|
||||||
|
return len(open_ports) == 0, open_ports, []
|
||||||
|
|
||||||
|
unexpected_open = []
|
||||||
|
unexpected_filtered_allowed = []
|
||||||
|
|
||||||
|
for port, name, result in results:
|
||||||
|
if port in exempt:
|
||||||
|
# This port should be allowed (not filtered by firewall)
|
||||||
|
status = result["status"]
|
||||||
|
if status in ["filtered", "open|filtered", "closed|filtered"]:
|
||||||
|
unexpected_filtered_allowed.append(f"{name}({port})")
|
||||||
|
else:
|
||||||
|
# This port should be blocked - only "open" is bad
|
||||||
|
if result["status"] == "open":
|
||||||
|
unexpected_open.append(f"{name}({port})")
|
||||||
|
|
||||||
|
policy_correct = (len(unexpected_open) == 0 and
|
||||||
|
len(unexpected_filtered_allowed) == 0)
|
||||||
|
return policy_correct, unexpected_open, unexpected_filtered_allowed
|
||||||
|
|
||||||
|
def verify_allowed(self, dest_ip: str, ports: List[Tuple[int, str, str]] = None,
|
||||||
|
timeout: int = 3) -> Tuple[bool, List[str]]:
|
||||||
|
"""
|
||||||
|
Verify specified ports are allowed (open or closed, not filtered)
|
||||||
|
Args:
|
||||||
|
dest_ip: Target hostname or IP address
|
||||||
|
ports: List of port tuples, defaults to
|
||||||
|
PortScanner.WELL_KNOWN_PORTS
|
||||||
|
timeout: Connection timeout per port
|
||||||
|
Returns:
|
||||||
|
Tuple of (all_allowed: bool, filtered_ports: List[str])
|
||||||
|
"""
|
||||||
|
if ports is None:
|
||||||
|
ports = PortScanner.WELL_KNOWN_PORTS
|
||||||
|
|
||||||
|
scanner = PortScanner(self.srcns)
|
||||||
|
results = scanner.scan_ports(dest_ip, ports, timeout)
|
||||||
|
filtered_ports = []
|
||||||
|
|
||||||
|
for port, name, result in results:
|
||||||
|
status = result["status"]
|
||||||
|
# Consider any form of filtering as "not allowed"
|
||||||
|
if status in ["filtered", "open|filtered", "closed|filtered"]:
|
||||||
|
filtered_ports.append(f"{name}({port})")
|
||||||
|
|
||||||
|
return len(filtered_ports) == 0, filtered_ports
|
||||||
|
|
||||||
|
def verify_dnat(self, gateway_ip: str, forward_port: int, target_port: int,
|
||||||
|
timeout: int = 5) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Verify DNAT (port forwarding) by testing end-to-end connectivity
|
||||||
|
|
||||||
|
Args:
|
||||||
|
gateway_ip: Gateway IP where port forwarding is configured
|
||||||
|
forward_port: External port being forwarded (e.g., 8080)
|
||||||
|
target_port: Internal target port (e.g., 80)
|
||||||
|
timeout: Connection timeout
|
||||||
|
Returns:
|
||||||
|
Tuple of (dnat_working: bool, details: str)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Use netcat to simulate a simple service on target port
|
||||||
|
cmd = f"nc -l -p {target_port} -e /bin/echo 'DNAT-TEST-OK'"
|
||||||
|
pid = self.dstns.popen(cmd.split(), stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE)
|
||||||
|
time.sleep(1) # Give server time to start
|
||||||
|
|
||||||
|
# Test connection from source to gateway:forward_port
|
||||||
|
cmd = f"nc -w {timeout} {gateway_ip} {forward_port}"
|
||||||
|
result = self.srcns.runsh(cmd)
|
||||||
|
|
||||||
|
try:
|
||||||
|
pid.terminate()
|
||||||
|
pid.wait(timeout=1)
|
||||||
|
except:
|
||||||
|
pid.kill()
|
||||||
|
|
||||||
|
# Check if we got the expected response
|
||||||
|
if "DNAT-TEST-OK" in result.stdout:
|
||||||
|
return True, f"DNAT working: {gateway_ip}:{forward_port} → target:{target_port}"
|
||||||
|
if result.returncode == 0:
|
||||||
|
return True, f"DNAT working: connection successful to {gateway_ip}:{forward_port}"
|
||||||
|
|
||||||
|
return False, f"DNAT failed: no response from {gateway_ip}:{forward_port}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"DNAT verification failed with error: {e}"
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
"""
|
||||||
|
Port scanning utilities
|
||||||
|
|
||||||
|
Lightweight wrapper around nmap for automated firewall testing.
|
||||||
|
Supports:
|
||||||
|
|
||||||
|
- Scanning individual TCP/UDP ports with detailed state detection
|
||||||
|
- Parallel scanning of multiple ports using threads
|
||||||
|
- Predefined set of common service ports for convenience
|
||||||
|
"""
|
||||||
|
import threading
|
||||||
|
from typing import List, Dict, Union, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
class PortScanner:
|
||||||
|
"""Simple port scanner using netcat for firewall testing"""
|
||||||
|
|
||||||
|
# Well-known ports for testing common services
|
||||||
|
WELL_KNOWN_PORTS = [
|
||||||
|
(22, "tcp", "ssh"),
|
||||||
|
(53, "udp", "dns"),
|
||||||
|
(67, "udp", "dhcp"),
|
||||||
|
(69, "udp", "tftp"),
|
||||||
|
(80, "tcp", "http"),
|
||||||
|
(443, "tcp", "https"),
|
||||||
|
(5353, "udp", "mdns"),
|
||||||
|
(7681, "tcp", "ttyd"),
|
||||||
|
(8080, "tcp", "http-alt"),
|
||||||
|
(8443, "tcp", "https-alt"),
|
||||||
|
(7, "tcp", "echo"),
|
||||||
|
(1234, "tcp", "test-mid"),
|
||||||
|
(9999, "tcp", "test-high"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, netns=None):
|
||||||
|
"""
|
||||||
|
Initialize port scanner
|
||||||
|
Args:
|
||||||
|
netns: Network namespace object (IsolatedMacVlan) or netns name
|
||||||
|
"""
|
||||||
|
self.netns = netns
|
||||||
|
|
||||||
|
def scan_port(self, host: str, port: int, protocol: str = "tcp",
|
||||||
|
timeout: int = 3) -> Dict[str, Union[str, bool]]:
|
||||||
|
"""
|
||||||
|
Scan a single port using nmap for accurate firewall state detection
|
||||||
|
Args:
|
||||||
|
host: Target hostname or IP address
|
||||||
|
port: Port number to scan
|
||||||
|
protocol: 'tcp' or 'udp'
|
||||||
|
timeout: Connection timeout in seconds
|
||||||
|
Returns:
|
||||||
|
Dict with keys: 'open' (bool), 'status' (str), 'response' (str)
|
||||||
|
"""
|
||||||
|
# Build optimized nmap command based on protocol
|
||||||
|
if protocol.lower() == "tcp":
|
||||||
|
proto = "-sT"
|
||||||
|
elif protocol.lower() == "udp":
|
||||||
|
proto = "-sU"
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unsupported protocol: {protocol}")
|
||||||
|
|
||||||
|
in6 = ""
|
||||||
|
if ":" in host:
|
||||||
|
in6 = "-6"
|
||||||
|
|
||||||
|
cmd = f"nmap -n {proto} {in6} -Pn -p {port} --host-timeout={timeout}" \
|
||||||
|
f" --min-rate=1000 --max-retries=1 --disable-arp-ping {host}"
|
||||||
|
result = self.netns.runsh(cmd)
|
||||||
|
|
||||||
|
# Parse nmap output to determine port state
|
||||||
|
output = result.stdout
|
||||||
|
|
||||||
|
# Look for the specific port line in nmap output
|
||||||
|
# Format: "PORT STATE SERVICE" or "22/tcp open ssh"
|
||||||
|
port_line = None
|
||||||
|
for line in output.split('\n'):
|
||||||
|
if f"{port}/" in line and protocol in line:
|
||||||
|
port_line = line.strip()
|
||||||
|
break
|
||||||
|
|
||||||
|
if port_line:
|
||||||
|
if "open|filtered" in port_line:
|
||||||
|
# No ICMP port unreachable, likely firewall dropping silently
|
||||||
|
status = "open|filtered"
|
||||||
|
is_open = False
|
||||||
|
elif "closed|filtered" in port_line:
|
||||||
|
# No service running, or firewall dropping
|
||||||
|
status = "closed|filtered"
|
||||||
|
is_open = False
|
||||||
|
elif "open" in port_line:
|
||||||
|
status = "open"
|
||||||
|
is_open = True
|
||||||
|
elif "filtered" in port_line:
|
||||||
|
# Blocked/Rejected by firewall
|
||||||
|
status = "filtered"
|
||||||
|
is_open = False
|
||||||
|
elif "closed" in port_line:
|
||||||
|
# Port reachable but service not running
|
||||||
|
status = "closed"
|
||||||
|
is_open = False
|
||||||
|
else:
|
||||||
|
# Unknown state
|
||||||
|
status = "unknown"
|
||||||
|
is_open = False
|
||||||
|
else:
|
||||||
|
# No port line found - likely filtered or error
|
||||||
|
status = "filtered"
|
||||||
|
is_open = False
|
||||||
|
|
||||||
|
return {
|
||||||
|
"open": is_open,
|
||||||
|
"status": status,
|
||||||
|
"response": output.strip()
|
||||||
|
}
|
||||||
|
|
||||||
|
def scan_ports(self, host: str,
|
||||||
|
port_specs: List[Tuple[int, str, str]],
|
||||||
|
timeout: int = 3) -> List[Tuple[int, str, Dict]]:
|
||||||
|
"""
|
||||||
|
Scan multiple ports in parallel using threads
|
||||||
|
Args:
|
||||||
|
host: Target hostname or IP address
|
||||||
|
port_specs: List (port, protocol, name) e.g.
|
||||||
|
[(80, "tcp", "http"), (53, "udp", "dns")]
|
||||||
|
timeout: Connection timeout per port
|
||||||
|
Returns:
|
||||||
|
List of (port, name, result) scan results.
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
threads = []
|
||||||
|
lock = threading.Lock()
|
||||||
|
|
||||||
|
def scan_worker(port: int, protocol: str, name: str):
|
||||||
|
try:
|
||||||
|
result = self.scan_port(host, port, protocol, timeout)
|
||||||
|
with lock:
|
||||||
|
results.append((port, name, result))
|
||||||
|
except Exception as e:
|
||||||
|
with lock:
|
||||||
|
results.append((port, name, {
|
||||||
|
"open": False,
|
||||||
|
"status": "error",
|
||||||
|
"response": str(e)
|
||||||
|
}))
|
||||||
|
|
||||||
|
for port, protocol, name in port_specs:
|
||||||
|
thread = threading.Thread(target=scan_worker, args=(port, protocol, name))
|
||||||
|
threads.append(thread)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
for thread in threads:
|
||||||
|
thread.join()
|
||||||
|
|
||||||
|
# Sort results by port number for consistent output
|
||||||
|
results.sort(key=lambda x: x[0])
|
||||||
|
return results
|
||||||
@@ -40,6 +40,10 @@ include::../case/ietf_routing/Readme.adoc[]
|
|||||||
|
|
||||||
<<<
|
<<<
|
||||||
|
|
||||||
|
include::../case/infix_firewall/Readme.adoc[]
|
||||||
|
|
||||||
|
<<<
|
||||||
|
|
||||||
include::../case/infix_containers/Readme.adoc[]
|
include::../case/infix_containers/Readme.adoc[]
|
||||||
|
|
||||||
<<<
|
<<<
|
||||||
|
|||||||