Files
deploy_home/ansible/roles/podman/tasks/containers/home/zomboid.yml
T
Bastian de BylandClaude Opus 5 1c467b1a76 fix: stop the world wipe taking the world settings with it
A wipe was quietly turning a Sophie world into an Apocalypse one. 139 sandbox
values had reverted -- loot rates from 0.35 back to 0.9, ranged weapons and ammo
to 2.0, CharacterFreePoints from 0 to 60 -- and ten of Sophie's mod-added
options had vanished entirely.

The reset script was not deleting the settings. PZ reads
Server/<name>_SandboxVars.lua only when it creates a world, and then writes the
running world's settings back over that same file. The file is an output, not an
input. So once any world came up with defaults, the server stamped those defaults
into the file, and every wipe afterwards regenerated from them -- inheriting the
corruption rather than causing it, and with no way back out on its own. The same
shape as the admin-password deadlock.

The intended settings now live in config-template/, which the server has no
reason to touch, and a wipe restores Server/<name>_{SandboxVars,spawnregions,
spawnpoints}.lua from there before restarting. That is also the file to hand-edit
when changing the world: edit the template, wipe, and the new world has the edits.

Verified by wiping: 139 differences before, 0 after. The server still rewrites
the live file on boot -- 996 keys become 1061 as it adds newer options -- but
every Sophie value survives that now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 22:55:06 -04:00

724 lines
26 KiB
YAML

