Files
deploy_home/ansible/roles/podman/tasks/containers/home/zomboid.yml
T
Bastian de BylandClaude Opus 5 be0d02b938 feat(zomboid): restore the world from one of PZ's own backups
Rolling a world back meant hand-work over ssh: stop the service, move the live
save aside, unzip the right archive, chown into the container's subuid range,
relabel, start. That is the wrong shape of task to do by hand, and it is always
done under time pressure -- by construction, because the archive you want is
being deleted while you work.

PZ keeps BackupsCount=10 per set and writes one every BackupsPeriod=30 minutes,
so a periodic backup is reachable for about five hours and then gone. On
2026-09-05 the snapshot the admins asked for (05:16, four minutes before the
incident) had about 90 minutes of life left when the request came in.

Same shape as the wipe: the Discord bot writes a trigger file into its own rw
volume, zomboid-restore.path notices it, and zomboid-restore.service runs the
script as the podman user. The bot gets no ssh, no systemd, and keeps only its
existing read-only mount of the Zomboid volume.

Two details carry most of the correctness.

Resolution is by mtime, not by index. The rotation renames the files -- today's
backup_7.zip is backup_8.zip half an hour from now, and a new backup_7.zip holds
a different world -- so an index is valid only while the listing is fresh, which
is not long enough to survive a human reading a confirmation prompt. The trigger
names a set and an mtime; the script resolves the path itself, whitelists the
filename, and refuses if nothing matches. It never accepts a path.

Everything that can fail is checked before the server is touched. A rotated-out
target, an archive with no debbzoid world in it, a bad action, a traversal
attempt in the set name: each aborts with the server still running and writes a
result file the bot reports back. The live world is moved aside rather than
deleted, so a restore is undoable and the last three are kept.

One thing PZ does not advertise: its backups do not cover the whole save
directory. blam/, a mod's own state, is in none of them -- not the 05:16 archive
and not the newest one. Restoring only what the archive holds therefore lands
the world slightly *behind* the target rather than on it, so anything present in
the displaced world and absent from the archive is carried across.

The gregtime tag moves to 3.17.0 for the bot half of this -- `backups`,
`restore <n>`, `restore confirm`, `restore undo`, gated to the same two admins
as the wipe. That image is built and running on the host; its source is not
committed yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016QdWYhwUtwM2NQGukiRh12
2026-09-05 09:12:09 -04:00

809 lines
29 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 host-side config directories
become: true
ansible.builtin.file:
path: "{{ zomboid_path }}/{{ item }}"
state: directory
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: 0755
loop:
- config-template
- config-backup
# Holds the world each restore displaces, so a restore is always undoable.
# Deliberately outside data/ -- that is the container's bind mount, and a
# stray world directory inside Saves/Multiplayer/ is something PZ would see.
- restore-backup
- 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 restore script
become: true
ansible.builtin.template:
src: zomboid/zomboid-restore.sh.j2
dest: "{{ podman_home }}/bin/zomboid-restore.sh"
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: '0755'
- name: deploy zomboid restore path unit
become: true
ansible.builtin.template:
src: zomboid/zomboid-restore.path.j2
dest: "{{ podman_home }}/.config/systemd/user/zomboid-restore.path"
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: '0644'
notify: reload zomboid systemd
- name: deploy zomboid restore service unit
become: true
ansible.builtin.template:
src: zomboid/zomboid-restore.service.j2
dest: "{{ podman_home }}/.config/systemd/user/zomboid-restore.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 *before* first boot, so the server never generates a
# vanilla INI we then have to patch.
#
# The INI and SandboxVars started as the vendored Sophie 42 preset and were
# re-synced from the running debbzoid world on 2026-08-31 -- see the header of
# templates/zomboid/server.ini.j2 for the INI keys that moved and why.
#
# 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
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 settings
become: true
ansible.builtin.copy:
src: zomboid/sophie/SandboxVars.lua
dest: "{{ zomboid_path }}/data/Server/{{ zomboid_server_name }}_SandboxVars.lua"
force: "{{ zomboid_config_force | bool }}"
owner: "{{ podman_subuid.stdout }}"
group: "{{ podman_user }}"
mode: 0644
notify: restorecon podman
tags: zomboid-conf
# Spawn config gets its own force switch, and it is not pedantry: these two
# files are the only part of the server config that a *running* world will pick
# up. PZ reads <name>_spawnregions.lua (and any serverfile it names) at every
# server start; SandboxVars is read once, when the world is created. So a spawn
# change is deployable on the live world -- push, restart, done -- while a
# SandboxVars change is not, and needs a wipe to mean anything.
#
# Sharing zomboid_config_force between them would mean force-pushing the whole
# preset to land a one-line spawnregions edit, which also rewrites the INI (a
# ResetID mismatch tells every client to reroll) and the world's SandboxVars.
#
# make deploy TAGS=zomboid-conf -e zomboid_spawn_force=true
#
# then restart the server -- with players offline -- for it to take effect.
- name: seed zomboid spawn config
become: true
ansible.builtin.copy:
src: "zomboid/sophie/{{ item }}.lua"
dest: "{{ zomboid_path }}/data/Server/{{ zomboid_server_name }}_{{ item }}.lua"
force: "{{ zomboid_spawn_force | bool }}"
owner: "{{ podman_subuid.stdout }}"
group: "{{ podman_user }}"
mode: 0644
loop:
- 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 settings this repo would deploy, kept where the server cannot overwrite them.
#
# Reference only -- nothing applies this automatically. A wipe deliberately leaves
# the live settings alone so hand tuning survives it.
#
# It exists because the live files cannot be trusted as a record of intent: PZ
# reads Server/<name>_SandboxVars.lua only when it creates a world, then writes
# the running world's settings back over it. The file in Server/ is an output as
# much as an input, and that is how a Sophie world quietly became an Apocalypse
# one -- 139 values reverted, loot from 0.35 back to 0.9, CharacterFreePoints
# from 0 to 60. When that happens again, this is what to copy back from.
#
# What it holds changed on 2026-08-31. It was the pristine Sophie preset; it is
# now a snapshot of the live debbzoid world, because the admins' tuning had by
# then diverged from the preset in the same 139 values and re-seeding from the
# preset would have thrown that tuning away rather than restored it.
#
# force is on here, and only here. Nothing on the host writes this directory --
# the server cannot see it -- so it has no hand edits to protect, and if it does
# not track the repo it is not a restore point, just an older world's settings.
- name: seed canonical zomboid world settings
become: true
ansible.builtin.copy:
src: "zomboid/sophie/{{ item }}.lua"
dest: "{{ zomboid_path }}/config-template/{{ item }}.lua"
force: true
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
# Restore is triggered the same way -- Discord bot -> trigger file -> path unit.
# See zomboid-restore.path and zomboid-restore.service.
- name: enable zomboid restore path unit
become: true
become_user: "{{ podman_user }}"
ansible.builtin.systemd:
name: zomboid-restore.path
scope: user
enabled: true
state: started
daemon_reload: true