From ae9be6373428471dd4c95f64741b990638c272c0 Mon Sep 17 00:00:00 2001 From: Jan Tuomi Date: Mon, 6 Oct 2025 00:06:54 +0300 Subject: Work on networking --- playbook.yml | 4 +- tasks/jail_ingress.yml | 53 +++++++++++++++++----- tasks/jail_single.yml | 3 ++ tasks/network.yml | 17 ------- templates/etc_jail.conf.j2 | 8 +++- templates/etc_resolv.conf.j2 | 1 - templates/usr_local_bin_hetzner_ddns.sh.j2 | 66 ++++++++++++++++++++++++++++ templates/usr_local_etc_do_dyndns_auth.j2 | 1 - templates/usr_local_etc_hetzner_auth.j2 | 1 + templates/usr_local_etc_nginx_nginx.conf.j2 | 13 ++++-- templates/usr_local_etc_rc.d_hetzner_ddns.j2 | 48 ++++++++++++++++++++ 11 files changed, 178 insertions(+), 37 deletions(-) create mode 100644 templates/usr_local_bin_hetzner_ddns.sh.j2 delete mode 100644 templates/usr_local_etc_do_dyndns_auth.j2 create mode 100644 templates/usr_local_etc_hetzner_auth.j2 create mode 100644 templates/usr_local_etc_rc.d_hetzner_ddns.j2 diff --git a/playbook.yml b/playbook.yml index d8de148..fd9d2ad 100644 --- a/playbook.yml +++ b/playbook.yml @@ -19,7 +19,9 @@ smtp_port: "{{ lookup('env', 'SMTP_PORT') }}" dma_mail_hostname: "{{ lookup('env', 'DMA_MAILNAME') }}" dma_to_address: "{{ lookup('env', 'DMA_TO_ADDRESS') }}" - do_api_token: "{{ lookup('env', 'DO_API_TOKEN') }}" + hetzner_zone_id: "{{ lookup('env', 'HETZNER_ZONE_ID') }}" + hetzner_record_id: "{{ lookup('env', 'HETZNER_RECORD_ID') }}" + hetzner_pat: "{{ lookup('env', 'HETZNER_PAT') }}" backup_ssh_host: "{{ lookup('env', 'BACKUP_SSH_HOST') }}" backup_ssh_user: "{{ lookup('env', 'BACKUP_SSH_USER') }}" backup_ssh_privkey_file: "{{ lookup('env', 'BACKUP_SSH_PRIVKEY_FILE') }}" diff --git a/tasks/jail_ingress.yml b/tasks/jail_ingress.yml index 1a87722..d6db2f5 100644 --- a/tasks/jail_ingress.yml +++ b/tasks/jail_ingress.yml @@ -2,17 +2,9 @@ loop: - { jail: ingress, package: nginx } - { jail: ingress, package: py311-certbot } + - { jail: ingress, package: py311-certbot-nginx } include_tasks: pkg_jail_install.yml -- name: Configure nginx.conf - template: - src: usr_local_etc_nginx_nginx.conf.j2 - dest: /usr/local/jails/containers/ingress/usr/local/etc/nginx/nginx.conf - owner: root - group: wheel - mode: "0644" - register: nginx_conf - - name: Create static sites log dir file: path: /usr/local/jails/containers/ingress/var/log/nginx/{{ item }} @@ -41,9 +33,19 @@ loop: "{{ static_sites | map(attribute='site') | unique }}" #- name: Generate LetsEncrypt certs -# shell: jexec ingress certbot certonly --standalone -n -m jan@jantuomi.fi --agree-tos -d {{ item }} +# shell: jexec ingress certbot certonly --nginx -n -m jan@jantuomi.fi --agree-tos -d {{ item }} # loop: "{{ static_sites | selectattr('tls') | map(attribute='host') }}" -# when: nginx_conf.changed + +# It's important to run this after generating certs, because nginx.conf refers to files +# generated by certbot. Certbot will fail validation if nginx.conf is configured too early. +- name: Configure nginx.conf + template: + src: usr_local_etc_nginx_nginx.conf.j2 + dest: /usr/local/jails/containers/ingress/usr/local/etc/nginx/nginx.conf + owner: root + group: wheel + mode: "0644" + register: nginx_conf - name: Copy over static HTML ansible.posix.synchronize: @@ -62,3 +64,32 @@ service -j ingress nginx enable service -j ingress nginx onestart when: ingress_nginx_enabled.rc != 0 + +- name: Configure hetzner-ddns + template: + src: usr_local_etc_rc.d_hetzner_ddns.j2 + dest: /usr/local/jails/containers/ingress/usr/local/etc/rc.d/hetzner_ddns + owner: root + group: wheel + mode: "0755" + +- name: Install hetzner-ddns script + template: + src: usr_local_bin_hetzner_ddns.sh.j2 + dest: /usr/local/jails/containers/ingress/usr/local/bin/hetzner_ddns.sh + owner: root + group: wheel + mode: "0755" + +- name: Set up hetzner_auth + template: + src: usr_local_etc_hetzner_auth.j2 + dest: /usr/local/jails/containers/ingress/usr/local/etc/hetzner_auth + owner: root + group: wheel + mode: "0600" + +- name: Restart hetzner-ddns + shell: | + service -j ingress hetzner_ddns enable + service -j ingress hetzner_ddns restart diff --git a/tasks/jail_single.yml b/tasks/jail_single.yml index 9c5d1ee..9d539d0 100644 --- a/tasks/jail_single.yml +++ b/tasks/jail_single.yml @@ -8,6 +8,9 @@ shell: zfs clone zroot/jails/templates/{{ jail_userland_version }}@base "zroot/jails/containers/{{ jail.name }}" when: check_jail_directory.rc != 0 +- name: Make resolv.conf immutable + shell: chflags schg /usr/local/jails/containers/{{ jail.name }}/etc/resolv.conf + - name: "Check if jail {{ jail.name }} is running" shell: jls -j "{{ jail.name }}" failed_when: false diff --git a/tasks/network.yml b/tasks/network.yml index 15c4494..bf22044 100644 --- a/tasks/network.yml +++ b/tasks/network.yml @@ -66,20 +66,3 @@ - name: Reload pf rules shell: pfctl -f /etc/pf.conf when: pf_conf.changed - -- name: Set up /usr/local/etc/do_dyndns_auth - template: - src: usr_local_etc_do_dyndns_auth.j2 - dest: /usr/local/etc/do_dyndns_auth - owner: root - group: wheel - mode: "0600" - -- name: Set up /usr/local/bin/do_dyndns.sh - template: - src: usr_local_bin_do_dyndns.sh.j2 - dest: /usr/local/bin/do_dyndns.sh - owner: root - group: wheel - mode: "0755" - register: do_dyndns_sh diff --git a/templates/etc_jail.conf.j2 b/templates/etc_jail.conf.j2 index 708e5c1..0b8c18b 100644 --- a/templates/etc_jail.conf.j2 +++ b/templates/etc_jail.conf.j2 @@ -12,7 +12,7 @@ exec.prestart += "ifconfig ${epl}a up"; exec.prestart += "ifconfig brlan0 addm ${epl}a"; exec.clean; exec.start = "/bin/sh /etc/rc"; -exec.poststart = "jexec ${name} ifconfig ${epl}b 192.168.2.${num}/16"; +exec.poststart = "jexec ${name} ifconfig ${epl}b 192.168.2.${num}/16 up"; exec.poststart += "jexec ${name} route add default 192.168.0.1 || echo 'Failed to add default route'"; exec.stop = "/bin/sh /etc/rc.shutdown"; exec.poststop += "ifconfig ${epl}b -vnet $name"; # workaround to bug 238326: move ${epl}b from the jail to the host when stopping jail services @@ -32,6 +32,10 @@ path = "/usr/local/jails/containers/${name}"; {% for jail in jails %} {{ jail.name }} { $num = "{{ jail.num }}"; + {% if jail.name == "ingress" %} + exec.poststart = "jexec ${name} ifconfig ${epl}b 192.168.2.${num}/16 up"; + exec.poststart = "jexec ${name} ifconfig epw1b up"; + exec.poststart += "jexec ${name} service dhclient restart epw1b"; + {% endif %} } - {% endfor %} diff --git a/templates/etc_resolv.conf.j2 b/templates/etc_resolv.conf.j2 index 29f6bf6..9232f25 100644 --- a/templates/etc_resolv.conf.j2 +++ b/templates/etc_resolv.conf.j2 @@ -1,3 +1,2 @@ search {{ lan_search_domain }} nameserver {{ lan_ipv4_gateway }} -nameserver 8.8.8.8 diff --git a/templates/usr_local_bin_hetzner_ddns.sh.j2 b/templates/usr_local_bin_hetzner_ddns.sh.j2 new file mode 100644 index 0000000..aee1016 --- /dev/null +++ b/templates/usr_local_bin_hetzner_ddns.sh.j2 @@ -0,0 +1,66 @@ +#!/bin/sh +# Minimal Hetzner DNS updater for pfSense/FreeBSD +# - Always updates on startup (no pre-check) +# - Then polls and only updates when IP changes +# - Keeps last IP only in memory (no files) + +# --- CONFIG --------------------------------------------------------------- +API_TOKEN="$(cat /usr/local/etc/hetzner_auth)" +ZONE_ID="{{ hetzner_zone_id }}" +RECORD_ID="{{ hetzner_record_id }}" +RECORD_NAME=pursotin +RECORD_TYPE="A" +IFACE_CMD="ifconfig epw1b" + +POLL_INTERVAL=60 +API_BASE="https://dns.hetzner.com/api/v1" +TTL="300" +# -------------------------------------------------------------------------- + +get_ip() { + # Expect FreeBSD-style ifconfig output; grab first IPv4 addr + # Example: 'inet 192.0.2.3 ...' + sh -c "$IFACE_CMD" 2>/dev/null | awk '/inet[[:space:]]/ {print $2; exit}' +} + +update_record() { + ip="$1" + body=$(printf '{"type":"%s","name":"%s","value":"%s","zone_id":"%s","ttl":%s}' \ + "$RECORD_TYPE" "$RECORD_NAME" "$ip" "$ZONE_ID" "$TTL") + + http_code=$( + curl -sS -o /dev/null -w "%{http_code}" -X PUT \ + -H "Content-Type: application/json" \ + -H "Auth-API-Token: ${API_TOKEN}" \ + --data "$body" \ + "${API_BASE}/records/${RECORD_ID}" + ) + + [ "$http_code" = "200" ] || { + echo "$(date -u +"%F %T") update failed (HTTP $http_code)" >&2 + return 1 + } + + echo "$(date -u +"%F %T") updated ${RECORD_NAME} to ${ip}" + return 0 +} + +# --- Startup: always update once (no check) -------------------------------- +last_ip="" +ip="$(get_ip)" +if [ -n "$ip" ]; then + update_record "$ip" && last_ip="$ip" +else + echo "$(date -u +"%F %T") no IPv4 from: ${IFACE_CMD}; will retry..." >&2 +fi + +# --- Poll loop: update only on change -------------------------------------- +while :; do + ip="$(get_ip)" + if [ -n "$ip" ] && [ "$ip" != "$last_ip" ]; then + if update_record "$ip"; then + last_ip="$ip" + fi + fi + sleep "$POLL_INTERVAL" +done diff --git a/templates/usr_local_etc_do_dyndns_auth.j2 b/templates/usr_local_etc_do_dyndns_auth.j2 deleted file mode 100644 index 2a9cbc5..0000000 --- a/templates/usr_local_etc_do_dyndns_auth.j2 +++ /dev/null @@ -1 +0,0 @@ -{{ do_api_token }} diff --git a/templates/usr_local_etc_hetzner_auth.j2 b/templates/usr_local_etc_hetzner_auth.j2 new file mode 100644 index 0000000..129dccf --- /dev/null +++ b/templates/usr_local_etc_hetzner_auth.j2 @@ -0,0 +1 @@ +{{ hetzner_pat }} diff --git a/templates/usr_local_etc_nginx_nginx.conf.j2 b/templates/usr_local_etc_nginx_nginx.conf.j2 index 55c55df..cee7878 100644 --- a/templates/usr_local_etc_nginx_nginx.conf.j2 +++ b/templates/usr_local_etc_nginx_nginx.conf.j2 @@ -22,17 +22,23 @@ http { {% for site in static_sites -%} {% if site.tls -%} - {#server { - listen 443 ssl; + server { server_name {{ site.host }}; - root /usr/local/www/{{ site.site }}; + + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + index index.html; location / { try_files $uri $uri.html $uri/ =404; } + # See https://ssl-config.mozilla.org/#server=nginx&version=1.28.0&config=intermediate&openssl=3.4.0&guideline=5.7 + add_header Strict-Transport-Security "max-age=63072000" always; + access_log /var/log/nginx/{{ site.site }}/access.log; error_log /var/log/nginx/{{ site.site }}/error.log; @@ -41,7 +47,6 @@ http { include /usr/local/etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /usr/local/etc/letsencrypt/ssl-dhparams.pem; } - #} {% endif -%} server { diff --git a/templates/usr_local_etc_rc.d_hetzner_ddns.j2 b/templates/usr_local_etc_rc.d_hetzner_ddns.j2 new file mode 100644 index 0000000..ccbbb8c --- /dev/null +++ b/templates/usr_local_etc_rc.d_hetzner_ddns.j2 @@ -0,0 +1,48 @@ +#!/bin/sh +# +# PROVIDE: hetzner_ddns +# REQUIRE: NETWORKING +# KEYWORD: shutdown + +# Enable via: +# hetzner_ddns_enable="YES" +# +# (pfSense: System -> Advanced -> System Tunables or /etc/rc.conf) + +. /etc/rc.subr + +name="hetzner_ddns" +rcvar=hetzner_ddns_enable + +load_rc_config $name + +: ${hetzner_ddns_enable:="NO"} +: ${hetzner_ddns_user:="root"} +: ${hetzner_ddns_command:="/usr/local/bin/hetzner-ddns.sh"} + +start_cmd="${name}_start" +stop_cmd="${name}_stop" +status_cmd="${name}_status" + +hetzner_ddns_start() { + echo "Starting ${name}..." + /usr/sbin/daemon -t "${name}" -u "${hetzner_ddns_user}" ${hetzner_ddns_command} +} + +hetzner_ddns_stop() { + echo "Stopping ${name}..." + # First try by daemon title, then by command as fallback + pkill -f "^daemon: ${name}$" || pkill -f "${hetzner_ddns_command}" +} + +hetzner_ddns_status() { + if pgrep -f "^daemon: ${name}$" >/dev/null 2>&1 || pgrep -f "${hetzner_ddns_command}" >/dev/null 2>&1; then + echo "${name} is running" + return 0 + else + echo "${name} is not running" + return 1 + fi +} + +run_rc_command "$1" -- cgit v1.3