---
- name: load vendored sophie modlist
ansible.builtin.include_vars:
file: zomboid_sophie_mods.yml
tags: zomboid-conf
- name: create zomboid host directory volumes
become: true
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ podman_subuid.stdout }}"
group: "{{ podman_user }}"
mode: 0755
notify: restorecon podman
loop:
- "{{ zomboid_path }}/server"
- "{{ zomboid_path }}/data"
- "{{ zomboid_path }}/scripts"
# Not a container volume -- nothing mounts it. It holds output from host-side
# helpers that run as {{ podman_user }}, so it must be owned by that user rather
# than the container's subuid. Getting this wrong silently broke world resets:
# the script's first log line failed with EACCES and set -e killed it before it
# stopped the server, so `@bot` resets did nothing at all.
- name: create zomboid canonical config directory
become: true
ansible.builtin.file:
path: "{{ zomboid_path }}/config-template"
state: directory
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: 0755
- name: create zomboid host-side log directory
become: true
ansible.builtin.file:
path: "{{ zomboid_path }}/logs"
state: directory
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: 0755
- name: create podman bin directory
become: true
ansible.builtin.file:
path: "{{ podman_home }}/bin"
state: directory
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: '0755'
- name: deploy zomboid world reset script
become: true
ansible.builtin.template:
src: zomboid/world-reset.sh.j2
dest: "{{ podman_home }}/bin/zomboid-world-reset.sh"
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: '0755'
- name: deploy zomboid world reset path unit
become: true
ansible.builtin.template:
src: zomboid/zomboid-world-reset.path.j2
dest: "{{ podman_home }}/.config/systemd/user/zomboid-world-reset.path"
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: '0644'
notify: reload zomboid systemd
- name: deploy zomboid world reset service unit
become: true
ansible.builtin.template:
src: zomboid/zomboid-world-reset.service.j2
dest: "{{ podman_home }}/.config/systemd/user/zomboid-world-reset.service"
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: '0644'
notify: reload zomboid systemd
- name: deploy zomboid stats script
become: true
ansible.builtin.template:
src: zomboid/zomboid-stats.sh.j2
dest: "{{ podman_home }}/bin/zomboid-stats.sh"
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: '0755'
- name: create zomboid stats file with correct permissions
become: true
ansible.builtin.file:
path: "{{ podman_volumes }}/zomboid-stats.json"
state: touch
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: '0644'
modification_time: preserve
access_time: preserve
- name: deploy zomboid stats service unit
become: true
ansible.builtin.template:
src: zomboid/zomboid-stats.service.j2
dest: "{{ podman_home }}/.config/systemd/user/zomboid-stats.service"
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: '0644'
notify: reload zomboid systemd
- name: deploy zomboid stats timer unit
become: true
ansible.builtin.template:
src: zomboid/zomboid-stats.timer.j2
dest: "{{ podman_home }}/.config/systemd/user/zomboid-stats.timer"
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: '0644'
notify: reload zomboid systemd
- name: enable zomboid stats timer
become: true
become_user: "{{ podman_user }}"
ansible.builtin.systemd:
name: zomboid-stats.timer
scope: user
enabled: true
state: started
daemon_reload: true
- name: copy zomboid entrypoint script
become: true
ansible.builtin.template:
src: zomboid/entrypoint.sh.j2
dest: "{{ zomboid_path }}/scripts/entrypoint.sh"
owner: "{{ podman_subuid.stdout }}"
group: "{{ podman_user }}"
mode: 0755
notify: restorecon podman
- name: copy zomboid steamcmd install script
become: true
ansible.builtin.template:
src: zomboid/install.scmd.j2
dest: "{{ zomboid_path }}/scripts/install.scmd"
owner: "{{ podman_subuid.stdout }}"
group: "{{ podman_user }}"
mode: 0644
notify: restorecon podman
# Server config is seeded from the vendored Sophie 42 preset *before* first
# boot, so the server never generates a vanilla INI we then have to patch.
#
# force is off by design: these files are a starting point, not a managed
# state. Stop the server, hand-edit them, regenerate the world -- Ansible will
# not clobber the edits on the next deploy. Re-push deliberately with
# -e zomboid_config_force=true.
- name: create zomboid server config directory
become: true
ansible.builtin.file:
path: "{{ zomboid_path }}/data/Server"
state: directory
owner: "{{ podman_subuid.stdout }}"
group: "{{ podman_user }}"
mode: 0755
notify: restorecon podman
tags: zomboid-conf
- name: seed zomboid server ini from sophie preset
become: true
ansible.builtin.template:
src: zomboid/server.ini.j2
dest: "{{ zomboid_path }}/data/Server/{{ zomboid_server_name }}.ini"
force: "{{ zomboid_config_force | bool }}"
owner: "{{ podman_subuid.stdout }}"
group: "{{ podman_user }}"
mode: 0644
notify: restorecon podman
tags: zomboid-conf
# Copied, not templated: these are Lua and must not go through Jinja.
- name: seed zomboid sandbox and spawn config from sophie preset
become: true
ansible.builtin.copy:
src: "zomboid/sophie/{{ item }}.lua"
dest: "{{ zomboid_path }}/data/Server/{{ zomboid_server_name }}_{{ item }}.lua"
force: "{{ zomboid_config_force | bool }}"
owner: "{{ podman_subuid.stdout }}"
group: "{{ podman_user }}"
mode: 0644
loop:
- SandboxVars
- spawnregions
- spawnpoints
notify: restorecon podman
tags: zomboid-conf
- name: deploy zomboid log prune script
become: true
ansible.builtin.template:
src: zomboid/zomboid-log-prune.sh.j2
dest: "{{ podman_home }}/bin/zomboid-log-prune.sh"
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: '0755'
- name: deploy zomboid log prune units
become: true
ansible.builtin.template:
src: "zomboid/zomboid-log-prune.{{ item }}.j2"
dest: "{{ podman_home }}/.config/systemd/user/zomboid-log-prune.{{ item }}"
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: '0644'
loop:
- service
- timer
notify: reload zomboid systemd
- name: enable zomboid log prune timer
become: true
become_user: "{{ podman_user }}"
ansible.builtin.systemd:
name: zomboid-log-prune.timer
scope: user
enabled: true
state: started
daemon_reload: true
# The intended world settings, kept somewhere the server cannot reach.
#
# PZ reads Server/<name>_SandboxVars.lua only when it creates a world, and then
# writes the *running world's* settings back over that same file. So the file in
# Server/ is an output, not an input: once any world is created with defaults,
# the server stamps those defaults into it and every later wipe regenerates from
# them. That is how a Sophie world quietly became an Apocalypse one -- 139 values
# reverted, loot rates from 0.35 back to 0.9, CharacterFreePoints from 0 to 60.
#
# This copy is what a wipe restores from, so settings survive it. Seeded once and
# then left alone, so it is the file to hand-edit when changing the world:
# edit here, then wipe, and the new world comes up with the edits.
- name: seed canonical zomboid world settings
become: true
ansible.builtin.copy:
src: "zomboid/sophie/{{ item }}.lua"
dest: "{{ zomboid_path }}/config-template/{{ item }}.lua"
force: "{{ zomboid_config_force | bool }}"
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: 0644
loop:
- SandboxVars
- spawnregions
- spawnpoints
tags: zomboid-conf
# The server's own logs, not the container's. PZ holds these fds open for the
# life of the process, so copytruncate is mandatory -- a rename would leave it
# writing into an unlinked inode and the file would appear to stop growing.
- name: deploy zomboid logrotate config
become: true
ansible.builtin.template:
src: zomboid/logrotate.j2
dest: /etc/logrotate.d/zomboid
owner: root
group: root
mode: 0644
# Set volume permissions for steam user (UID 1000) inside container
# This uses podman unshare to set ownership correctly for rootless podman
- name: set zomboid volume permissions for steam user
become: true
become_user: "{{ podman_user }}"
ansible.builtin.shell: |
podman unshare chown -R 1000:1000 {{ zomboid_path }}/server
podman unshare chown -R 1000:1000 {{ zomboid_path }}/data
changed_when: false
- name: flush handlers
ansible.builtin.meta: flush_handlers
- import_tasks: podman/podman-check.yml
vars:
container_name: zomboid
container_image: "{{ image }}"
- name: create zomboid container
become: true
become_user: "{{ podman_user }}"
containers.podman.podman_container:
name: zomboid
image: "{{ image }}"
restart_policy: on-failure:3
log_driver: k8s-file
log_opt:
max_size: 50m
env:
SERVER_NAME: "{{ zomboid_server_name }}"
MIN_RAM: "{{ zomboid_min_ram }}"
MAX_RAM: "{{ zomboid_max_ram }}"
AUTO_UPDATE: "true"
ADMIN_PASSWORD: "{{ zomboid_admin_password }}"
SERVER_PASSWORD: "{{ zomboid_password }}"
PUID: "1000"
PGID: "1000"
volumes:
- "{{ zomboid_path }}/server:/project-zomboid"
- "{{ zomboid_path }}/data:/project-zomboid-config"
- "{{ zomboid_path }}/scripts/entrypoint.sh:/entrypoint.sh:ro"
- "{{ zomboid_path }}/scripts/install.scmd:/home/steam/install.scmd:ro"
ports:
- "16261:16261/udp"
- "16262:16262/udp"
- "{{ zomboid_rcon_port }}:{{ zomboid_rcon_port }}/tcp"
command: /bin/bash /entrypoint.sh
- name: create systemd startup job for zomboid
include_tasks: podman/systemd-generate.yml
vars:
container_name: zomboid
# Make systemd actually supervise the container, so it restarts on any exit --
# including the clean one that `@bot restart` produces via RCON quit.
#
# `podman generate systemd` emits Type=forking with ExecStart=podman start and
# a PIDFile pointing at conmon. podman start returns immediately, so the process
# systemd is told to watch was never its child; it says so itself in the journal
# ("Supervising process N which is not our child. We'll most likely not notice
# when it exits") and it does not notice. The unit sat at active/running with
# NRestarts=0 while the container was exited(0) and the server was down. The
# PIDFile is worse than useless here: it embeds the container ID, so it goes
# stale every time a deploy recreates the container.
#
# Type=simple with `podman start -a` keeps podman in the foreground as systemd's
# own child, so the exit is seen and Restart=always fires.
#
# stdout/stderr are discarded on purpose. Attaching re-emits the container's
# output on podman's stdout, which systemd would capture straight back into the
# journal -- exactly the flood the k8s-file log driver exists to avoid. Nothing
# is lost: `podman logs` still serves it from the container's own log.
- name: configure zomboid systemd supervision
become: true
become_user: "{{ podman_user }}"
ansible.builtin.lineinfile:
path: "{{ podman_home }}/.config/systemd/user/zomboid.service"
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
state: "{{ item.state | default('present') }}"
insertafter: '^\[Service\]'
loop:
- { regexp: '^Restart=', line: 'Restart=always' }
- { regexp: '^Type=', line: 'Type=simple' }
- { regexp: '^ExecStart=', line: 'ExecStart=/usr/bin/podman start -a zomboid' }
- { regexp: '^StandardOutput=', line: 'StandardOutput=null' }
- { regexp: '^StandardError=', line: 'StandardError=null' }
- { regexp: '^PIDFile=', line: '', state: absent }
notify: reload zomboid systemd
# Firewall logging for player IP correlation
# Logs new UDP connections to Zomboid port for IP address tracking
- name: add firewall rule to log zomboid connections
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 0
-p udp --dport 16261 -m conntrack --ctstate NEW
-j LOG --log-prefix "ZOMBOID_CONN: " --log-level 4
register: firewall_result
changed_when: "'already' not in firewall_result.stderr"
failed_when: false
notify: restart firewalld
tags: firewall
- name: add firewall rule to log zomboid connections (runtime)
become: true
ansible.builtin.command: >
firewall-cmd --direct --add-rule ipv4 filter INPUT 0
-p udp --dport 16261 -m conntrack --ctstate NEW
-j LOG --log-prefix "ZOMBOID_CONN: " --log-level 4
changed_when: false
failed_when: false
tags: firewall
# =============================================================================
# Add logging for port 16262 (mirrors existing 16261 logging)
# =============================================================================
- name: add firewall rule to log zomboid connections on 16262
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 0
-p udp --dport 16262 -m conntrack --ctstate NEW
-j LOG --log-prefix "ZOMBOID_CONN: " --log-level 4
register: firewall_result_16262
changed_when: "'already' not in firewall_result_16262.stderr"
failed_when: false
notify: restart firewalld
tags: firewall
- name: add firewall rule to log zomboid connections on 16262 (runtime)
become: true
ansible.builtin.command: >
firewall-cmd --direct --add-rule ipv4 filter INPUT 0
-p udp --dport 16262 -m conntrack --ctstate NEW
-j LOG --log-prefix "ZOMBOID_CONN: " --log-level 4
changed_when: false
failed_when: false
tags: firewall
# =============================================================================
# Zomboid Rate Limiting and Query Flood Protection
# =============================================================================
# These rules mitigate Steam server query floods while allowing legitimate play.
# Query packets are typically 53 bytes; game traffic is larger and sustained.
#
# Rule priority: 0=logging (existing), 1=allow established, 2=rate limit queries
# Allow established/related connections without rate limiting
# This ensures active players aren't affected by query rate limits
- name: allow established zomboid connections on 16261
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 1
-p udp --dport 16261 -m conntrack --ctstate ESTABLISHED,RELATED
-j ACCEPT
register: established_result
changed_when: "'already' not in established_result.stderr"
failed_when: false
notify: restart firewalld
tags: firewall
- name: allow established zomboid connections on 16262
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 1
-p udp --dport 16262 -m conntrack --ctstate ESTABLISHED,RELATED
-j ACCEPT
register: established_result_16262
changed_when: "'already' not in established_result_16262.stderr"
failed_when: false
notify: restart firewalld
tags: firewall
# =============================================================================
# Smart Zomboid Traffic Filtering (Packet-Size Based)
# =============================================================================
# Distinguishes legitimate players from scanner bots:
# - Players send varied packet sizes (53, 37, 1472 bytes)
# - Scanners only send 53-byte query packets
#
# Rule priority:
# 0 = LOG all (existing above)
# 1 = ACCEPT established (existing above)
# 2 = Mark + ACCEPT non-query packets (verifies player)
# 3 = ACCEPT queries from verified IPs
# 4 = LOG rate-limited queries from unverified IPs
# 5 = DROP rate-limited queries from unverified IPs
# Priority 2: Mark IPs sending non-query packets as verified (1 hour TTL)
# Any packet NOT 53 bytes proves actual connection attempt
- name: mark verified players on 16261 (non-query packets)
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 2
-p udp --dport 16261 -m conntrack --ctstate NEW
-m length ! --length 53
-m recent --name zomboid_verified --set
-j ACCEPT
register: verify_result
changed_when: "'already' not in verify_result.stderr"
failed_when: false
notify: restart firewalld
tags: firewall
- name: mark verified players on 16262 (non-query packets)
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 2
-p udp --dport 16262 -m conntrack --ctstate NEW
-m length ! --length 53
-m recent --name zomboid_verified --set
-j ACCEPT
register: verify_result_16262
changed_when: "'already' not in verify_result_16262.stderr"
failed_when: false
notify: restart firewalld
tags: firewall
# Priority 3: Allow queries from verified players (within 1 hour)
- name: allow queries from verified players on 16261
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 3
-p udp --dport 16261 -m conntrack --ctstate NEW
-m length --length 53
-m recent --name zomboid_verified --rcheck --seconds 3600
-j ACCEPT
register: verified_query_result
changed_when: "'already' not in verified_query_result.stderr"
failed_when: false
notify: restart firewalld
tags: firewall
- name: allow queries from verified players on 16262
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 3
-p udp --dport 16262 -m conntrack --ctstate NEW
-m length --length 53
-m recent --name zomboid_verified --rcheck --seconds 3600
-j ACCEPT
register: verified_query_result_16262
changed_when: "'already' not in verified_query_result_16262.stderr"
failed_when: false
notify: restart firewalld
tags: firewall
# Priority 4/5: query flood limiting.
#
# The original thresholds were 1/hour burst 2 per source IP, on the theory that
# only scanners send 53-byte query packets and that real players would first be
# marked verified by the priority-2 rule when they sent something else. That
# premise is inverted: a client's *first* contact is a 53-byte query, so nobody
# can be verified before they query, and nobody can query more than twice an
# hour without being dropped. The counters were unambiguous -- 5 packets ever
# matched the verified-accept rule against 30,563 drops -- and fail2ban then
# banned the dropped players for a week each, several an hour, all residential
# IPs. The server was unreachable for everyone.
#
# Remove the old rules so hosts carrying them converge, then re-add the same
# shape at a threshold no real client reaches. Opening the server browser sends
# a handful of queries; a flood sends thousands.
- name: remove broken log query rate-limit rule on 16261 (permanent)
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --remove-rule ipv4 filter INPUT 4
-p udp --dport 16261 -m conntrack --ctstate NEW
-m length --length 53
-m hashlimit --hashlimit-above 1/hour --hashlimit-burst 2
--hashlimit-mode srcip --hashlimit-name zomboid_query_16261
--hashlimit-htable-expire 3600000
-j LOG --log-prefix "ZOMBOID_RATELIMIT: " --log-level 4
changed_when: false
failed_when: false
tags: firewall
- name: remove broken log query rate-limit rule on 16261 (runtime)
become: true
ansible.builtin.command: >
firewall-cmd --direct --remove-rule ipv4 filter INPUT 4
-p udp --dport 16261 -m conntrack --ctstate NEW
-m length --length 53
-m hashlimit --hashlimit-above 1/hour --hashlimit-burst 2
--hashlimit-mode srcip --hashlimit-name zomboid_query_16261
--hashlimit-htable-expire 3600000
-j LOG --log-prefix "ZOMBOID_RATELIMIT: " --log-level 4
changed_when: false
failed_when: false
tags: firewall
- name: remove broken drop query rate-limit rule on 16261 (permanent)
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --remove-rule ipv4 filter INPUT 5
-p udp --dport 16261 -m conntrack --ctstate NEW
-m length --length 53
-m hashlimit --hashlimit-above 1/hour --hashlimit-burst 2
--hashlimit-mode srcip --hashlimit-name zomboid_query_16261
--hashlimit-htable-expire 3600000
-j DROP
changed_when: false
failed_when: false
tags: firewall
- name: remove broken drop query rate-limit rule on 16261 (runtime)
become: true
ansible.builtin.command: >
firewall-cmd --direct --remove-rule ipv4 filter INPUT 5
-p udp --dport 16261 -m conntrack --ctstate NEW
-m length --length 53
-m hashlimit --hashlimit-above 1/hour --hashlimit-burst 2
--hashlimit-mode srcip --hashlimit-name zomboid_query_16261
--hashlimit-htable-expire 3600000
-j DROP
changed_when: false
failed_when: false
tags: firewall
- name: remove broken log query rate-limit rule on 16262 (permanent)
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --remove-rule ipv4 filter INPUT 4
-p udp --dport 16262 -m conntrack --ctstate NEW
-m length --length 53
-m hashlimit --hashlimit-above 1/hour --hashlimit-burst 2
--hashlimit-mode srcip --hashlimit-name zomboid_query_16262
--hashlimit-htable-expire 3600000
-j LOG --log-prefix "ZOMBOID_RATELIMIT: " --log-level 4
changed_when: false
failed_when: false
tags: firewall
- name: remove broken log query rate-limit rule on 16262 (runtime)
become: true
ansible.builtin.command: >
firewall-cmd --direct --remove-rule ipv4 filter INPUT 4
-p udp --dport 16262 -m conntrack --ctstate NEW
-m length --length 53
-m hashlimit --hashlimit-above 1/hour --hashlimit-burst 2
--hashlimit-mode srcip --hashlimit-name zomboid_query_16262
--hashlimit-htable-expire 3600000
-j LOG --log-prefix "ZOMBOID_RATELIMIT: " --log-level 4
changed_when: false
failed_when: false
tags: firewall
- name: remove broken drop query rate-limit rule on 16262 (permanent)
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --remove-rule ipv4 filter INPUT 5
-p udp --dport 16262 -m conntrack --ctstate NEW
-m length --length 53
-m hashlimit --hashlimit-above 1/hour --hashlimit-burst 2
--hashlimit-mode srcip --hashlimit-name zomboid_query_16262
--hashlimit-htable-expire 3600000
-j DROP
changed_when: false
failed_when: false
tags: firewall
- name: remove broken drop query rate-limit rule on 16262 (runtime)
become: true
ansible.builtin.command: >
firewall-cmd --direct --remove-rule ipv4 filter INPUT 5
-p udp --dport 16262 -m conntrack --ctstate NEW
-m length --length 53
-m hashlimit --hashlimit-above 1/hour --hashlimit-burst 2
--hashlimit-mode srcip --hashlimit-name zomboid_query_16262
--hashlimit-htable-expire 3600000
-j DROP
changed_when: false
failed_when: false
tags: firewall
- name: log query floods on 16261
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 4
-p udp --dport 16261 -m conntrack --ctstate NEW
-m length --length 53
-m hashlimit --hashlimit-above {{ zomboid_query_rate_limit }}
--hashlimit-burst {{ zomboid_query_burst }}
--hashlimit-mode srcip --hashlimit-name zomboid_query_16261
--hashlimit-htable-expire 3600000
-j LOG --log-prefix "ZOMBOID_RATELIMIT: " --log-level 4
register: qflood_log_16261
changed_when: "'already' not in qflood_log_16261.stderr"
failed_when: false
notify: restart firewalld
tags: firewall
- name: drop query floods on 16261
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 5
-p udp --dport 16261 -m conntrack --ctstate NEW
-m length --length 53
-m hashlimit --hashlimit-above {{ zomboid_query_rate_limit }}
--hashlimit-burst {{ zomboid_query_burst }}
--hashlimit-mode srcip --hashlimit-name zomboid_query_16261
--hashlimit-htable-expire 3600000
-j DROP
register: qflood_drop_16261
changed_when: "'already' not in qflood_drop_16261.stderr"
failed_when: false
notify: restart firewalld
tags: firewall
- name: log query floods on 16262
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 4
-p udp --dport 16262 -m conntrack --ctstate NEW
-m length --length 53
-m hashlimit --hashlimit-above {{ zomboid_query_rate_limit }}
--hashlimit-burst {{ zomboid_query_burst }}
--hashlimit-mode srcip --hashlimit-name zomboid_query_16262
--hashlimit-htable-expire 3600000
-j LOG --log-prefix "ZOMBOID_RATELIMIT: " --log-level 4
register: qflood_log_16262
changed_when: "'already' not in qflood_log_16262.stderr"
failed_when: false
notify: restart firewalld
tags: firewall
- name: drop query floods on 16262
become: true
ansible.builtin.command: >
firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 5
-p udp --dport 16262 -m conntrack --ctstate NEW
-m length --length 53
-m hashlimit --hashlimit-above {{ zomboid_query_rate_limit }}
--hashlimit-burst {{ zomboid_query_burst }}
--hashlimit-mode srcip --hashlimit-name zomboid_query_16262
--hashlimit-htable-expire 3600000
-j DROP
register: qflood_drop_16262
changed_when: "'already' not in qflood_drop_16262.stderr"
failed_when: false
notify: restart firewalld
tags: firewall
# World reset is now triggered via Discord bot -> systemd path unit
# See zomboid-world-reset.path and zomboid-world-reset.service
- name: enable zomboid world reset path unit
become: true
become_user: "{{ podman_user }}"
ansible.builtin.systemd:
name: zomboid-world-reset.path
scope: user
enabled: true
state: started
daemon_reload: true