initial commit

This commit is contained in:
2026-08-16 23:16:49 +00:00
commit 2dc83c0626
67 changed files with 1176 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
flowtable ft {
hook ingress priority filter
devices = { eth0, eth1 }
}
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
ct state invalid drop
iif lo accept
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
meta mark 0x00000001 accept
iifname eth0 tcp dport 22 accept
iifname eth0.11 tcp dport 22 accept
iifname eth1 udp dport 51820 accept
iifname { eth0, eth0.2, eth0.3, eth0.4, eth0.10, eth0.11, eth0.12 } udp dport 53 accept
iifname { eth0, eth0.2, eth0.3, eth0.4, eth0.10, eth0.11, eth0.12 } tcp dport 53 accept
iifname eth0.3 udp dport 67 accept
iifname eth1 udp dport 68 accept
#include "/etc/nftables.d/90-input.nft"
}
chain forward {
type filter hook forward priority filter; policy drop;
ct state established,related accept
ct state invalid drop
iifname { eth0, eth0.2, eth0.3, eth0.4, eth0.10, eth0.11, eth0.12 } oifname eth1 ct state new flow add @ft
iifname { eth0, eth0.2, eth0.3, eth0.4, eth0.10, eth0.11, eth0.12 } oifname eth1 accept
tcp flags syn tcp option maxseg size set rt mtu
include "/etc/nftables.d/90-forward.nft"
}
chain output {
type route hook output priority filter; policy accept;
#include "/etc/nftables.d/90-output.nft"
}
+9
View File
@@ -0,0 +1,9 @@
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oifname eth1 masquerade
}
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
include "/etc/nftables.d/90-dstnat.nft"
}
+35
View File
@@ -0,0 +1,35 @@
chain vpn_prerouting_dnat {
type nat hook prerouting priority dstnat - 5; policy accept;
iifname wg0 ip daddr 10.250.251.0/24 counter dnat ip prefix to 10.1.0.0/24
iifname wg0 ip daddr 10.250.252.0/24 counter dnat ip prefix to 10.2.0.0/24
iifname wg0 ip daddr 10.250.253.0/24 counter dnat ip prefix to 10.10.0.0/24
iifname wg0 ip daddr 10.250.254.0/24 counter dnat ip prefix to 10.11.0.0/24
iifname wg0 ip daddr 10.250.255.0/24 counter dnat ip prefix to 10.12.0.0/24
iifname wg0 ip daddr 10.250.249.0/24 counter dnat ip prefix to 10.13.0.0/24
}
chain vpn_postrouting_snat {
type nat hook postrouting priority srcnat; policy accept;
oifname wg0 ip saddr 10.1.0.0/24 counter snat ip prefix to 10.250.251.0/24
oifname wg0 ip saddr 10.2.0.0/24 counter snat ip prefix to 10.250.252.0/24
oifname wg0 ip saddr 10.10.0.0/24 counter snat ip prefix to 10.250.253.0/24
oifname wg0 ip saddr 10.11.0.0/24 counter snat ip prefix to 10.250.254.0/24
oifname wg0 ip saddr 10.12.0.0/24 counter snat ip prefix to 10.250.255.0/24
oifname wg0 ip saddr 10.13.0.0/24 counter snat ip prefix to 10.250.249.0/24
}
chain vpn_prerouting_pbr {
type filter hook prerouting priority mangle - 10; policy accept;
iifname wg0 ct state new counter ct mark set 0x000000c7
ip daddr 10.0.0.0/8 return
iifname != "wg0" ct mark 0x000000c7 counter mark set 0x000000c7
}
chain vpn_output_pbr {
type route hook output priority mangle - 10; policy accept;
ct mark 0x000000c7 counter meta mark set 0x000000c7
}
+15
View File
@@ -0,0 +1,15 @@
chain proxy_prerouting {
type filter hook prerouting priority filter - 50; policy accept;
fib daddr type local accept
include "/etc/nftables.d/90-proxy.nft"
}
chain proxy_output {
type route hook output priority mangle; policy accept;
#meta mark 0x000000ff return
#meta l4proto { tcp, udp } ip daddr @cloudflare_ip meta mark set 0x00000001 accept
}
+6
View File
@@ -0,0 +1,6 @@
set private_ip {
type ipv4_addr
flags interval
auto-merge
elements = { 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, 192.88.99.0/24, 192.168.0.0/16, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 224.0.0.0/4, 240.0.0.0/4 }
}
+15
View File
@@ -0,0 +1,15 @@
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
include "/etc/nftables.d/40-sets.nft"
include "/etc/nftables.d/90-sets.nft"
include "/etc/nftables.d/10-filter.nft"
include "/etc/nftables.d/20-vpn.nft"
include "/etc/nftables.d/30-proxy.nft"
}
table ip nat {
include "/etc/nftables.d/10-nat.nft"
}
+4
View File
@@ -0,0 +1,4 @@
---
- name: reload nftables
ansible.builtin.command: nft -f /etc/nftables.conf
listen: reload nftables
+41
View File
@@ -0,0 +1,41 @@
---
- name: ensure /etc/nftables.d exists
ansible.builtin.file:
path: /etc/nftables.d
state: directory
mode: "0755"
- name: deploy nftables rule
ansible.builtin.copy:
src: "{{ item }}"
dest: "/etc/nftables.d/{{ item }}"
mode: "0644"
loop:
- 10-filter.nft
- 10-nat.nft
- 20-vpn.nft
- 30-proxy.nft
- 40-sets.nft
notify: reload nftables
- name: render forward
ansible.builtin.template:
src: 90-forward.nft.j2
dest: /etc/nftables.d/90-forward.nft
mode: "0644"
notify: reload nftables
- name: render dstnat
ansible.builtin.template:
src: 90-dstnat.nft.j2
dest: /etc/nftables.d/90-dstnat.nft
mode: "0644"
notify: reload nftables
- name: deploy nftables.conf
ansible.builtin.copy:
src: nftables.conf
dest: /etc/nftables.conf
mode: "0644"
validate: "nft -c -f %s"
notify: reload nftables
+31
View File
@@ -0,0 +1,31 @@
#jinja2: trim_blocks: True, lstrip_blocks: True
{% macro render_dstnat_rule(ifaces, proto, port, target_ip, item_name) %}
{% set lines = [] %}
{% set active_ifaces = ifaces if (ifaces is iterable and ifaces is not string) else [ifaces] %}
{% for current_iface in active_ifaces %}
{% set comment_str = ' comment "' ~ current_iface ~ ' -> ' ~ item_name ~ '"' %}
{% set rule_line = 'iifname "' ~ current_iface ~ '" ' ~ proto ~ ' dport ' ~ port ~ ' counter dnat ip to ' ~ target_ip ~ ':' ~ port ~ comment_str %}
{% set _ = lines.append(rule_line) %}
{% endfor %}
{{ lines | join('\n') }}
{% endmacro %}
{% filter regex_replace('\n[ \t]*\n+', '\n') %}
{% for item in groups[nft_managed_group] | sort %}
{% set client = hostvars[item] %}
{% if 'nft_dst' in client and client.nft_dst is not none %}
{% set target_ip = client.container_ip | default(client.ansible_host | default(item)) %}
{% set raw_expose = client.nft_dst %}
{% set exposes = raw_expose if (raw_expose is iterable and raw_expose is not string and raw_expose is not mapping) else [raw_expose] %}
{% for expose in exposes %}
{% set protos = expose.proto if (expose.proto is defined and expose.proto is iterable and expose.proto is not string) else [expose.proto | default('tcp')] %}
{% set ports = expose.port if (expose.port is defined and expose.port is iterable and expose.port is not string) else [expose.port] %}
{% set ifaces = expose.iface %}
{% for p in protos | sort %}
{% for port in ports | sort %}
{{ render_dstnat_rule(ifaces, p, port, target_ip, item) }}
{% endfor %}
{% endfor %}
{% endfor %}
{% endif %}
{% endfor %}
{% endfilter %}
@@ -0,0 +1,95 @@
#jinja2: trim_blocks: True, lstrip_blocks: True
{% set ip_to_host = {} %}
{% for host in groups['all'] | default([]) %}
{% set hv = hostvars[host] | default({}) %}
{% if hv.ansible_host is defined and (hv.ansible_connection | default('')) != 'community.proxmox.proxmox_pct_remote' %}
{% set _ = ip_to_host.update({(hv.ansible_host | string): host}) %}
{% endif %}
{% if hv.container_ip is defined and hv.container_ip %}
{% set _ = ip_to_host.update({(hv.container_ip | string): host}) %}
{% endif %}
{% endfor %}
{% macro render_rule(service_name, iif, saddr, oif, daddr, protos, ports, dest_name) %}
{% set lines = [] %}
{% set iifs = iif if (iif is iterable and iif is not string) else [iif] %}
{% set oifs = oif if (oif is iterable and oif is not string) else [oif] %}
{% set active_protos = protos | sort if protos | length > 0 else [none] %}
{% set active_ports = ports if ports | length > 0 else [none] %}
{% for current_iif in iifs %}
{% for current_oif in oifs %}
{% for p in active_protos %}
{% for port in active_ports %}
{% set proto_rule = '' %}
{% if p and port %}
{% set proto_rule = p ~ ' dport ' ~ port %}
{% elif p %}
{% set proto_rule = 'meta l4proto ' ~ p %}
{% endif %}
{# Resolve source name: prefer an explicit host resolved via saddr, otherwise fall back
to the current interface for this specific line (not the whole iif list/service_name) #}
{% set resolved_service_name = service_name if service_name else current_iif %}
{% if saddr and ip_to_host[saddr | string] is defined %}
{% set resolved_service_name = ip_to_host[saddr | string] %}
{% endif %}
{# Resolve destination IP to inventory hostname only for comment #}
{% set resolved_dest_name = dest_name %}
{% if daddr and ip_to_host[daddr | string] is defined %}
{% set resolved_dest_name = ip_to_host[daddr | string] %}
{% endif %}
{% set comment_text = resolved_service_name ~ ' -> ' ~ resolved_dest_name %}
{% set comment_str = ' comment "' ~ comment_text ~ '"' %}
{% set parts = ['iifname "' ~ current_iif ~ '"'] %}
{% if saddr %}
{% set _ = parts.append('ip saddr ' ~ saddr) %}
{% endif %}
{% if current_oif %}
{% set _ = parts.append('oifname "' ~ current_oif ~ '"') %}
{% endif %}
{% if daddr %}
{% set _ = parts.append('ip daddr ' ~ daddr) %}
{% endif %}
{% if proto_rule %}
{% set _ = parts.append(proto_rule) %}
{% endif %}
{% set _ = parts.append('counter accept' ~ comment_str) %}
{% set _ = lines.append(parts | join(' ')) %}
{% endfor %}
{% endfor %}
{% endfor %}
{% endfor %}
{{ lines | join('\n') }}
{% endmacro %}
{% filter regex_replace('\n[ \t]*\n+', '\n') %}
{# === Managed Hosts Forward Rules === #}
{% for item in groups[nft_managed_group] | sort %}
{% set client = hostvars[item] %}
{% if client.nft_to is defined and client.nft_to is not none %}
{% set raw_rules = client.nft_to if (client.nft_to is iterable and client.nft_to is not string and client.nft_to is not mapping) else [client.nft_to] %}
{% for r in raw_rules %}
{% set rule_dict = r if (r is mapping) else {'to': r} %}
{% set raw_dests = rule_dict.to if (rule_dict.to is iterable and rule_dict.to is not string) else [rule_dict.to] %}
{% set protos = rule_dict.proto if (rule_dict.proto is defined and rule_dict.proto is iterable and rule_dict.proto is not string) else ([rule_dict.proto] if rule_dict.proto is defined else []) %}
{% set ports = rule_dict.port if (rule_dict.port is defined and rule_dict.port is iterable and rule_dict.port is not string) else ([rule_dict.port] if rule_dict.port is defined else []) %}
{% for dest in raw_dests %}
{% set dest_name = dest | regex_replace('^zone:', '') %}
{% if dest.startswith('zone:') %}
{{ render_rule(item, client.zone_iface, client.container_ip, dest.split(':')[1], none, protos, ports, dest_name) }}
{% else %}
{{ render_rule(item, client.zone_iface, client.container_ip, hostvars[dest].zone_iface, hostvars[dest].container_ip, protos, ports, dest_name) }}
{% endif %}
{% endfor %}
{% endfor %}
{% endif %}
{% endfor %}
{% for item in groups[nft_managed_group] | sort %}
{% set client = hostvars[item] %}
{% if client.nft_from is defined and client.nft_from is not none %}
{% set raw_from_rules = client.nft_from if (client.nft_from is iterable and client.nft_from is not string and client.nft_from is not mapping) else [client.nft_from] %}
{% for r in raw_from_rules %}
{% set protos = r.proto if (r.proto is defined and r.proto is iterable and r.proto is not string) else ([r.proto] if r.proto is defined else []) %}
{% set ports = r.port if (r.port is defined and r.port is iterable and r.port is not string) else ([r.port] if r.port is defined else []) %}
{{ render_rule(none, r.iface, none, client.zone_iface, client.container_ip, protos, ports, item) }}
{% endfor %}
{% endif %}
{% endfor %}
{% endfilter %}