Import firewall role from network repo

Move, actually.
This commit is contained in:
Timotej Lazar 2026-02-23 09:56:33 +01:00
parent 88061d97b2
commit 754c3da31f
21 changed files with 801 additions and 1 deletions

View file

@ -0,0 +1,50 @@
{% set fw = inventory_hostname.split('-')[1]|int -%}
Sync {
Mode FTFW {
# Add received rules immediately so we don’t need a
# signal on failover.
DisableExternalCache On
}
UDP {
Interface {{ iface_sync }}
IPv6_address fe80::{{ fw }}
IPv6_Destination_Address fe80::{{ 2 if fw == 1 else 1 }}
Port 3780
# Recommended by manual.
Checksum on
RcvSocketBuffer 1249280
SndSocketBuffer 1249280
}
#Options {
# TCPWindowTracking Off
#}
}
General {
UNIX {
Path /var/run/conntrackd.ctl
}
Syslog on
# Recommended by manual.
HashLimit 524288
NetlinkBufferSize 2097152
NetlinkBufferSizeMaxGrowth 8388608
Filter From Kernelspace {
# Don’t replicate rules for traffic from/to firewall.
Address Ignore {
IPv4_address 127.0.0.1/8
IPv6_address ::1/128
IPv6_address fe80::/64 # link-local addresses
IPv4_address {{ wg_ip }}
{% for address in interfaces | map(attribute='ip_addresses') | flatten | sort(attribute='address') %}
IPv{{ address.family.value }}_address {{ address.address }}
{% endfor %}
}
}
}

View file

@ -0,0 +1,16 @@
# disable IP forwarding on management interfaces
{% for iface in interfaces | map(attribute="name") | select("match", "^mgmt[0-9]+") %}
iface {{ iface }}
pre-up sysctl -w net.ipv4.conf.$IFACE.forwarding=0
pre-up sysctl -w net.ipv6.conf.$IFACE.forwarding=0
{% endfor -%}
# create VLANs 2 and 4 on firewal—exit links for inside and outside traffic
{% for iface in interfaces | map(attribute="name") | select("match", "^lan")
| product([2, 4])
| map("join", ".") %}
auto {{ iface }}
iface {{ iface }}
{% endfor %}

View file

