change nftables role

This commit is contained in:
2026-08-28 11:10:51 +00:00
parent abeb2e0eb9
commit 271c290498
8 changed files with 104 additions and 154 deletions
-2
View File
@@ -10,8 +10,6 @@ chain input {
ct state invalid drop
iif lo accept
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
meta mark 0x00000001 accept
+5 -4
View File
@@ -3,13 +3,14 @@ chain proxy_prerouting {
fib daddr type local accept
include "/etc/nftables.d/90-proxy.nft"
meta mark 0x00000001 iif "lo" meta l4proto { tcp, udp } tproxy ip to :61219 counter accept
include "/etc/nftables.d/90-proxy-prerouting.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
meta mark != 0 return
include "/etc/nftables.d/90-proxy-output.nft"
}
+2 -2
View File
@@ -1,4 +1,4 @@
---
- name: reload nftables
- name: restart nftables
ansible.builtin.command: nft -f /etc/nftables.conf
listen: reload nftables
listen: restart 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: restart nftables
- name: render forward
ansible.builtin.template:
src: 90-forward.nft.j2
dest: /etc/nftables.d/90-forward.nft
mode: "0644"
notify: restart nftables
- name: render dstnat
ansible.builtin.template:
src: 90-dstnat.nft.j2
dest: /etc/nftables.d/90-dstnat.nft
mode: "0644"
notify: restart nftables
- name: deploy nftables.conf
ansible.builtin.copy:
src: nftables.conf
dest: /etc/nftables.conf
mode: "0644"
validate: "nft -c -f %s"
notify: restart nftables
+5
View File
@@ -0,0 +1,5 @@
---
- name: install nftables
ansible.builtin.package:
name: nftables
state: present
+4 -39
View File
@@ -1,41 +1,6 @@
---
- name: ensure /etc/nftables.d exists
ansible.builtin.file:
path: /etc/nftables.d
state: directory
mode: "0755"
- name: include nftables install
ansible.builtin.include_tasks: install.yml
- 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
- name: include nftables configurure
ansible.builtin.include_tasks: configure.yml
+10 -22
View File
@@ -1,31 +1,19 @@
#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) }}
{% set target_ip = client.container_ip %}
{% for client in client.nft_dst %}
{% set ports = client.port if (client.port is iterable and client.port is not string) else [client.port] %}
{% set protos = client.proto if (client.proto is iterable and client.proto is not string) else [client.proto] %}
{% set ifaces = client.iface if (client.iface is iterable and client.iface is not string) else [client.iface] %}
{% for proto in protos %}
{% for port in ports %}
{% for iface in ifaces %}
iifname "{{ iface }}" {{ proto }} dport {{ port }} counter dnat ip to {{ target_ip }}:{{ port }} comment "{{ iface }} -> {{ item }}"
{% endfor %}
{% endfor %}
{% endfor %}
{% endfor %}
{% endif %}
{% endfor %}
{% endfilter %}
+37 -85
View File
@@ -1,95 +1,47 @@
#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(' ')) %}
{% for item in groups[nft_managed_group] | sort %}
{% set client = hostvars[item] %}
{% if 'nft_to' in client and client.nft_to is not none %}
{% set rules = client.nft_to if (client.nft_to is iterable and client.nft_to is not string) else [client.nft_to] %}
{% for rule in rules %}
{% set rule = rule if rule is mapping else {'to': rule} %}
{% set dests = rule.to if (rule.to is iterable and rule.to is not string) else [rule.to] %}
{% set protos = rule.proto if (rule.proto is iterable and rule.proto is not string) else [rule.proto | default(none)] %}
{% set ports = rule.port if (rule.port is iterable and rule.port is not string) else [rule.port | default(none)] %}
{% for dest in dests %}
{% if dest.startswith('zone:') %}
{% set oif = dest.split(':')[1] %}
{% set daddr = none %}
{% set dest_name = oif %}
{% else %}
{% set oif = hostvars[dest].zone_iface %}
{% set daddr = hostvars[dest].container_ip %}
{% set dest_name = dest %}
{% endif %}
{% for proto in protos %}
{% for port in ports %}
iifname "{{ client.zone_iface }}" ip saddr {{ client.container_ip }} oifname "{{ oif }}" {% if daddr %}ip daddr {{ daddr }} {% endif %}{% if proto and port %}{{ proto }} dport {{ port }} {% endif %}counter accept comment "{{ item }} -> {{ dest_name }}"
{% endfor %}
{% endfor %}
{% endfor %}
{% endfor %}
{% endfor %}
{{ lines | join('\n') }}
{% endmacro %}
{% filter regex_replace('\n[ \t]*\n+', '\n') %}
{# === Managed Hosts Forward Rules === #}
{% endif %}
{% endfor %}
{% 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 %}
{% if 'nft_from' in client and client.nft_from is not none %}
{% set rules = client.nft_from if (client.nft_from is iterable and client.nft_from is not string) else [client.nft_from] %}
{% for rule in rules %}
{% set ifaces = rule.iface if (rule.iface is iterable and rule.iface is not string) else [rule.iface] %}
{% set protos = rule.proto if (rule.proto is iterable and rule.proto is not string) else [rule.proto | default(none)] %}
{% set ports = rule.port if (rule.port is iterable and rule.port is not string) else [rule.port | default(none)] %}
{% for iface in ifaces %}
{% for proto in protos %}
{% for port in ports %}
iifname "{{ iface }}" oifname "{{ client.zone_iface }}" ip daddr {{ client.container_ip }} {% if proto and port %}{{ proto }} dport {{ port }} {% endif %}counter accept comment "{{ iface }} -> {{ item }}"
{% endfor %}
{% endfor %}
{% 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 %}