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 mike
|
||||
pipx inject mkdocs mkdocs-to-pdf
|
||||
pipx inject mkdocs mkdocs-glightbox
|
||||
# Workaround, if pipx inject fails to install symlink
|
||||
ln -s "$(pipx environment -V PIPX_LOCAL_VENVS)/mkdocs/bin/mike" \
|
||||
"$(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]
|
||||
|
||||
<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
|
||||
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
|
||||
opts="-n1"
|
||||
|
||||
if [ "$1" = "-q" ]; then
|
||||
opts="$opts -s"
|
||||
shift
|
||||
fi
|
||||
|
||||
Q=$@
|
||||
|
||||
/bin/echo -n "$Q, are you sure (y/N)? "
|
||||
read -n1 yorn
|
||||
read $opts yorn
|
||||
echo
|
||||
|
||||
if [ x$yorn != "xy" ] && [ x$yorn != "xY" ]; then
|
||||
if [ "x$yorn" != "xy" ] && [ "x$yorn" != "xY" ]; then
|
||||
echo "OK, aborting."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -73,6 +73,7 @@ BR2_PACKAGE_CONNTRACK_TOOLS=y
|
||||
BR2_PACKAGE_DNSMASQ=y
|
||||
BR2_PACKAGE_ETHTOOL=y
|
||||
BR2_PACKAGE_FPING=y
|
||||
BR2_PACKAGE_FIREWALL=y
|
||||
BR2_PACKAGE_FRR=y
|
||||
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
|
||||
BR2_PACKAGE_IPERF3=y
|
||||
|
||||
@@ -67,6 +67,7 @@ BR2_PACKAGE_AVAHI_DEFAULT_SERVICES=y
|
||||
BR2_PACKAGE_CHRONY=y
|
||||
BR2_PACKAGE_DNSMASQ=y
|
||||
BR2_PACKAGE_ETHTOOL=y
|
||||
BR2_PACKAGE_FIREWALL=y
|
||||
BR2_PACKAGE_FRR=y
|
||||
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
|
||||
BR2_PACKAGE_IPROUTE2=y
|
||||
|
||||
@@ -91,6 +91,7 @@ BR2_PACKAGE_CONNTRACK_TOOLS=y
|
||||
BR2_PACKAGE_DNSMASQ=y
|
||||
BR2_PACKAGE_ETHTOOL=y
|
||||
BR2_PACKAGE_FPING=y
|
||||
BR2_PACKAGE_FIREWALL=y
|
||||
BR2_PACKAGE_FRR=y
|
||||
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
|
||||
BR2_PACKAGE_IPERF3=y
|
||||
|
||||
@@ -86,6 +86,7 @@ BR2_PACKAGE_CONNTRACK_TOOLS=y
|
||||
BR2_PACKAGE_DNSMASQ=y
|
||||
BR2_PACKAGE_ETHTOOL=y
|
||||
BR2_PACKAGE_FPING=y
|
||||
BR2_PACKAGE_FIREWALL=y
|
||||
BR2_PACKAGE_FRR=y
|
||||
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
|
||||
BR2_PACKAGE_IPERF3=y
|
||||
|
||||
@@ -71,6 +71,7 @@ BR2_PACKAGE_CONNTRACK_TOOLS=y
|
||||
BR2_PACKAGE_DNSMASQ=y
|
||||
BR2_PACKAGE_ETHTOOL=y
|
||||
BR2_PACKAGE_FPING=y
|
||||
BR2_PACKAGE_FIREWALL=y
|
||||
BR2_PACKAGE_FRR=y
|
||||
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
|
||||
BR2_PACKAGE_IPERF3=y
|
||||
|
||||
@@ -65,6 +65,7 @@ BR2_PACKAGE_AVAHI_DEFAULT_SERVICES=y
|
||||
BR2_PACKAGE_CHRONY=y
|
||||
BR2_PACKAGE_DNSMASQ=y
|
||||
BR2_PACKAGE_ETHTOOL=y
|
||||
BR2_PACKAGE_FIREWALL=y
|
||||
BR2_PACKAGE_FRR=y
|
||||
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
|
||||
BR2_PACKAGE_IPROUTE2=y
|
||||
|
||||
@@ -1,3 +1,53 @@
|
||||
* TODO Add support for firewall
|
||||
- [X] All "implicit" policies in zones are now policies: intra- and inter-zone policies
|
||||
- [X] Add locked rules for implicit drop/reject policy as last rule in policy (ANY, ANY)
|
||||
- [X] Firewall logs should show IN=iface (IIF) before SOURCE
|
||||
- [X] Interfaces are not defaulting to the default zone, must handle bridge
|
||||
ports and changes to enslavement, so regenerate every time is a must!
|
||||
- [ ] firewalld helpers -- possibly for conntrack, e.g., ftp
|
||||
- [ ] With =modprobe br_netfilter= firewalld would see *all* traffic, but there are
|
||||
issues, <https://github.com/firewalld/firewalld/issues/1236>, and limits to what
|
||||
seem to be possible atm. You may also need to enable these callbacks:
|
||||
=echo 1 | sudo tee /proc/sys/net/bridge/bridge-nf-call-iptables=
|
||||
=echo 1 | sudo tee /proc/sys/net/bridge/bridge-nf-call-ip6tables=
|
||||
=echo 1 | sudo tee /proc/sys/net/bridge/bridge-nf-call-arptables=
|
||||
- [X] Add missing upper to port forward since port ranges are supported, see services!
|
||||
- [X] =[do] show firewall= not available yet
|
||||
- [X] Add RPC to pause firewall using =firewall-cmd --panic-on= and restart
|
||||
firewall again with =firewall-cmd --panic-off=. The current state can
|
||||
be queried using =firewall-cmd --query-panic=, which returns =yes=
|
||||
- [X] Remove debug log messages!
|
||||
- [X] Add "Log Messages" section to =show firewall= when =LogDenied ≠ off=
|
||||
- [ ] Investigate filtering out firewall log messages from other log files
|
||||
- [1/2] Rename policy->policy to policy->action, and replace allow->forward
|
||||
- [X] Rename zone->sources to networks
|
||||
- [X] A zone's action is for ingress, clarify this if missing!
|
||||
- [X] Any services/ports listed in a zone with policy:accept are a NO-OP
|
||||
- [X] =firwall-cmd --reload= takes fooooorever! :-(
|
||||
- [X] If forwarding is disabled in a zone then the zone matrix should
|
||||
show deny for the same zone-to-zone communication
|
||||
- [X] We should show the implicit rules for communicating with the HOST
|
||||
- [X] Investigate "padlock" on built-in policys (and zones?) and expose more?
|
||||
- [X] Document established,related somewhere, fixed/padlocked policy? Also,
|
||||
document why this is a good idea to always have enabled. See RH docs.
|
||||
- [ ] Podman published ports, <https://firewalld.org/2024/11/strict-forward-ports>
|
||||
- [ ] Software fastpath <https://firewalld.org/2023/05/nftables-flowtable>
|
||||
- +[ ] Allow overriding/editing immutable policies and zones+
|
||||
- [X] Add tests: basic (end device), wan-lan, wan-lan-dmz, +hammer (stress)+
|
||||
- [X] Add documentation
|
||||
- See <https://docs.rockylinux.org/guides/security/firewalld-beginners/>
|
||||
- Add some tool tips: nc, nmap, ping, and socat to stress the firewall
|
||||
- [X] Fix inference so we can remove defaults from factory-config!
|
||||
- [X] Add iperf service
|
||||
- [X] Add nftables ownership=yes setting, introduced in later firewalld versions
|
||||
- [ ] Investigate fail2ban integration with firewalld, for more info, see:
|
||||
https://github.com/firewalld/firewalld/issues/1466#issuecomment-2773130569
|
||||
- [ ] Update screenshots for documentation
|
||||
- [ ] Review both cli-pretty and yanger code
|
||||
- [ ] Review default-zone handling (needed?)
|
||||
- [ ] Clean up =INFER_POLICY= ifdefs
|
||||
- [ ] Revisit built-in fallback zones (public, block, drop)
|
||||
|
||||
* TODO doc: User Guide
|
||||
|
||||
- Feature set and scope, e.g.
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
line-height: 2.6rem;
|
||||
}
|
||||
|
||||
.md-typeset code {
|
||||
font-size: .75em;
|
||||
}
|
||||
|
||||
[data-md-color-primary="orange"] {
|
||||
--md-primary-fg-color: #ff7f2a;
|
||||
--md-primary-bg-color: #5c5f5c;
|
||||
@@ -10,3 +14,13 @@
|
||||
[data-md-color-primary="black"] {
|
||||
--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
|
||||
- Network Calculator: cli/netcalc.md
|
||||
- Network Monitoring: cli/tcpdump.md
|
||||
- Quickstart Guide: cli/quick.md
|
||||
- Quickstart Guide: cli/quick.md
|
||||
- Text Editor: cli/text-editor.md
|
||||
- Upgrading: cli/upgrade.md
|
||||
- Docker Containers: container.md
|
||||
- Networking:
|
||||
- Network Configuration: networking.md
|
||||
- Firewall Configuration: firewall.md
|
||||
- Quality of Service: qos.md
|
||||
- RMON Counters: eth-counters.md
|
||||
- Tunneling (L2/L3): tunnels.md
|
||||
@@ -98,10 +99,18 @@ theme:
|
||||
icon: material/weather-sunny
|
||||
name: Switch to light mode
|
||||
|
||||
# https://squidfunk.github.io/mkdocs-material/reference/formatting/
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- attr_list
|
||||
- footnotes
|
||||
- md_in_html
|
||||
- pymdownx.blocks.caption
|
||||
- pymdownx.critic
|
||||
- pymdownx.caret
|
||||
- pymdownx.keys
|
||||
- pymdownx.mark
|
||||
- pymdownx.tilde
|
||||
- pymdownx.details
|
||||
- pymdownx.superfences
|
||||
- pymdownx.highlight:
|
||||
@@ -118,6 +127,23 @@ plugins:
|
||||
- search
|
||||
- callouts
|
||||
- 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:
|
||||
cover: true
|
||||
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/faux/Config.in"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/finit/Config.in"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/firewall/Config.in"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/greenpak-programmer/Config.in"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/ifupdown-ng/Config.in"
|
||||
source "$BR2_EXTERNAL_INFIX_PATH/package/iito/Config.in"
|
||||
|
||||
@@ -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 \
|
||||
gen-interfaces gen-motd gen-hardware gen-version \
|
||||
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-server.c \
|
||||
infix-factory.c \
|
||||
infix-firewall.c \
|
||||
infix-meta.c \
|
||||
infix-services.c \
|
||||
infix-system-software.c \
|
||||
|
||||
@@ -172,6 +172,9 @@ int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv)
|
||||
if (rc)
|
||||
goto err;
|
||||
rc = ietf_hardware_init(&confd);
|
||||
if (rc)
|
||||
goto err;
|
||||
rc = infix_firewall_init(&confd);
|
||||
if (rc)
|
||||
goto err;
|
||||
|
||||
|
||||
@@ -256,4 +256,7 @@ int ietf_hardware_init(struct confd *confd);
|
||||
/* ietf-keystore.c */
|
||||
int ietf_keystore_init(struct confd *confd);
|
||||
|
||||
/* infix-firewall.c */
|
||||
int infix_firewall_init(struct confd *confd);
|
||||
|
||||
#endif /* CONFD_CORE_H_ */
|
||||
|
||||
@@ -908,6 +908,72 @@ cleanup:
|
||||
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 rc;
|
||||
|
||||
@@ -102,6 +102,7 @@ int netdag_gen_ethtool(struct dagger *net, struct lyd_node *cif, struct lyd_node
|
||||
/* ietf-interfaces.c */
|
||||
const char *get_chassis_addr(void);
|
||||
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 */
|
||||
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-client@2025-01-29.yang"
|
||||
"infix-dhcp-server@2025-01-29.yang"
|
||||
"infix-firewall@2025-04-26.yang"
|
||||
"infix-firewall-services@2025-04-26.yang"
|
||||
"infix-firewall-icmp-types@2025-04-26.yang"
|
||||
"infix-meta@2024-10-18.yang"
|
||||
"infix-system@2025-01-25.yang"
|
||||
"infix-services@2024-12-03.yang"
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
module infix-firewall-icmp-types {
|
||||
yang-version 1.1;
|
||||
namespace "urn:infix:firewall:icmp-types:ns:yang:1.0";
|
||||
prefix ifw-icmp;
|
||||
|
||||
organization "KernelKit";
|
||||
contact "kernelkit@googlegroups.com";
|
||||
description "Common well-defined network services.";
|
||||
|
||||
revision 2025-04-26 {
|
||||
description "Initial revision.";
|
||||
reference "internal";
|
||||
}
|
||||
|
||||
/* firewall-cmd --get-icmptypes */
|
||||
typedef type {
|
||||
description "Available ICMP/ICMPv6 types.";
|
||||
|
||||
type enumeration {
|
||||
enum address-unreachable {
|
||||
description "Error sent when a packet cannot be delivered to its IPv6 destination address.";
|
||||
}
|
||||
enum bad-header {
|
||||
description "IPv6 error indicating there is a problem with the packet header structure or format.";
|
||||
}
|
||||
enum beyond-scope {
|
||||
description "IPv6 error sent when transmitting a packet would cross a zone boundary of the source address scope.";
|
||||
}
|
||||
enum communication-prohibited {
|
||||
description "Error indicating that communication with the destination has been administratively blocked.";
|
||||
}
|
||||
enum destination-unreachable {
|
||||
description "General error sent by hosts or gateways when a destination cannot be reached.";
|
||||
}
|
||||
enum echo-reply {
|
||||
description "Response message sent back to acknowledge receipt of an echo request (ping response/pong).";
|
||||
}
|
||||
enum echo-request {
|
||||
description "Test message used to check if a host is reachable, commonly sent by the ping utility.";
|
||||
}
|
||||
enum failed-policy {
|
||||
description "IPv6 error indicating the source address failed to meet ingress or egress policy requirements.";
|
||||
}
|
||||
enum fragmentation-needed {
|
||||
description "IPv4 error sent when a packet needs fragmentation but the 'Don't Fragment' flag is set.";
|
||||
}
|
||||
enum host-precedence-violation {
|
||||
description "IPv4 error sent when communication is administratively prohibited due to precedence rules.";
|
||||
}
|
||||
enum host-prohibited {
|
||||
description "IPv4 error indicating that access from a specific host has been administratively blocked.";
|
||||
}
|
||||
enum host-redirect {
|
||||
description "IPv4 message instructing to redirect packets to a different route for the specific host.";
|
||||
}
|
||||
enum host-unknown {
|
||||
description "IPv4 error sent when the destination host cannot be identified or located.";
|
||||
}
|
||||
enum host-unreachable {
|
||||
description "IPv4 error sent when the destination host exists but cannot be reached.";
|
||||
}
|
||||
enum ip-header-bad {
|
||||
description "IPv4 error indicating malformed or corrupted IP header information.";
|
||||
}
|
||||
enum mld-listener-done {
|
||||
description "IPv6 multicast message sent when a host leaves a multicast group.";
|
||||
}
|
||||
enum mld-listener-query {
|
||||
description "IPv6 multicast router query to discover group membership information.";
|
||||
}
|
||||
enum mld-listener-report {
|
||||
description "IPv6 multicast message sent by hosts to report group membership.";
|
||||
}
|
||||
enum mld2-listener-report {
|
||||
description "IPv6 multicast listener report message for MLDv2 enhanced reporting.";
|
||||
}
|
||||
enum neighbour-advertisement {
|
||||
description "IPv6 message sent in response to neighbor solicitation to propagate new network information.";
|
||||
}
|
||||
enum neighbour-solicitation {
|
||||
description "IPv6 message used to discover link-layer addresses of neighbors and verify reachability.";
|
||||
}
|
||||
enum network-prohibited {
|
||||
description "IPv4 error sent when access to an entire network has been administratively blocked.";
|
||||
}
|
||||
enum network-redirect {
|
||||
description "IPv4 message instructing to redirect packets to a different route for the entire network.";
|
||||
}
|
||||
enum network-unknown {
|
||||
description "IPv4 error sent when the destination network cannot be identified or located.";
|
||||
}
|
||||
enum network-unreachable {
|
||||
description "IPv4 error sent when the destination network exists but cannot be reached.";
|
||||
}
|
||||
enum no-route {
|
||||
description "IPv6 error sent when there is no routing table entry available for the destination.";
|
||||
}
|
||||
enum packet-too-big {
|
||||
description "IPv6 error sent by routers when they cannot forward a packet because it exceeds the MTU.";
|
||||
}
|
||||
enum parameter-problem {
|
||||
description "Error sent when IP header contains bad parameters or missing required options.";
|
||||
}
|
||||
enum port-unreachable {
|
||||
description "Error sent when the destination port on a reachable host is not available or not listening.";
|
||||
}
|
||||
enum precedence-cutoff {
|
||||
description "IPv4 error sent when the packet's precedence level is lower than the required minimum.";
|
||||
}
|
||||
enum protocol-unreachable {
|
||||
description "IPv4 error sent when the specified protocol is not supported at the destination.";
|
||||
}
|
||||
enum redirect {
|
||||
description "General message instructing a host to use a different route for future packets.";
|
||||
}
|
||||
enum reject-route {
|
||||
description "IPv6 error sent when the routing table explicitly rejects the route to the destination.";
|
||||
}
|
||||
enum required-option-missing {
|
||||
description "IPv4 error sent when a mandatory IP option is not present in the packet header.";
|
||||
}
|
||||
enum router-advertisement {
|
||||
description "Message sent by routers to periodically announce their presence and network configuration.";
|
||||
}
|
||||
enum router-solicitation {
|
||||
description "Message sent by hosts to request router advertisements and discover available routers.";
|
||||
}
|
||||
enum source-quench {
|
||||
description "IPv4 flow control message telling a host to reduce its packet transmission rate.";
|
||||
}
|
||||
enum source-route-failed {
|
||||
description "IPv4 error sent when source routing specified in the packet cannot be completed.";
|
||||
}
|
||||
enum time-exceeded {
|
||||
description "Error sent when a packet's time-to-live expires during transit or reassembly.";
|
||||
}
|
||||
enum timestamp-reply {
|
||||
description "IPv4 response message containing timestamp information for network time synchronization.";
|
||||
}
|
||||
enum timestamp-request {
|
||||
description "IPv4 message requesting timestamp information from the destination for time synchronization.";
|
||||
}
|
||||
enum tos-host-redirect {
|
||||
description "IPv4 message instructing to redirect packets based on both the type of service and specific host.";
|
||||
}
|
||||
enum tos-host-unreachable {
|
||||
description "IPv4 error sent when a host is unreachable for the specific type of service requested.";
|
||||
}
|
||||
enum tos-network-redirect {
|
||||
description "IPv4 message instructing to redirect packets based on both the type of service and network.";
|
||||
}
|
||||
enum tos-network-unreachable {
|
||||
description "IPv4 error sent when a network is unreachable for the specific type of service requested.";
|
||||
}
|
||||
enum ttl-zero-during-reassembly {
|
||||
description "Error sent when a host fails to completely reassemble fragmented packets within the time limit.";
|
||||
}
|
||||
enum ttl-zero-during-transit {
|
||||
description "Error sent when a packet's time-to-live counter reaches zero while being forwarded.";
|
||||
}
|
||||
enum unknown-header-type {
|
||||
description "IPv6 error sent when an unrecognized Next Header type is encountered in the packet.";
|
||||
}
|
||||
enum unknown-option {
|
||||
description "IPv6 error sent when an unrecognized or unsupported IPv6 option is encountered.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
infix-firewall-icmp-types.yang
|
||||
@@ -0,0 +1,386 @@
|
||||
module infix-firewall-services {
|
||||
yang-version 1.1;
|
||||
namespace "urn:infix:firewall:services:ns:yang:1.0";
|
||||
prefix ifw-svc;
|
||||
|
||||
organization "KernelKit";
|
||||
contact "kernelkit@googlegroups.com";
|
||||
description "Common well-defined network services.";
|
||||
|
||||
revision 2025-04-26 {
|
||||
description "Initial revision.";
|
||||
reference "internal";
|
||||
}
|
||||
|
||||
typedef well-known-service {
|
||||
description "Well-known network services, with standard port assignments from IANA.";
|
||||
type enumeration {
|
||||
enum "amqp" {
|
||||
description "5672/tcp — Advanced Message Queuing Protocol for message-oriented middleware";
|
||||
}
|
||||
enum "amqps" {
|
||||
description "5671/tcp — Secure Advanced Message Queuing Protocol over SSL";
|
||||
}
|
||||
enum "apcupsd" {
|
||||
description "3551/tcp — APC uninterruptible power supply daemon protocol";
|
||||
}
|
||||
enum "audit" {
|
||||
description "60/tcp — Linux audit subsystem for security event logging";
|
||||
}
|
||||
enum "bacula" {
|
||||
description "9101-9103/tcp — Open source network backup tool";
|
||||
}
|
||||
enum "bacula-client" {
|
||||
description "9102/tcp — Bacula client for backup operations";
|
||||
}
|
||||
enum "bgp" {
|
||||
description "179/tcp — Border Gateway Protocol for internet routing";
|
||||
}
|
||||
enum "cockpit" {
|
||||
description "9090/tcp — Web-based server administration interface";
|
||||
}
|
||||
enum "dhcp" {
|
||||
description "67-68/udp — Dynamic Host Configuration Protocol for network configuration";
|
||||
}
|
||||
enum "dhcpv6" {
|
||||
description "547/udp — Allow incoming DHCP for IPv6 requests from clients or relay agents.";
|
||||
}
|
||||
enum "dhcpv6-client" {
|
||||
description "546/udp — Allow a DHCP for IPv6 client to obtain a lease.";
|
||||
}
|
||||
enum "dns" {
|
||||
description "53/tcp+udp — Domain Name System for name resolution";
|
||||
}
|
||||
enum "dns-over-quic" {
|
||||
description "853/udp — DNS over QUIC protocol for encrypted domain name resolution";
|
||||
}
|
||||
enum "dns-over-tls" {
|
||||
description "853/tcp — DNS over TLS protocol for encrypted domain name resolution";
|
||||
}
|
||||
enum "elasticsearch" {
|
||||
description "9300/tcp — Distributed search and analytics engine";
|
||||
}
|
||||
enum "ftp" {
|
||||
description "20-21/tcp — File Transfer Protocol for file transfers";
|
||||
}
|
||||
enum "git" {
|
||||
description "9418/tcp — Git daemon for version control repository access";
|
||||
}
|
||||
enum "grafana" {
|
||||
description "3000/tcp — Analytics and monitoring dashboard platform";
|
||||
}
|
||||
enum "gpsd" {
|
||||
description "2947/tcp — GPS daemon for location services";
|
||||
}
|
||||
enum "gre" {
|
||||
description "47/ip — Generic Routing Encapsulation for tunneling";
|
||||
}
|
||||
enum "http" {
|
||||
description "80/tcp — Hypertext Transfer Protocol for web traffic";
|
||||
}
|
||||
enum "https" {
|
||||
description "443/tcp — Secure Hypertext Transfer Protocol for encrypted web traffic";
|
||||
}
|
||||
enum "http3" {
|
||||
description "443/udp — HTTP/3 protocol over QUIC for faster web traffic";
|
||||
}
|
||||
enum "imap" {
|
||||
description "143/tcp — Internet Message Access Protocol for email access";
|
||||
}
|
||||
enum "imaps" {
|
||||
description "993/tcp — Secure Internet Message Access Protocol for encrypted email access";
|
||||
}
|
||||
enum "ipp" {
|
||||
description "631/tcp+udp — Internet Printing Protocol (IPP) is used for distributed printing.";
|
||||
}
|
||||
enum "iperf2" {
|
||||
description "5001/tcp — Network bandwidth measurement tool version 2";
|
||||
}
|
||||
enum "iperf3" {
|
||||
description "5201/tcp — Network bandwidth measurement tool version 3";
|
||||
}
|
||||
enum "ipsec" {
|
||||
description "500/udp — Internet Protocol Security for VPN connections";
|
||||
}
|
||||
enum "irc" {
|
||||
description "6667/tcp — Internet Relay Chat for text messaging";
|
||||
}
|
||||
enum "ircs" {
|
||||
description "6697/tcp — Secure Internet Relay Chat over SSL";
|
||||
}
|
||||
enum "jenkins" {
|
||||
description "8080/tcp — Open source automation server for CI/CD";
|
||||
}
|
||||
enum "kerberos" {
|
||||
description "88/tcp+udp — Network authentication protocol";
|
||||
}
|
||||
enum "kadmin" {
|
||||
description "749/tcp — Kerberos administration server";
|
||||
}
|
||||
enum "kibana" {
|
||||
description "5601/tcp — Data visualization dashboard for Elasticsearch";
|
||||
}
|
||||
enum "klogin" {
|
||||
description "543/tcp — Kerberos remote login";
|
||||
}
|
||||
enum "kpasswd" {
|
||||
description "464/tcp+udp — Kerberos password changing protocol";
|
||||
}
|
||||
enum "kprop" {
|
||||
description "754/tcp — Kerberos database propagation";
|
||||
}
|
||||
enum "kshell" {
|
||||
description "544/tcp — Kerberos remote shell";
|
||||
}
|
||||
enum "ldap" {
|
||||
description "389/tcp — Lightweight Directory Access Protocol for directory services";
|
||||
}
|
||||
enum "ldaps" {
|
||||
description "636/tcp — Lightweight Directory Access Protocol over SSL";
|
||||
}
|
||||
enum "libvirt" {
|
||||
description "16509/tcp — Virtualization management daemon";
|
||||
}
|
||||
enum "libvirt-tls" {
|
||||
description "16514/tcp — Secure virtualization management over TLS";
|
||||
}
|
||||
enum "llmnr" {
|
||||
description "5355/tcp+udp — Link-Local Multicast Name Resolution";
|
||||
}
|
||||
enum "llmnr-client" {
|
||||
description "5355/udp — LLMNR client for name resolution";
|
||||
}
|
||||
enum "llmnr-tcp" {
|
||||
description "5355/tcp — LLMNR over TCP";
|
||||
}
|
||||
enum "llmnr-udp" {
|
||||
description "5355/udp — LLMNR over UDP";
|
||||
}
|
||||
enum "matrix" {
|
||||
description "8008/tcp — Matrix chat protocol server";
|
||||
}
|
||||
enum "mdns" {
|
||||
description "5353/udp — Multicast DNS for local network service discovery";
|
||||
}
|
||||
enum "mongodb" {
|
||||
description "27017/tcp — Document-oriented NoSQL database";
|
||||
}
|
||||
enum "minecraft" {
|
||||
description "25565/tcp — Minecraft game server";
|
||||
}
|
||||
enum "minidlna" {
|
||||
description "8200/tcp — Lightweight DLNA/UPnP media server";
|
||||
}
|
||||
enum "mndp" {
|
||||
description "5678/udp — MikroTik Neighbor Discovery Protocol";
|
||||
}
|
||||
enum "mosh" {
|
||||
description "60000-61000/udp — Mobile shell for remote terminal access";
|
||||
}
|
||||
enum "mpd" {
|
||||
description "6600/tcp — Music Player Daemon";
|
||||
}
|
||||
enum "mqtt" {
|
||||
description "1883/tcp — Message Queuing Telemetry Transport for IoT";
|
||||
}
|
||||
enum "mqtt-tls" {
|
||||
description "8883/tcp — Secure MQTT over TLS for IoT";
|
||||
}
|
||||
enum "mssql" {
|
||||
description "1433/tcp — Microsoft SQL Server database";
|
||||
}
|
||||
enum "mysql" {
|
||||
description "3306/tcp — MySQL database server connections";
|
||||
}
|
||||
enum "nbd" {
|
||||
description "10809/tcp — Network Block Device";
|
||||
}
|
||||
enum "netbios-ns" {
|
||||
description "137/udp — NetBIOS Name Service for Windows networking";
|
||||
}
|
||||
enum "netconf" {
|
||||
description "830/tcp — Network Configuration Protocol for network device management";
|
||||
}
|
||||
enum "nfs" {
|
||||
description "2049/tcp+udp — Network File System for distributed file sharing";
|
||||
}
|
||||
enum "nfs3" {
|
||||
description "2049/tcp+udp — Network File System version 3";
|
||||
}
|
||||
enum "nmea-0183" {
|
||||
description "10110/tcp — Marine electronics data interface";
|
||||
}
|
||||
enum "nrpe" {
|
||||
description "5666/tcp — Nagios Remote Plugin Executor";
|
||||
}
|
||||
enum "ntp" {
|
||||
description "123/udp — Network Time Protocol for time synchronization";
|
||||
}
|
||||
enum "openvpn" {
|
||||
description "1194/udp — OpenVPN secure tunnel for VPN connections";
|
||||
}
|
||||
enum "opentelemetry" {
|
||||
description "4317/tcp — Open source observability framework";
|
||||
}
|
||||
enum "pop3" {
|
||||
description "110/tcp — Post Office Protocol version 3 for email retrieval";
|
||||
}
|
||||
enum "pop3s" {
|
||||
description "995/tcp — Secure Post Office Protocol version 3 for encrypted email retrieval";
|
||||
}
|
||||
enum "plex" {
|
||||
description "32400/tcp — Plex media server";
|
||||
}
|
||||
enum "postgresql" {
|
||||
description "5432/tcp — PostgreSQL database server connections";
|
||||
}
|
||||
enum "prometheus" {
|
||||
description "9090/tcp — Monitoring system and time series database";
|
||||
}
|
||||
enum "proxy-dhcp" {
|
||||
description "4011/udp — Proxy DHCP for PXE boot";
|
||||
}
|
||||
enum "ptp" {
|
||||
description "319-320/udp — Precision Time Protocol";
|
||||
}
|
||||
enum "puppetmaster" {
|
||||
description "8140/tcp — Puppet configuration management server";
|
||||
}
|
||||
enum "radius" {
|
||||
description "1812-1813/tcp+udp — Remote Authentication Dial-in User Service";
|
||||
}
|
||||
enum "quassel" {
|
||||
description "4242/tcp — Quassel IRC client-server protocol";
|
||||
}
|
||||
enum "radsec" {
|
||||
description "2083/tcp — RADIUS over TLS";
|
||||
}
|
||||
enum "rdp" {
|
||||
description "3389/tcp — Remote Desktop Protocol for Windows remote access";
|
||||
}
|
||||
enum "redis" {
|
||||
description "6379/tcp — In-memory data structure store for database and cache";
|
||||
}
|
||||
enum "redis-sentinel" {
|
||||
description "26379/tcp — Redis Sentinel for high availability";
|
||||
}
|
||||
enum "restconf" {
|
||||
description "443/tcp — RESTful Network Configuration Protocol for HTTP-based network management";
|
||||
}
|
||||
enum "rsyncd" {
|
||||
description "873/tcp — Rsync daemon for centralized file synchronization";
|
||||
}
|
||||
enum "rtsp" {
|
||||
description "554/tcp — Real Time Streaming Protocol";
|
||||
}
|
||||
enum "samba" {
|
||||
description "445/tcp — Windows file and printer sharing";
|
||||
}
|
||||
enum "samba-client" {
|
||||
description "138/udp — Windows file and printer sharing (client-only)";
|
||||
}
|
||||
enum "samba-dc" {
|
||||
description "389/tcp — Samba Active Directory Domain Controller";
|
||||
}
|
||||
enum "sane" {
|
||||
description "6566/tcp — Scanner Access Now Easy network scanning";
|
||||
}
|
||||
enum "sip" {
|
||||
description "5060/tcp+udp — Session Initiation Protocol for VoIP communications";
|
||||
}
|
||||
enum "sips" {
|
||||
description "5061/tcp+udp — Secure Session Initiation Protocol for encrypted VoIP";
|
||||
}
|
||||
enum "smtp" {
|
||||
description "25/tcp — Simple Mail Transfer Protocol for email transmission";
|
||||
}
|
||||
enum "smtps" {
|
||||
description "465/tcp — Secure Simple Mail Transfer Protocol over SSL";
|
||||
}
|
||||
enum "slp" {
|
||||
description "427/tcp+udp — Service Location Protocol";
|
||||
}
|
||||
enum "snmp" {
|
||||
description "161/udp — Simple Network Management Protocol for network monitoring";
|
||||
}
|
||||
enum "snmptrap" {
|
||||
description "162/udp — Simple Network Management Protocol trap notifications";
|
||||
}
|
||||
enum "snmptls" {
|
||||
description "10161/tcp — SNMP over TLS";
|
||||
}
|
||||
enum "snmptls-trap" {
|
||||
description "10162/tcp — SNMP trap over TLS";
|
||||
}
|
||||
enum "spotify-sync" {
|
||||
description "57621/tcp — Spotify Connect synchronization";
|
||||
}
|
||||
enum "ssh" {
|
||||
description "22/tcp — Secure Shell for remote login and command execution";
|
||||
}
|
||||
enum "stun" {
|
||||
description "3478/tcp+udp — Session Traversal Utilities for NAT";
|
||||
}
|
||||
enum "stuns" {
|
||||
description "5349/tcp — Secure STUN over TLS";
|
||||
}
|
||||
enum "supertuxkart" {
|
||||
description "7321/tcp — SuperTuxKart racing game server";
|
||||
}
|
||||
enum "svn" {
|
||||
description "3690/tcp — Subversion version control system";
|
||||
}
|
||||
enum "syslog" {
|
||||
description "514/udp — System logging protocol for log message transmission";
|
||||
}
|
||||
enum "syslog-tls" {
|
||||
description "6514/tcp — Secure system logging protocol over TLS";
|
||||
}
|
||||
enum "syncthing" {
|
||||
description "22000/tcp — Continuous file synchronization";
|
||||
}
|
||||
enum "syncthing-gui" {
|
||||
description "8384/tcp — Syncthing web interface";
|
||||
}
|
||||
enum "syncthing-relay" {
|
||||
description "22067/tcp — Syncthing relay protocol";
|
||||
}
|
||||
enum "synergy" {
|
||||
description "24800/tcp — Keyboard and mouse sharing";
|
||||
}
|
||||
enum "ssdp" {
|
||||
description "1900/udp — Simple Service Discovery Protocol for UPnP device discovery";
|
||||
}
|
||||
enum "telnet" {
|
||||
description "23/tcp — Telnet protocol for remote terminal access";
|
||||
}
|
||||
enum "tftp" {
|
||||
description "69/udp — Trivial File Transfer Protocol for simple file transfers";
|
||||
}
|
||||
enum "turn" {
|
||||
description "3478/tcp+udp — Traversal Using Relay NAT for firewall traversal";
|
||||
}
|
||||
enum "turns" {
|
||||
description "5349/tcp — Secure TURN over TLS";
|
||||
}
|
||||
enum "vnc-server" {
|
||||
description "5900-5906/tcp — Virtual Network Computing server for remote desktop access";
|
||||
}
|
||||
enum "vrrp" {
|
||||
description "112/ip — Virtual Router Redundancy Protocol";
|
||||
}
|
||||
enum "warpinator" {
|
||||
description "42000/tcp — File sharing tool by Linux Mint";
|
||||
}
|
||||
enum "wireguard" {
|
||||
description "51820/udp — Modern VPN tunnel for secure networking";
|
||||
}
|
||||
enum "xdmcp" {
|
||||
description "177/tcp+udp — X Display Manager Control Protocol for remote X11 sessions";
|
||||
}
|
||||
enum "zerotier" {
|
||||
description "9993/udp — ZeroTier virtual network service";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
infix-firewall-services.yang
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
static int firewall_dbus_completion(const char *interface, const char *method, const char *parser)
|
||||
{
|
||||
return systemf("gdbus call --system --dest org.fedoraproject.FirewallD1 "
|
||||
"--object-path /org/fedoraproject/FirewallD1 "
|
||||
"--method org.fedoraproject.FirewallD1.%s.%s 2>/dev/null "
|
||||
"| %s", interface, method, parser);
|
||||
}
|
||||
|
||||
/*
|
||||
* Completion function for firewall zones.
|
||||
* D-Bus returns variant format: ({'zone1': {...}},)
|
||||
* Pipeline:
|
||||
* - sed removes wrapper parentheses
|
||||
* - tr converts single to double quotes
|
||||
* - jq extracts keys
|
||||
*/
|
||||
int infix_firewall_zones(kcontext_t *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
return firewall_dbus_completion("zone", "getActiveZones",
|
||||
"sed 's/^(//; s/,)$//' | sed 's/@as \\[\\]/[]/g' | tr \"'\" '\"' | jq -r 'keys[]' 2>/dev/null");
|
||||
}
|
||||
|
||||
/*
|
||||
* Completion function for firewall policies.
|
||||
* D-Bus returns variant format: (['policy1', 'policy2'],)
|
||||
* Pipeline:
|
||||
* - sed removes wrapper parentheses
|
||||
* - tr converts single to double quotes
|
||||
* - jq extracts array items
|
||||
*/
|
||||
int infix_firewall_policies(kcontext_t *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
return firewall_dbus_completion("policy", "getPolicies",
|
||||
"sed 's/^(//; s/,)$//' | tr \"'\" '\"' | jq -r '.[]' 2>/dev/null");
|
||||
}
|
||||
|
||||
/*
|
||||
* Completion function for firewall services.
|
||||
* D-Bus returns variant format: (['dhcp', 'dns', 'ssh'],)
|
||||
* Pipeline:
|
||||
* - sed removes wrapper parentheses
|
||||
* - tr converts single to double quotes
|
||||
* - jq extracts array items
|
||||
*/
|
||||
int infix_firewall_services(kcontext_t *ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
return systemf("gdbus call --system --dest org.fedoraproject.FirewallD1 "
|
||||
"--object-path /org/fedoraproject/FirewallD1 "
|
||||
"--method org.fedoraproject.FirewallD1.listServices 2>/dev/null "
|
||||
"| sed 's/^(//; s/,)$//' | tr \"'\" '\"' | jq -r '.[]' 2>/dev/null");
|
||||
}
|
||||
|
||||
int infix_copy(kcontext_t *ctx)
|
||||
{
|
||||
kpargv_t *pargv = kcontext_pargv(ctx);
|
||||
@@ -228,6 +283,9 @@ int kplugin_infix_init(kcontext_t *ctx)
|
||||
kplugin_add_syms(plugin, ksym_new("erase", infix_erase));
|
||||
kplugin_add_syms(plugin, ksym_new("files", infix_files));
|
||||
kplugin_add_syms(plugin, ksym_new("ifaces", infix_ifaces));
|
||||
kplugin_add_syms(plugin, ksym_new("firewall_zones", infix_firewall_zones));
|
||||
kplugin_add_syms(plugin, ksym_new("firewall_policies", infix_firewall_policies));
|
||||
kplugin_add_syms(plugin, ksym_new("firewall_services", infix_firewall_services));
|
||||
kplugin_add_syms(plugin, ksym_new("shell", infix_shell));
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -113,6 +113,27 @@
|
||||
<ACTION sym="STRING"/>
|
||||
</PTYPE>
|
||||
|
||||
<PTYPE name="FIREWALL_ZONES">
|
||||
<COMPL>
|
||||
<ACTION sym="firewall_zones@infix"/>
|
||||
</COMPL>
|
||||
<ACTION sym="STRING"/>
|
||||
</PTYPE>
|
||||
|
||||
<PTYPE name="FIREWALL_POLICIES">
|
||||
<COMPL>
|
||||
<ACTION sym="firewall_policies@infix"/>
|
||||
</COMPL>
|
||||
<ACTION sym="STRING"/>
|
||||
</PTYPE>
|
||||
|
||||
<PTYPE name="FIREWALL_SERVICES">
|
||||
<COMPL>
|
||||
<ACTION sym="firewall_services@infix"/>
|
||||
</COMPL>
|
||||
<ACTION sym="STRING"/>
|
||||
</PTYPE>
|
||||
|
||||
<VIEW name="main">
|
||||
<HOTKEY key="^D" cmd="exit"/>
|
||||
|
||||
@@ -490,6 +511,43 @@
|
||||
<COMMAND name="startup-config" help="Show startup-config">
|
||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">jq -C . /cfg/startup-config.cfg |pager</ACTION>
|
||||
</COMMAND>
|
||||
|
||||
<COMMAND name="firewall" help="Show firewall status and configuration">
|
||||
<SWITCH name="optional" min="0" max="1">
|
||||
<COMMAND name="log" help="Show firewall log (jumps to end), alias to 'show log firewall.log'">
|
||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
||||
doas -u $USER cat /log/firewall.log |pager +G
|
||||
</ACTION>
|
||||
</COMMAND>
|
||||
<COMMAND name="zone" help="Show firewall zones">
|
||||
<SWITCH name="optional" min="0">
|
||||
<PARAM name="name" ptype="/FIREWALL_ZONES" help="Zone name"/>
|
||||
</SWITCH>
|
||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
||||
sysrepocfg -X -d operational -x /infix-firewall:firewall -f json -t 60 | /usr/libexec/statd/cli-pretty show-firewall-zone "$KLISH_PARAM_name" |pager
|
||||
</ACTION>
|
||||
</COMMAND>
|
||||
<COMMAND name="policy" help="Show firewall policies">
|
||||
<SWITCH name="optional" min="0">
|
||||
<PARAM name="name" ptype="/FIREWALL_POLICIES" help="Policy name"/>
|
||||
</SWITCH>
|
||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
||||
sysrepocfg -X -d operational -x /infix-firewall:firewall -f json -t 60 | /usr/libexec/statd/cli-pretty show-firewall-policy "$KLISH_PARAM_name" |pager
|
||||
</ACTION>
|
||||
</COMMAND>
|
||||
<COMMAND name="service" help="Show firewall services">
|
||||
<SWITCH name="optional" min="0">
|
||||
<PARAM name="name" ptype="/FIREWALL_SERVICES" help="Service name"/>
|
||||
</SWITCH>
|
||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
||||
sysrepocfg -X -d operational -x /infix-firewall:firewall -f json -t 60 | /usr/libexec/statd/cli-pretty show-firewall-service "$KLISH_PARAM_name" |pager
|
||||
</ACTION>
|
||||
</COMMAND>
|
||||
</SWITCH>
|
||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
||||
sysrepocfg -X -d operational -x /infix-firewall:firewall -f json -t 60 | /usr/libexec/statd/cli-pretty show-firewall |pager
|
||||
</ACTION>
|
||||
</COMMAND>
|
||||
</COMMAND>
|
||||
|
||||
<COMMAND name="factory-reset" help="Restore the system to factory default state">
|
||||
@@ -499,6 +557,25 @@
|
||||
<ACTION sym="srp_rpc@sysrepo">/ietf-factory-default:factory-reset</ACTION>
|
||||
</COMMAND>
|
||||
|
||||
<COMMAND name="firewall" help="Control the firewall" mode="switch">
|
||||
<COMMAND name="lockdown" help="Emergency lockdown mode">
|
||||
<PARAM name="operation" ptype="/STRING" help="Lockdown commands.">
|
||||
<COMPL>
|
||||
<ACTION sym="printl">now</ACTION>
|
||||
<ACTION sym="printl">cancel</ACTION>
|
||||
</COMPL>
|
||||
</PARAM>
|
||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
||||
if [ "${KLISH_PARAM_operation}" = "now" ]; then
|
||||
if ! firewall panic status; then
|
||||
/bin/yorn -q "WARNING: This will block ALL network traffic and sever existing connections"
|
||||
fi
|
||||
fi
|
||||
</ACTION>
|
||||
<ACTION sym="srp_rpc@sysrepo">/infix-firewall:firewall/lockdown-mode</ACTION>
|
||||
</COMMAND>
|
||||
</COMMAND>
|
||||
|
||||
<COMMAND name="follow" help="Monitor a log file, use Ctrl-C to abort">
|
||||
<PARAM name="fn" ptype="/LOGFILES" help="Optional log file to monitor, default: syslog"/>
|
||||
<ACTION sym="script" in="tty" out="tty" interrupt="true">
|
||||
|
||||
@@ -149,28 +149,59 @@ bool srx_isset(sr_session_ctx_t *session, const char *fmt, ...)
|
||||
return isset;
|
||||
}
|
||||
|
||||
int srx_set_item(sr_session_ctx_t *session, const sr_val_t *val, sr_edit_options_t opts,
|
||||
const char *fmt, ...)
|
||||
static int set_vaitem(sr_session_ctx_t *session, const sr_val_t *val, sr_edit_options_t opts,
|
||||
const char *fmt, va_list ap)
|
||||
{
|
||||
va_list apdup;
|
||||
char *xpath;
|
||||
va_list ap;
|
||||
size_t len;
|
||||
|
||||
va_start(ap, fmt);
|
||||
len = vsnprintf(NULL, 0, fmt, ap) + 1;
|
||||
va_end(ap);
|
||||
va_copy(apdup, ap);
|
||||
len = vsnprintf(NULL, 0, fmt, apdup) + 1;
|
||||
va_end(apdup);
|
||||
|
||||
xpath = alloca(len);
|
||||
if (!xpath)
|
||||
return -1;
|
||||
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(xpath, len, fmt, ap);
|
||||
va_end(ap);
|
||||
va_copy(apdup, ap);
|
||||
vsnprintf(xpath, len, fmt, apdup);
|
||||
va_end(apdup);
|
||||
|
||||
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,
|
||||
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)));
|
||||
int srx_set_str(sr_session_ctx_t *, const char *, sr_edit_options_t, const char *fmt, ...)
|
||||
__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, ...)
|
||||
__attribute__ ((format (printf, 2, 3)));
|
||||
|
||||
@@ -78,6 +78,9 @@ def main():
|
||||
elif args.model == 'ieee802-dot1ab-lldp':
|
||||
from . import infix_lldp
|
||||
yang_data = infix_lldp.operational()
|
||||
elif args.model == 'infix-firewall':
|
||||
from . import infix_firewall
|
||||
yang_data = infix_firewall.operational()
|
||||
else:
|
||||
common.LOG.warning("Unsupported model %s", args.model)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -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_DHCP_SERVER_BASE "/infix-dhcp-server:dhcp-server"
|
||||
#define XPATH_LLDP_BASE "/ieee802-dot1ab-lldp:lldp"
|
||||
#define XPATH_FIREWALL_BASE "/infix-firewall:firewall"
|
||||
|
||||
TAILQ_HEAD(sub_head, sub);
|
||||
|
||||
@@ -356,6 +357,8 @@ static int subscribe_to_all(struct statd *statd)
|
||||
#endif
|
||||
if (subscribe(statd, "infix-dhcp-server", XPATH_DHCP_SERVER_BASE, sr_generic_cb))
|
||||
return SR_ERR_INTERNAL;
|
||||
if (subscribe(statd, "infix-firewall", XPATH_FIREWALL_BASE, sr_generic_cb))
|
||||
return SR_ERR_INTERNAL;
|
||||
|
||||
INFO("Successfully subscribed to all models");
|
||||
return SR_ERR_OK;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# shellcheck disable=SC2034,SC2154
|
||||
|
||||
# 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/..")
|
||||
logdir=$(readlink -f "$testdir/.log")
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
- name: "IETF Routing"
|
||||
suite: ietf_routing/all.yaml
|
||||
|
||||
- name: "Infix Firewall"
|
||||
suite: infix_firewall/all.yaml
|
||||
|
||||
- name: "Infix Containers"
|
||||
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
|
||||
USB locked
|
||||
USB2 locked
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
FROM alpine:3.18.0
|
||||
|
||||
# NOTE: please add packages alphabetically!
|
||||
RUN apk add --no-cache \
|
||||
busybox-extras \
|
||||
curl \
|
||||
e2fsprogs \
|
||||
e2tools \
|
||||
ethtool \
|
||||
fakeroot \
|
||||
gcc \
|
||||
git \
|
||||
@@ -13,7 +17,10 @@ RUN apk add --no-cache \
|
||||
libc-dev \
|
||||
libyang-dev \
|
||||
linux-headers \
|
||||
make \
|
||||
nmap \
|
||||
openssh-client \
|
||||
openssl \
|
||||
python3-dev \
|
||||
qemu-img \
|
||||
qemu-system-x86_64 \
|
||||
@@ -22,12 +29,7 @@ RUN apk add --no-cache \
|
||||
squashfs-tools \
|
||||
sshpass \
|
||||
tcpdump \
|
||||
tshark \
|
||||
openssl \
|
||||
curl \
|
||||
e2tools \
|
||||
make \
|
||||
ethtool
|
||||
tshark
|
||||
|
||||
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
|
||||
|
||||
@@ -4,8 +4,10 @@ from .container import Container
|
||||
from .env import Env
|
||||
from .env import ArgumentParser
|
||||
from .env import test_argument
|
||||
from .firewall import Firewall
|
||||
from .furl import Furl
|
||||
from .netns import IsolatedMacVlan,IsolatedMacVlans
|
||||
from .portscanner import PortScanner
|
||||
from .sniffer import Sniffer
|
||||
from .tap import Test
|
||||
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[]
|
||||
|
||||
<<<
|
||||
|
||||