@ -0,0 +1,141 @@
{% set addrs = interfaces | selectattr('name', '==', 'lo') |
map(attribute='ip_addresses') | first | selectattr('role') %}
{% set loopback = addrs | selectattr('role.value', '==', 'loopback') | map(attribute='address') -%}
frr defaults datacenter
service integrated-vtysh-config
log syslog
# Without this frr and kernel ECMP routes sometimes get desynced when a link is
# lost and found. Maybe related to https://github.com/FRRouting/frr/issues/12239.
zebra nexthop-group keep 1
router-id {{ loopback | ipv4 | first | ansible.utils.ipaddr('address') }}
bfd
profile fast
receive-interval 150
transmit-interval 150
# Default VRF has two connections to each exit, one for inside and one
# for outside networks. The efault route is received from the outside
# peers and distributed back to inside peers. Routes to office
# networks and NAT IPs are distributed to outside peers.
router bgp {{ asn.asn }}
# Allow multipathing through different ASs with equal path length.
bgp bestpath as-path multipath-relax
# NAT IPs are not on any interface so disable checking for it.
no bgp network import-check
{% for group in ['inside', 'outside'] %}
neighbor {{ group }} peer-group
neighbor {{ group }} remote-as external
neighbor {{ group }} capability extended-nexthop
{% endfor %}
{% for iface in interfaces | selectattr('name', 'match', '^lan') %}
neighbor {{ iface.name }}.2 interface peer-group inside
neighbor {{ iface.name }}.2 bfd profile fast
neighbor {{ iface.name }}.4 interface peer-group outside
neighbor {{ iface.name }}.4 bfd profile fast
{% endfor %}
{% for family in ['ipv4', 'ipv6'] %}
address-family {{ family }} unicast
{% if family == 'ipv4' %}
{% for network in nat %}
network {{ network }}
{% endfor %}
{% endif %}
redistribute connected route-map loopback
maximum-paths 16
neighbor outside activate
neighbor outside soft-reconfiguration inbound
neighbor outside route-map outside->default in
neighbor outside route-map default->outside out
neighbor inside activate
neighbor inside allowas-in origin
neighbor inside default-originate
neighbor inside soft-reconfiguration inbound
neighbor inside route-map inside->default in
neighbor inside route-map default->inside out
exit-address-family
{% endfor %}
# Prefix lists.
ip prefix-list default permit 0.0.0.0/0
ipv6 prefix-list default permit ::/0
ip prefix-list fabric permit 10.34.0.0/24 ge 32
{% for prefix in vrf_prefixes | rejectattr('vrf.name', '==', 'outside')
| sort(attribute='family.value') %}
{% if prefix.family.value == 4 %}
ip prefix-list office permit {{ prefix.prefix }}
{% elif prefix.family.value == 6 %}
ipv6 prefix-list office permit {{ prefix.prefix }}
{% endif %}
{% endfor %}
{% if wg_net is defined %}
ip prefix-list vpn permit {{ wg_net | ansible.utils.ipaddr('subnet') }}
{% endif %}
{% if wg_net6 is defined %}
ipv6 prefix-list vpn permit {{ wg_net6 | ansible.utils.ipaddr('subnet') }}
{% endif %}
{% for network in nat %}
ip prefix-list nat permit {{ network }}
{% endfor %}
{# TODO WG endpoint should probably be in a separate prefix-list. #}
ip prefix-list nat permit {{ wg_ip }}
route-map loopback permit 1
match interface lo
route-map loopback permit 2
match interface wg
# Get routes to offices from inside peers.
route-map inside->default permit 10
match ip address prefix-list fabric
route-map inside->default permit 20
match ip address prefix-list office
route-map inside->default permit 21
match ipv6 address prefix-list office
# Send default route and VPN network to inside peers.
route-map default->inside permit 1
match interface lo
route-map default->inside permit 20
match ip address prefix-list default
route-map default->inside permit 21
match ipv6 address prefix-list default
# I don’t think these /need/ to be announced separately since we are sending the default route anyway.
#route-map default->inside permit 30
# match ip address prefix-list vpn
#route-map default->inside permit 31
# match ipv6 address prefix-list vpn
# Get default route from outside peers.
route-map outside->default permit 10
match ip address prefix-list default
route-map outside->default permit 11
match ipv6 address prefix-list default
# Send inside and NAT addresses to outside peers so inbound packets go through the firewall.
route-map default->outside permit 1
match interface lo
route-map default->outside permit 20
match ip address prefix-list office
route-map default->outside permit 21
match ipv6 address prefix-list office
route-map default->outside permit 30
match ip address prefix-list nat
route-map default->outside permit 40
match ip address prefix-list vpn
route-map default->outside permit 41
match ipv6 address prefix-list vpn

View file

@ -0,0 +1,10 @@
{% set ifaces_fabric = interfaces | selectattr('name', 'match', '^lan') | map(attribute='name') %}
set inside {
type iface_index
elements = { {{ ifaces_fabric | product(['2']) | map('join', '.') | join(', ') }}, wg }
}
set outside {
type iface_index
elements = { {{ ifaces_fabric | product(['4']) | map('join', '.') | join(', ') }} }
}

View file

@ -0,0 +1,9 @@
{% for family, family_prefixes in vrf_prefixes | groupby('family.value') %}
{% for vlan, vlan_prefixes in family_prefixes | groupby('vlan.vid') %}
set {{ vlan_prefixes[0].vlan.name }}{% if family == 6 %}/6{% endif %} {
type ipv{{ family }}_addr; flags interval
elements = { {{ vlan_prefixes | map(attribute='prefix') | join(',') }} }
}
{% endfor %}
{% endfor %}

View file

@ -0,0 +1,190 @@
#!/usr/sbin/nft -f
{% set ifaces_fabric = interfaces | selectattr('name', 'match', '^lan') | map(attribute='name') %}
flush ruleset
table inet filter {
include "/etc/nftables.d/interfaces.nft"
include "/etc/nftables.d/networks.nft"
include "/etc/nftables.d/sets.nft*"
set link {
type iface_index
elements = { {{ ifaces_fabric | product(['2', '4']) | map('join', '.') | join(', ') }} }
}
# convenience port set definitions
set ad-ports { # https://learn.microsoft.com/en-us/troubleshoot/windows-server/active-directory/config-firewall-for-ad-domains-and-trusts
type inet_proto . inet_service
flags interval
elements = {
tcp . 53,
tcp . 88,
tcp . 135,
tcp . 139,
tcp . 389,
tcp . 445,
tcp . 464,
tcp . 636,
tcp . 3268-3269,
#tcp . 3389, # RDP
tcp . 5000-5100,
tcp . 5985,
tcp . 5986,
tcp . 9389,
tcp . 22222-22224,
tcp . 49152-65535,
udp . 53,
udp . 88,
udp . 135,
udp . 137, # netbios, maybe can do without
udp . 138, # netbios, maybe can do without
udp . 389,
udp . 464,
udp . 3269
}
}
set ldap-ports {
type inet_proto . inet_service
flags interval
elements = {
tcp . 88,
tcp . 389,
tcp . 636,
tcp . 3268,
tcp . 3269,
udp . 88,
udp . 389
}
}
chain input {
type filter hook input priority 0; policy drop
ct state vmap { established : accept, related : accept, invalid : drop } \
comment "Accept established streams and drop invalid connections"
iif lo accept \
comment "Accept any localhost traffic"
iif mgmt tcp dport ssh accept \
comment "Accept SSH from management VRF"
# allow SSH connections from firewall master’s IPs
{% for iface in hostvars[master].interfaces %}
{% for address in iface.ip_addresses %}
tcp dport ssh {{ 'ip' if address.family.value == 4 else 'ip6' }} saddr {{ address.address | ansible.utils.ipaddr('address') }} accept
{% for nat_address in address.nat_outside %}
tcp dport ssh ip saddr {{ nat_address.address | ansible.utils.ipaddr('address') }} accept
{% endfor %}
{% endfor %}
{% endfor %}
iif @link tcp dport bgp ip6 saddr fe80::/10 accept \
comment "Accept link-local BGP on fabric links"
iif @link udp dport 3784 ip6 saddr fe80::/10 accept \
comment "Accept link-local BFD on fabric links"
udp dport 51820 accept \
comment "Accept WireGuard from anywhere"
iif {{ iface_sync }} ip6 saddr fe80::/10 udp dport 3780 accept \
comment "Accept connection tracking sync data"
tcp dport auth reject with icmpx type port-unreachable \
comment "Reject AUTH to make it fail fast"
# ICMPv4
ip protocol icmp icmp type {
echo-request, echo-reply, destination-unreachable,
parameter-problem, time-exceeded,
} accept \
comment "Accept ICMP"
# ICMPv6
ip6 nexthdr icmpv6 icmpv6 type {
echo-request, echo-reply, destination-unreachable,
packet-too-big, parameter-problem, time-exceeded,
} accept \
comment "Accept basic IPv6 functionality"
ip6 nexthdr icmpv6 icmpv6 type {
nd-neighbor-solicit, nd-neighbor-advert,
nd-router-solicit, nd-router-advert,
} ip6 hoplimit 255 accept \
comment "Allow IPv6 neighbor discovery"
}
chain forward {
type filter hook forward priority filter; policy drop
ct state { established, related } accept \
comment "Forward all established and related traffic"
ct status dnat accept \
comment "Forward DNAT traffic for servers and suchlike"
ip protocol icmp icmp type {
echo-request, echo-reply, destination-unreachable,
parameter-problem, time-exceeded,
} accept \
comment "Accept ICMPv4"
ip6 nexthdr icmpv6 icmpv6 type {
echo-request, echo-reply, destination-unreachable,
packet-too-big, parameter-problem, time-exceeded,
} accept \
comment "Accept ICMPv6"
include "/etc/nftables.d/forward.nft*"
}
chain output {
type filter hook output priority 0; policy accept
}
}
table inet wireguard {
chain input {
type filter hook prerouting priority raw; policy accept
ip daddr 193.2.76.190 udp dport 51820 notrack \
comment "Disable connection tracking for wireguard"
}
chain output {
type route hook output priority raw; policy accept
meta mark 51820 meta nfproto ipv4 ip saddr set 193.2.76.190 notrack \
comment "Disable connection tracking and set anycast source IP for wireguard"
}
}
table ip nat {
include "/etc/nftables.d/interfaces.nft"
include "/etc/nftables.d/networks.nft"
include "/etc/nftables.d/sets.nft*"
include "/etc/nftables.d/netmap.nft*"
# Ensure these maps exist even if empty.
map netmap-in { type ipv4_addr : interval ipv4_addr; flags interval; }
map netmap-out { type ipv4_addr : interval ipv4_addr; flags interval; }
chain postrouting {
type nat hook postrouting priority srcnat
iif @inside oif @outside snat ip prefix to ip saddr map @netmap-out \
comment "Static source NAT for 1:1 mapped addresses"
include "/etc/nftables.d/nat.nft*"
}
chain prerouting {
type nat hook prerouting priority dstnat
dnat ip prefix to ip daddr map @netmap-in \
comment "Static destination NAT for 1:1 mapped addresses"
}
}
{# for compatibility with base alpine role #}
include "/etc/nftables.d/services.nft*"

View file

@ -0,0 +1,16 @@
# We are router.
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
# Zebra docs recommend these.
net.ipv6.conf.all.keep_addr_on_down = 1
net.ipv6.route.skip_notify_on_dev_down = 1
# Do not send ICMP redirects. Happens because firewall sees all office
# networks coming from the same routers, and gets confused as to why
# firewall is routing packets between them.
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
# Increase max connections for netfilter.
net.netfilter.nf_conntrack_max = 1048576

View file

@ -0,0 +1,12 @@
iface lo
address {{ wg_ip }}
auto wg
iface wg
use wireguard
{% if wg_net is defined %}
address {{ wg_net }}
{% endif %}
{% if wg_net6 is defined %}
address {{ wg_net6 }}
{% endif %}