warn when the services can't reach moved folders

- list, new, import and enable check TERRARIA_BASE and TERRARIA_DIST
  against the units' ProtectSystem/ProtectHome/ReadWritePaths
  (terraria@ always, terractld when it's on) and print the drop-in
  to add. prepare explains why it can't create an instance folder.
- Guide: bind mounts and drop-ins under "Changing locations", and a
  new section for instances or zips under /home.
- journal-access in tab completion and the command reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-10-02 21:51:29 +00:00
1 parent 96f7876729
commit 7adf5c2999
5 files changed
+195 -9

No files matched your search

+3
View File
@@ -8,6 +8,9 @@
- Going to an older version needs `--downgrade`, because worlds saved by a newer version can't be opened by an older one.
- Bash completion offers the instance's zips. Works with `--host`.
- `reinstall` now always unpacks the **same** version again. Before, an instance with a blank `ZIP=` picked the newest zip in `dist/`, so `reinstall` could silently upgrade it. It now pins the zip the instance runs first, and refuses if that zip is gone from `dist/`. It also keeps the record of the installed version (`server/.installed`) until the new unpack is done.
- `list`, `new`, `import` and `enable` warn when `TERRARIA_BASE` or `TERRARIA_DIST` is somewhere the services' sandbox doesn't allow (the instances folder not in `ReadWritePaths=`, or either folder under `/home`), and print the exact drop-in to add. The daemon (`terractld.service`) is checked too when it's on. A server that can't create its folder now says why in its log, instead of only "Read-only file system".
- The guide's "Changing locations" now covers bind mounts (the simplest way to use another disk) as well as drop-ins, and a new section explains keeping instances or zips under `/home`.
- `journal-access` is offered by tab completion and listed in the guide's command reference.
- `terractl` run by a relative path (e.g. `sudo ./bin/terractl prepare <name>` from a source tree) no longer fails to switch to the `terraria` user.
- New `fix-perms` command: `sudo terractl fix-perms <name>` gives every file of an instance back to the `terraria` user and reapplies the usual modes (group can read, others nothing; server binaries executable), e.g. after copying worlds in as root. Symlinks are not followed. Works with `--host` too.
- `list` warns about zips in `dist/` that the `terraria` user can't read (e.g. copied in with `scp -p` from a private file), and `info` lists them as `unreadable_zip=`. A server that fails to install because of one now says how to fix it (`sudo chmod 644 <zip>`) instead of just "cannot read", and a missing zip is reported as not found.
+107 -2
View File
@@ -311,8 +311,11 @@ instance_running() {
}
cmd_prepare() {
local name=$1 dir; dir=$(inst_dir "$name")
mkdir -p "$dir/data/Worlds"
local name=$1 dir err; dir=$(inst_dir "$name")
if ! err=$(mkdir -p "$dir/data/Worlds" 2>&1); then
location_warnings
die "can't create $dir/data/Worlds: ${err##*: }"
fi
take_lock "$name" # never re-unpack server/ under a running server
[[ -f $dir/instance.conf ]] || write_instance_conf "$name" vanilla
load_conf "$name"
@@ -833,6 +836,7 @@ cmd_import() {
echo "imported '$name' into $dir"
fi
echo "Review $dir/serverconfig.txt, then: $(hint root enable "$name" --now)"
location_warnings
}
# ---------------------------------------------------------------- mods ----
@@ -1017,6 +1021,7 @@ EOF
$(hint root enable "$name" --now)
$(hint group attach "$name")
EOF
location_warnings
}
# Whether the server starts at boot. Needs root: systemd doesn't let polkit
@@ -1035,6 +1040,7 @@ cmd_enable() {
# Enabling a name without an instance would create a default one at boot
[[ -f $(inst_dir "$name")/instance.conf ]] ||
die "no instance '$name' (create it with: $(hint root new "$name"))"
location_warnings
systemctl enable -q "$unit"
if [[ $now -eq 1 ]]; then
systemctl start "$unit"
@@ -1088,6 +1094,104 @@ zip_warning() {
log "fix it with$where: sudo chmod 644 ${bad[*]@Q}"
}
# ------------------------------------------------------------ locations ----
# The services' sandboxes (ProtectSystem=, ProtectHome=, ReadWritePaths=) are
# set in their unit files, which can't follow /etc/default/terractl. If
# TERRARIA_BASE or TERRARIA_DIST is moved somewhere a unit can't reach, it needs
# a drop-in; these checks say so, with the drop-in to add.
# Is <path> <dir>, or inside it?
path_in() { [[ $1 == "$2" || $1 == "$2"/* ]]; }
# Why <unit> can't use <path> (need: rw or ro): 'home' (ProtectHome hides it),
# 'home-ro' (ProtectHome=read-only, not writable) or 'system' (ProtectSystem
# makes it read-only). Prints nothing if the unit can use it, or if systemd or
# the unit isn't there to ask.
unit_path_problem() {
local unit=$1 path=$2 need=$3 key value load="" ps=no ph=no p covered=0
local -a rw=()
command -v systemctl >/dev/null || return 0
while IFS='=' read -r key value; do
case $key in
LoadState) load=$value ;;
ProtectSystem) ps=$value ;;
ProtectHome) ph=$value ;;
ReadWritePaths) read -ra rw <<< "$value" ;;
esac
done < <(systemctl show -p LoadState,ProtectSystem,ProtectHome,ReadWritePaths "$unit" 2>/dev/null)
[[ $load == loaded ]] || return 0
path=$(readlink -m "$path") # where it really is, as systemd sees it
for p in "${rw[@]}"; do
p=${p#-} # '-': ignored if missing
if path_in "$path" "$(readlink -m "$p")"; then covered=1; fi
done
if path_in "$path" /home || path_in "$path" /root || path_in "$path" /run/user; then
case $ph in
no) ;;
read-only) if [[ $need == rw && $covered -eq 0 ]]; then echo home-ro; fi ;;
*) echo home ;;
esac
return 0
fi
[[ $need == rw && $covered -eq 0 ]] || return 0
case $ps in
strict) echo system ;;
yes|full)
if path_in "$path" /usr || path_in "$path" /boot || path_in "$path" /efi ||
{ [[ $ps == full ]] && path_in "$path" /etc; }; then
echo system
fi ;;
esac
}
# Warns about one unit: location_warning <unit> <name to edit> <who it breaks>
# <dist access needed>
location_warning() {
local unit=$1 edit=$2 effect=$3 dist_need=$4 what path need problem home=0 where=""
local -a lines=() rw=()
for what in TERRARIA_BASE TERRARIA_DIST; do
if [[ $what == TERRARIA_BASE ]]; then path=$BASE_DIR need=rw; else path=$DIST_DIR need=$dist_need; fi
problem=$(unit_path_problem "$unit" "$path" "$need")
case $problem in
home)
lines+=("$edit hides $path ($what) under ProtectHome=, so $effect")
home=1
if [[ $need == rw ]]; then rw+=("$path"); fi ;;
home-ro|system)
lines+=("$edit can't write to $path ($what), so $effect")
if [[ $problem == home-ro ]]; then home=1; fi
rw+=("$path") ;;
esac
done
[[ ${#lines[@]} -gt 0 ]] || return 0
if [[ -n ${TERRACTL_VIA:-} ]]; then where=" (on $(hostname 2>/dev/null || echo that host))"; fi
printf 'terractl: warning: %s\n' "${lines[@]}" >&2
{
echo " Fix it with a drop-in$where: sudo systemctl edit $edit"
echo " [Service]"
if [[ $home -eq 1 ]]; then echo " ProtectHome=read-only"; fi
for path in "${rw[@]}"; do echo " ReadWritePaths=$path"; done
if [[ $home -eq 1 ]]; then
echo " The '$SVC_USER' user also needs access to every folder above it; see"
echo " \"Instances or zips under /home\" in the guide."
else
echo " See \"Changing locations\" in the guide."
fi
} >&2
}
# The servers always; the daemon only if it's in use. The daemon writes zips
# into dist/ when instances are copied to this host, so it needs both writable.
location_warnings() {
location_warning terraria@terractl-check.service terraria@.service \
"servers can't start" ro
if systemctl is-enabled --quiet terractld.service 2>/dev/null ||
systemctl is-active --quiet terractld.service 2>/dev/null; then
location_warning terractld.service terractld.service \
"other hosts can't copy, move or import instances here" rw
fi
}
cmd_list() {
local d n port boot state
printf '%-20s %-11s %-6s %-9s %s\n' INSTANCE TYPE PORT BOOT STATE
@@ -1105,6 +1209,7 @@ cmd_list() {
) 2>/dev/null || printf '%-20s %s\n' "$n" "(cannot read config - try sudo)"
done
zip_warning
location_warnings
}
# ------------------------------------------------------------ versions ----
+2 -2
View File
@@ -141,7 +141,7 @@ _terractl() {
cword=$((cword - 2))
commands+=" --help --version"
else
commands+=" copy move remote daemon prepare run --host --help --version"
commands+=" copy move remote daemon prepare run journal-access --host --help --version"
fi
if (( cword == 1 )); then
@@ -151,7 +151,7 @@ _terractl() {
local command=${words[1]}
case $command in
list|help|-h|--help|version|-V|--version)
list|journal-access|help|-h|--help|version|-V|--version)
return ;;
new)
# new <new name> [vanilla|tmodloader] [port]: the name is new, so nothing to offer
+6 -3
View File
@@ -1,13 +1,16 @@
# Overrides for terractl. Uncomment to change.
# Changes apply to new commands and to each instance's next start.
# Where instances live. If you change this, the service must also be allowed
# to write there: sudo systemctl edit terraria@.service
# Where instances live. To use another disk, it's simplest to mount it at the
# default location instead. If you change this, the service must also be
# allowed to write there: sudo systemctl edit terraria@.service
# [Service]
# ReadWritePaths=/your/new/path
# Under /home it needs more than that. 'terractl list' warns about what's
# missing; see "Changing locations" in the guide.
#TERRARIA_BASE=/var/lib/terraria/instances
# Where server zips are read from
# Where server zips are read from. Outside /home, no drop-in is needed.
#TERRARIA_DIST=/var/lib/terraria/dist
# Seconds to wait for a server to save and exit on stop (keep below 90)
+77 -2
View File
@@ -199,16 +199,86 @@ The version comes from the `VERSION` file. Set `Maintainer:` in `packaging/deb/c
### Changing locations
`/etc/default/terractl` can move the instances or zips directory. If you move the instances directory, the service also needs permission to write to the new location:
The servers run in a systemd sandbox: they can write only to `/var/lib/terraria/instances`, they can read but not change the rest of the system, and they can't see `/home` at all. These rules are in the unit files, which can't read `/etc/default/terractl`. So if the instances or zips go somewhere else, the services must be told as well.
**The simplest way: mount the storage at the usual place.** To keep instances on another disk, mount it (or a folder on it) at `/var/lib/terraria/instances`, and change no settings at all. For a folder on a disk that's already mounted, add a bind mount to `/etc/fstab`:
```
/srv/bigdisk/terraria /var/lib/terraria/instances none bind 0 0
```
```bash
sudo systemctl stop 'terraria@*' # if servers are running
sudo cp -a /var/lib/terraria/instances/. /srv/bigdisk/terraria/ # copy existing instances over
sudo mount /var/lib/terraria/instances
sudo chown terraria:terraria /var/lib/terraria/instances
sudo chmod 0750 /var/lib/terraria/instances
```
The old copies stay hidden underneath the mount. Once everything works, you can free that space: `sudo umount /var/lib/terraria/instances`, delete the old instance folders, and `sudo mount /var/lib/terraria/instances` again.
The same works for `dist/`. Use a mount, not a symbolic link: systemd follows a link to where it really points, and that place isn't covered by the sandbox rules.
**Or change the setting, and add a drop-in.** Set `TERRARIA_BASE` and/or `TERRARIA_DIST` in `/etc/default/terractl`, create the folders with the same owners and modes as the default ones, and let the servers write to the new instances folder:
```bash
sudo install -d -o terraria -g terraria -m 0750 /srv/terraria/instances
sudo install -d -o root -g terraria -m 2775 /srv/terraria/dist
sudo systemctl edit terraria@.service
```
```ini
[Service]
ReadWritePaths=/your/new/path
ReadWritePaths=/srv/terraria/instances
```
A zips folder outside `/home` needs no drop-in, because the servers only read it.
`terractl list`, `new`, `import` and `enable` check that the services can reach both folders, and print the exact drop-in to add if they can't. A server that can't create its folder says so in its log, with the same advice. If the daemon for other hosts (`terractld.service`) is on, it's checked too; it only needs a drop-in for folders under `/home` (see below).
### Instances or zips under `/home`
This works, but needs more changes, because the services are set up to never touch home folders (`ProtectHome=yes`). That hides `/home`, `/root` and `/run/user` from them completely. Prefer a folder outside `/home`, or a bind mount (above), if you can.
If they must be under `/home`:
1. **Let the services see `/home`, read-only, and write to the instances folder.** For the servers:
```bash
sudo systemctl edit terraria@.service
```
```ini
[Service]
ProtectHome=read-only
ReadWritePaths=/home/you/terraria/instances
```
If the daemon for other hosts is on, it also needs to write to the zips folder, because copying an instance here can bring its server zip along:
```bash
sudo systemctl edit terractld.service
```
```ini
[Service]
ProtectHome=read-only
ReadWritePaths=/home/you/terraria/instances /home/you/terraria/dist
```
`ProtectHome=read-only` lets the servers see all home folders. Normal file permissions still apply, so as the `terraria` user they can only read what other users can.
2. **Let the `terraria` user into the folders above.** On Ubuntu (21.04 and later) and some other systems, home folders are private (mode `750`), so the `terraria` user can't even pass through `/home/you`. Give it just that, without letting it list or read anything there:
```bash
sudo setfacl -m u:terraria:x /home/you
```
Do the same for every folder between `/home/you` and the instances or zips folder that isn't open to others. `sudo -u terraria ls /home/you/terraria/instances` tells you whether it worked.
3. **Create the folders** with the right owners and modes, and set `TERRARIA_BASE` and/or `TERRARIA_DIST` in `/etc/default/terractl`, as above.
4. **Restart running servers** so they pick up the drop-in: `sudo terractl restart <name>`. If the daemon is on: `sudo systemctl restart terractld`.
`terractl list` stops warning once the drop-ins match. Members of the `terraria` group also need to get through `/home/you` to use `list`, `info` and tab completion without `sudo`.
---
## Migrating from the manual install
@@ -948,6 +1018,9 @@ sudo cp -a /var/lib/terraria/instances/other/server/dotnet /var/lib/terraria/ins
sudo systemctl start terraria@<name>
```
**The server won't start: "Read-only file system" or "No such file or directory" for its folder.**
The instances folder was moved (`TERRARIA_BASE`) to where the service isn't allowed to write, or under `/home`. The log names the drop-in to add, and so does `terractl list`. See [Changing locations](#changing-locations).
**The server can't save, or you see "Permission denied" errors.**
Some files are owned by root, or have the wrong modes. Run `sudo terractl fix-perms <name>`.
@@ -1056,6 +1129,8 @@ Used by the service:
terractl prepare <name> Create dirs/configs, unpack zip if needed
terractl run <name> Run the server
terractl service-stop <name> <pid> Save and shut down via the console (service only)
terractl journal-access Let the terraria group read the servers' journal
(runs before every start; safe to run by hand)
terractl daemon serve Run the daemon (terractld.service)
```