From 271c29049841656127d755f5f506fb22ebdc0e86 Mon Sep 17 00:00:00 2001 From: pyrschtjag Date: Fri, 28 Aug 2026 11:03:56 +0000 Subject: [PATCH] change nftables role --- roles/nftables/files/10-filter.nft | 2 - roles/nftables/files/30-proxy.nft | 9 +- roles/nftables/handlers/main.yml | 4 +- roles/nftables/tasks/configure.yml | 41 +++++++ roles/nftables/tasks/install.yml | 5 + roles/nftables/tasks/main.yml | 43 +------- roles/nftables/templates/90-dstnat.nft.j2 | 32 ++---- roles/nftables/templates/90-forward.nft.j2 | 122 +++++++-------------- 8 files changed, 104 insertions(+), 154 deletions(-) create mode 100644 roles/nftables/tasks/configure.yml create mode 100644 roles/nftables/tasks/install.yml diff --git a/roles/nftables/files/10-filter.nft b/roles/nftables/files/10-filter.nft index 766eeaf..42cc23f 100644 --- a/roles/nftables/files/10-filter.nft +++ b/roles/nftables/files/10-filter.nft @@ -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 diff --git a/roles/nftables/files/30-proxy.nft b/roles/nftables/files/30-proxy.nft index 4aacf78..d3cf7dd 100644 --- a/roles/nftables/files/30-proxy.nft +++ b/roles/nftables/files/30-proxy.nft @@ -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" } diff --git a/roles/nftables/handlers/main.yml b/roles/nftables/handlers/main.yml index 62573de..b2116b8 100644 --- a/roles/nftables/handlers/main.yml +++ b/roles/nftables/handlers/main.yml @@ -1,4 +1,4 @@ --- -- name: reload nftables +- name: restart nftables ansible.builtin.command: nft -f /etc/nftables.conf - listen: reload nftables + listen: restart nftables diff --git a/roles/nftables/tasks/configure.yml b/roles/nftables/tasks/configure.yml new file mode 100644 index 0000000..b66b01c --- /dev/null +++ b/roles/nftables/tasks/configure.yml @@ -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 diff --git a/roles/nftables/tasks/install.yml b/roles/nftables/tasks/install.yml new file mode 100644 index 0000000..49d4014 --- /dev/null +++ b/roles/nftables/tasks/install.yml @@ -0,0 +1,5 @@ +--- +- name: install nftables + ansible.builtin.package: + name: nftables + state: present diff --git a/roles/nftables/tasks/main.yml b/roles/nftables/tasks/main.yml index 965647d..afead39 100644 --- a/roles/nftables/tasks/main.yml +++ b/roles/nftables/tasks/main.yml @@ -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 diff --git a/roles/nftables/templates/90-dstnat.nft.j2 b/roles/nftables/templates/90-dstnat.nft.j2 index 02916c5..04a50df 100644 --- a/roles/nftables/templates/90-dstnat.nft.j2 +++ b/roles/nftables/templates/90-dstnat.nft.j2 @@ -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 %} diff --git a/roles/nftables/templates/90-forward.nft.j2 b/roles/nftables/templates/90-forward.nft.j2 index fb1a47c..d4b4fef 100644 --- a/roles/nftables/templates/90-forward.nft.j2 +++ b/roles/nftables/templates/90-forward.nft.j2 @@ -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 %}