From 507f4822f029160dc64811513247a16c40fd681b Mon Sep 17 00:00:00 2001 From: ALI YESILKAYA Date: Fri, 3 Jul 2026 00:56:28 +0200 Subject: [PATCH] =?UTF-8?q?feat(ansible):=20serveur=20d=C3=A9di=C3=A9=20Sa?= =?UTF-8?q?tisfactory=20sur=20gpu-01=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(ansible): serveur dédié Satisfactory sur gpu-01 - Nouveau rôle satisfactory_server : install steamcmd (login anonyme, app 1690800), service systemd avec update auto en ExecStartPre, MemoryMax=16G + Nice=5 (protège le llama-server), firewalld 7777 tcp+udp - Retries steamcmd (« Missing configuration » rc=8 au premier run) - Fcontext SELinux bin_t sur steamcmd/ et server/ (203/EXEC sinon) - gateway : DNAT 192.168.1.200:7777 → gpu-01:7777 (TCP+UDP) + forward - Doc admin/ops/satisfactory.md (accès, exploitation, pièges rencontrés) Co-Authored-By: Claude Fable 5 * fix(satisfactory): ouvrir le port 8888/tcp Reliable Messaging Depuis la 1.1, le client ouvre un socket TCP :8888 en plus du jeu UDP :7777. Bloqué par firewalld, le join réussissait puis le client était déconnecté (~60 s) avec « connection to the host has been lost » (log client : LogReliableMessaging: Failed to connect to server socket). - firewalld gpu-01 : + 8888/tcp (variable satisfactory_reliable_port) - gateway s01 : DNAT + forward étendus à 8888/tcp - doc : tableau des 3 sockets + piège n°3 Co-Authored-By: Claude Fable 5 * docs(satisfactory): accès internet via IPv4 full-stack Free Le remap de port Freebox (17777→7777) ne fonctionne pas : le serveur annonce ses ports dans le protocole (externe = interne obligatoire). Option IPv4 full-stack activée → redirections 1:1 7777 tcp+udp + 8888 tcp vers funk-s01, DNAT existant inchangé. Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Claude Fable 5 --- CLAUDE.md | 2 +- admin/ops/satisfactory.md | 71 ++++++++++ ansible/playbooks/gpu-01.yml | 1 + .../roles/gateway/templates/nftables.conf.j2 | 11 ++ ansible/roles/satisfactory_server/README.md | 35 +++++ .../satisfactory_server/defaults/main.yml | 16 +++ .../satisfactory_server/handlers/main.yml | 9 ++ .../roles/satisfactory_server/tasks/main.yml | 123 ++++++++++++++++++ .../templates/satisfactory-server.service.j2 | 23 ++++ 9 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 admin/ops/satisfactory.md create mode 100644 ansible/roles/satisfactory_server/README.md create mode 100644 ansible/roles/satisfactory_server/defaults/main.yml create mode 100644 ansible/roles/satisfactory_server/handlers/main.yml create mode 100644 ansible/roles/satisfactory_server/tasks/main.yml create mode 100644 ansible/roles/satisfactory_server/templates/satisfactory-server.service.j2 diff --git a/CLAUDE.md b/CLAUDE.md index a409773..9ee4e2d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -94,7 +94,7 @@ Les tags correspondent aux noms de rôles. Tous les rôles sont idempotents. ### Rôles — gpu-01 -`hermes_user`, `common`, `rocm`, `nfs_client`, `llama_server`, `node_exporter` +`hermes_user`, `common`, `rocm`, `nfs_client`, `llama_server`, `node_exporter`, `satisfactory_server` (serveur dédié Satisfactory `:7777`, steamcmd + systemd) > Chaque rôle a un `README.md` (variables, caveats, gaps IaC). > `lm_studio` est présent dans `ansible/roles/` mais n'est plus utilisé dans aucun playbook (déprécié, voir son README). diff --git a/admin/ops/satisfactory.md b/admin/ops/satisfactory.md new file mode 100644 index 0000000..ef2faf4 --- /dev/null +++ b/admin/ops/satisfactory.md @@ -0,0 +1,71 @@ +# Serveur dédié Satisfactory (gpu-01) + +Serveur de jeu **Satisfactory** hébergé sur **gpu-01** (192.168.10.20), à côté du +llama-server. Installé par le rôle Ansible **`satisfactory_server`** (steamcmd, login +anonyme Steam, app `1690800`) — service systemd `satisfactory-server`. + +## Accès + +| Depuis | Adresse | +|---|---| +| LAN domestique (192.168.1.x) | `192.168.10.20:7777` (route via s01) **ou** `192.168.1.200:7777` (DNAT) | +| Internet (amis) | `kaya.freeboxos.fr:7777` — redirections Freebox → funk-s01, le DNAT fait le reste | + +> ⚠️ **Le remap de port (ex. 17777→7777) ne marche PAS** : le serveur annonce ses ports +> au client dans le protocole (API → port de jeu, jeu → reliable port). Externe = interne +> obligatoire ([wiki](https://satisfactory.wiki.gg/wiki/Dedicated_servers)). Free IPv4 +> partagée ne permettant pas de rediriger < 16384, l'option **IPv4 full-stack** (gratuite, +> espace abonné Free) a été activée le 2026-07-03 (IP fixe `82.67.223.17`) pour pouvoir +> ouvrir 7777/8888 en 1:1. Redirections Freebox requises (destination **funk-s01**) : +> `7777/tcp`, `7777/udp`, `8888/tcp` — ports de début/fin/destination identiques. + +Dans le jeu : **Server Manager → Add Server**. Nom du serveur et mot de passe admin se +définissent **en jeu à la première connexion** (pas de fichier de config). + +## Réseau + +Le serveur utilise **trois sockets** (Satisfactory ≥ 1.1) : + +| Port | Proto | Rôle | +|---|---|---| +| 7777 | UDP | Jeu | +| 7777 | TCP | API HTTPS (Server Manager) | +| 8888 | TCP | **Reliable Messaging** — bloqué = déconnexion ~60 s après chaque join | + +- **gpu-01 firewalld** : `7777/tcp+udp` + `8888/tcp` ouverts (gérés par le rôle). + Les ports `7778/udp` et `27015/udp` encore ouverts sont des restes pré-1.0, inutiles. +- **storage-01 nftables** (rôle `gateway`) : DNAT `192.168.1.200 → 192.168.10.20` + (7777 TCP+UDP, 8888 TCP) + règles forward associées. Redéploiement : `--tags gateway`. + +## Exploitation + +```bash +# état / logs (sur gpu-01) +systemctl status satisfactory-server +journalctl -u satisfactory-server -f + +# mise à jour du jeu = simple restart (steamcmd +app_update en ExecStartPre) +systemctl restart satisfactory-server +``` + +- **Install** : `/srv/satisfactory/server` (~10 Go, disque OS), user système `satisfactory` +- **Sauvegardes** : `/srv/satisfactory/.config/Epic/FactoryGame/Saved/SaveGames/server/` + — ⚠️ **aucun backup automatisé** (gap connu, comme etcd/PostgreSQL) +- **Garde-fous ressources** (32 Go partagés avec llama-server) : `MemoryMax=16G` — une + usine late-game qui explose la RAM fait tuer **le jeu**, pas le LLM — et `Nice=5` + (priorité CPU au llama-server). À ajuster via host_vars si besoin. + +## Pièges rencontrés à l'installation (2026-07-02) + +1. **steamcmd « Missing configuration » (rc=8)** au tout premier `+app_update` — bug + connu steamcmd (app info pas encore en cache). Le rôle fait des retries. +2. **SELinux 203/EXEC** : `steamcmd.sh` extrait depuis /tmp gardait le contexte + `user_tmp_t`, le jeu héritait de `var_t` (/srv) — systemd (init_t) refuse d'exécuter + ces types. Le rôle pose un fcontext **`bin_t`** sur `steamcmd/` et `server/` + + `restorecon`. Symptôme : `Failed at step EXEC ... Permission denied`. +3. **Déconnexion ~60 s après chaque join** (« Your connection to the host has been + lost ») : le port **8888/tcp (Reliable Messaging)** n'était pas ouvert. Le join UDP + réussit, puis le client tente le socket TCP 8888, se fait rejeter par firewalld et + abandonne. Diagnostic dans le log **client** + (`.../FactoryGame/Saved/Logs/FactoryGame.log`) : + `LogReliableMessaging: Error: Failed to connect to server socket`. diff --git a/ansible/playbooks/gpu-01.yml b/ansible/playbooks/gpu-01.yml index bbafb01..18b1a21 100644 --- a/ansible/playbooks/gpu-01.yml +++ b/ansible/playbooks/gpu-01.yml @@ -8,3 +8,4 @@ - { role: nfs_client, tags: [nfs_client] } - { role: llama_server, tags: [llama_server] } - { role: node_exporter, tags: [node_exporter] } + - { role: satisfactory_server, tags: [satisfactory_server] } diff --git a/ansible/roles/gateway/templates/nftables.conf.j2 b/ansible/roles/gateway/templates/nftables.conf.j2 index eeec749..9450336 100644 --- a/ansible/roles/gateway/templates/nftables.conf.j2 +++ b/ansible/roles/gateway/templates/nftables.conf.j2 @@ -67,6 +67,12 @@ table inet filter { # Réponses WAN → cluster (connexions initiées depuis le cluster) iif {{ wan_interface }} oif {{ lan_interface }} ct state established,related accept + + # Satisfactory — trafic DNATé (192.168.1.200) → gpu-01, quelle que soit la source + # (couvre un éventuel port-forward internet depuis la box). + # 7777 = jeu/API ; 8888 = reliable messaging TCP (1.1) — bloqué = kick à ~60 s + ip daddr 192.168.10.20 tcp dport { 7777, 8888 } ct state new,established,related accept + ip daddr 192.168.10.20 udp dport 7777 ct state new,established,related accept } chain output { @@ -77,6 +83,11 @@ table inet filter { table ip nat { chain prerouting { type nat hook prerouting priority -100; + + # Satisfactory — DNAT WAN → serveur dédié sur gpu-01 + # (joignable via 192.168.1.200 sans route vers 192.168.10.0/24) + iif {{ wan_interface }} tcp dport { 7777, 8888 } dnat to 192.168.10.20 + iif {{ wan_interface }} udp dport 7777 dnat to 192.168.10.20:7777 } chain postrouting { diff --git a/ansible/roles/satisfactory_server/README.md b/ansible/roles/satisfactory_server/README.md new file mode 100644 index 0000000..6d2ecac --- /dev/null +++ b/ansible/roles/satisfactory_server/README.md @@ -0,0 +1,35 @@ +# satisfactory_server + +Serveur dédié **Satisfactory** sur gpu-01 — installé via **steamcmd** (login anonyme, app +Steam `1690800`), lancé par systemd (`satisfactory-server.service`). + +## Variables (defaults) + +| Variable | Défaut | Rôle | +|---|---|---| +| `satisfactory_user` | `satisfactory` | Utilisateur système du service | +| `satisfactory_home` | `/srv/satisfactory` | Home + racine d'install | +| `satisfactory_install_dir` | `/srv/satisfactory/server` | Fichiers du jeu (~10 Go) | +| `satisfactory_port` | `7777` | Jeu (UDP) + API HTTPS (TCP) | +| `satisfactory_reliable_port` | `8888` | Reliable Messaging (TCP, ≥ 1.1) — **bloqué = kick à ~60 s** | +| `satisfactory_update_on_start` | `true` | `steamcmd +app_update` en `ExecStartPre` à chaque (re)start | +| `satisfactory_memory_max` | `16G` | `MemoryMax` systemd — protège llama-server (32 Go partagés) | +| `satisfactory_nice` | `5` | Priorité CPU réduite vs llama-server | + +## Caveats + +- **Premier run long** : le task d'installation télécharge ~10 Go depuis Steam + (guardé par `creates:` — ne rejoue jamais). Prévoir 10-30 min selon le débit. +- **Mises à jour du jeu** : gérées par l'`ExecStartPre` du service (à chaque restart), + pas par Ansible. `systemctl restart satisfactory-server` = update + relance. +- **Sauvegardes** : `~satisfactory/.config/Epic/FactoryGame/Saved/SaveGames/server/` + — **non sauvegardées ailleurs** (pas de backup automatisé, gap connu). +- **Config serveur** (nom, mot de passe admin) : se fait **en jeu** à la première + connexion (menu Server Manager), pas par fichier. +- **RAM** : usine late-game > 12 Go possible. `MemoryMax=16G` fait tuer le jeu par + l'OOM-killer plutôt que d'affamer le slot LLM (:1234) — augmenter si besoin via host_vars. +- **Réseau** : ton PC (192.168.1.x) joint gpu-01:7777 via storage-01 — la chaîne + `forward` du rôle `gateway` autorise déjà tout le trafic LAN domestique → LAN cluster. + Dans le jeu : ajouter le serveur avec l'adresse `192.168.10.20:7777`. +- Les ports firewalld `7778/udp` et `27015/udp` présents sur gpu-01 sont des restes + des anciens ports « Experimental » (avant la 1.0) — inutiles désormais. diff --git a/ansible/roles/satisfactory_server/defaults/main.yml b/ansible/roles/satisfactory_server/defaults/main.yml new file mode 100644 index 0000000..6416f43 --- /dev/null +++ b/ansible/roles/satisfactory_server/defaults/main.yml @@ -0,0 +1,16 @@ +--- +satisfactory_user: satisfactory +satisfactory_home: /srv/satisfactory +satisfactory_steamcmd_dir: "{{ satisfactory_home }}/steamcmd" +satisfactory_install_dir: "{{ satisfactory_home }}/server" +satisfactory_app_id: 1690800 # Satisfactory Dedicated Server (Steam, login anonyme) +satisfactory_port: 7777 # jeu (UDP) + API HTTPS (TCP) +satisfactory_reliable_port: 8888 # socket TCP « Reliable Messaging » (depuis la 1.1) — + # bloqué = déconnexion ~60 s après chaque join +satisfactory_update_on_start: true # steamcmd +app_update à chaque (re)démarrage du service + +# Garde-fous ressources : gpu-01 partage ses 32 Go avec llama-server (~10 Go résidents). +# Une usine late-game peut dépasser 12 Go — MemoryMax fait tuer le jeu par l'OOM-killer +# plutôt que d'affamer le slot LLM. Nice > 0 : priorité CPU au llama-server. +satisfactory_memory_max: 16G +satisfactory_nice: 5 diff --git a/ansible/roles/satisfactory_server/handlers/main.yml b/ansible/roles/satisfactory_server/handlers/main.yml new file mode 100644 index 0000000..121622e --- /dev/null +++ b/ansible/roles/satisfactory_server/handlers/main.yml @@ -0,0 +1,9 @@ +--- +- name: Reload systemd + ansible.builtin.systemd: + daemon_reload: true + +- name: Restart satisfactory-server + ansible.builtin.systemd: + name: satisfactory-server + state: restarted diff --git a/ansible/roles/satisfactory_server/tasks/main.yml b/ansible/roles/satisfactory_server/tasks/main.yml new file mode 100644 index 0000000..4c532cf --- /dev/null +++ b/ansible/roles/satisfactory_server/tasks/main.yml @@ -0,0 +1,123 @@ +--- +# --- Dépendances steamcmd (binaire 32 bits) --- + +- name: Install steamcmd 32-bit dependencies + ansible.builtin.dnf: + name: + - glibc.i686 + - libstdc++.i686 + - policycoreutils-python-utils # semanage (contexte SELinux, cf. plus bas) + state: present + +# --- Utilisateur système --- + +- name: Create satisfactory system user + ansible.builtin.user: + name: "{{ satisfactory_user }}" + system: true + shell: /sbin/nologin + home: "{{ satisfactory_home }}" + create_home: true + state: present + +- name: Create install directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: "{{ satisfactory_user }}" + group: "{{ satisfactory_user }}" + mode: '0750' + loop: + - "{{ satisfactory_steamcmd_dir }}" + - "{{ satisfactory_install_dir }}" + +# --- steamcmd --- + +- name: Download steamcmd + ansible.builtin.get_url: + url: "https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz" + dest: /tmp/steamcmd_linux.tar.gz + mode: '0644' + +- name: Extract steamcmd + ansible.builtin.unarchive: + src: /tmp/steamcmd_linux.tar.gz + dest: "{{ satisfactory_steamcmd_dir }}" + remote_src: true + owner: "{{ satisfactory_user }}" + group: "{{ satisfactory_user }}" + creates: "{{ satisfactory_steamcmd_dir }}/steamcmd.sh" + +# --- Installation initiale du jeu (~10 Go — long au premier run) --- +# Les mises à jour suivantes passent par l'ExecStartPre du service +# (satisfactory_update_on_start), pas par ce task. + +# Retries : au tout premier run, steamcmd échoue souvent avec « Missing configuration » +# (rc=8) — l'app info n'est pas encore en cache. Le run suivant passe. +- name: Install Satisfactory dedicated server via steamcmd (first run only) + ansible.builtin.command: + cmd: > + {{ satisfactory_steamcmd_dir }}/steamcmd.sh + +force_install_dir {{ satisfactory_install_dir }} + +login anonymous + +app_update {{ satisfactory_app_id }} validate + +quit + creates: "{{ satisfactory_install_dir }}/FactoryServer.sh" + become: true + become_user: "{{ satisfactory_user }}" + environment: + HOME: "{{ satisfactory_home }}" + register: satisfactory_steamcmd_install + retries: 3 + delay: 10 + until: satisfactory_steamcmd_install.rc | default(0) == 0 + +# --- SELinux --- +# L'unarchive depuis /tmp laisse steamcmd.sh en user_tmp_t et le jeu hérite de var_t +# (/srv) : systemd (init_t) refuse d'exécuter ces types → 203/EXEC Permission denied. +# bin_t sur les deux arborescences exécutables règle le problème. + +- name: Set bin_t SELinux context on executable trees + community.general.sefcontext: + target: "{{ satisfactory_home }}/(steamcmd|server)(/.*)?" + setype: bin_t + state: present + when: ansible_selinux.status | default('disabled') == 'enabled' + +- name: Apply SELinux contexts + ansible.builtin.command: + cmd: restorecon -R {{ satisfactory_home }} + changed_when: false + when: ansible_selinux.status | default('disabled') == 'enabled' + +# --- Service systemd --- + +- name: Install satisfactory-server systemd unit + ansible.builtin.template: + src: satisfactory-server.service.j2 + dest: /etc/systemd/system/satisfactory-server.service + mode: '0644' + notify: + - Reload systemd + - Restart satisfactory-server + +- name: Enable satisfactory-server service + ansible.builtin.systemd: + name: satisfactory-server + enabled: true + daemon_reload: true + state: started + +# --- Firewall --- +# 7777 : jeu (UDP) + API HTTPS (TCP). 8888 : reliable messaging (TCP, depuis la 1.1). + +- name: Open game ports in firewall + ansible.posix.firewalld: + port: "{{ item }}" + permanent: true + state: enabled + immediate: true + loop: + - "{{ satisfactory_port }}/tcp" + - "{{ satisfactory_port }}/udp" + - "{{ satisfactory_reliable_port }}/tcp" diff --git a/ansible/roles/satisfactory_server/templates/satisfactory-server.service.j2 b/ansible/roles/satisfactory_server/templates/satisfactory-server.service.j2 new file mode 100644 index 0000000..56daa09 --- /dev/null +++ b/ansible/roles/satisfactory_server/templates/satisfactory-server.service.j2 @@ -0,0 +1,23 @@ +[Unit] +Description=Satisfactory dedicated server (steamcmd, app {{ satisfactory_app_id }}) +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User={{ satisfactory_user }} +WorkingDirectory={{ satisfactory_install_dir }} +{% if satisfactory_update_on_start %} +# Mise à jour du jeu avant chaque démarrage (login anonyme) — ajoute ~1 min au boot. +# Désactivable via satisfactory_update_on_start: false (host_vars). +ExecStartPre={{ satisfactory_steamcmd_dir }}/steamcmd.sh +force_install_dir {{ satisfactory_install_dir }} +login anonymous +app_update {{ satisfactory_app_id }} +quit +{% endif %} +ExecStart={{ satisfactory_install_dir }}/FactoryServer.sh -Port={{ satisfactory_port }} -unattended +Restart=on-failure +RestartSec=15 +Nice={{ satisfactory_nice }} +MemoryMax={{ satisfactory_memory_max }} +TimeoutStartSec=600 + +[Install] +WantedBy=multi-user.target