rescriptum
Synology DSM 7
Guide
Synology DSM 7
Synology DSM 7
A Synology DS416j is why this project exists: ARMv7, 512 MB of RAM, DSM 7, no Docker. A static binary with no runtime is not an aesthetic preference there — it is the only thing that fits.
DSM 7 does run systemd, but it offers no supported place for a unit of your own: files in
/usr/lib/systemd/system are Synology’s, and a DSM update is free to replace them. The
supported route to a service is a package — install one and DSM generates
pkgctl-rescriptum.service from it. That is what this page leads with; the older
Task Scheduler route still works and is kept at the bottom.
Install the package
Download the .spk for your model from the
releases page:
| File | For |
|---|---|
rescriptum-<version>-armv7.spk | DS416j and other Marvell armada38x models |
rescriptum-<version>-x86_64.spk | every Intel model |
Not sure which? Ask the machine:
$ ssh admin@nas synogetkeyvalue /etc.defaults/synoinfo.conf unique
synology_armada38x_ds416j
Then Package Center → Manual Install, pick the file, and click through the warning that
the package is not verified by Synology. That warning is not about this package in
particular: DSM 7 removed third-party signing altogether and no longer offers a trust-level
setting, so every non-Synology package shows it. Our verification is the SHA-256 sum
published beside the .spk:
$ shasum -a 256 -c rescriptum-0.2.0-1-armv7.spk.sha256
The wizard asks two things — where the answers live and which port to listen on — and then the package:
- creates a
rescriptumshared folder and grants itself read/write access to it (if you already have one by that name, it is kept and simply gains the grant); - creates the
answersdirectory inside it at every start; - registers the port with the DSM firewall, so the service is selectable by name;
- links
rescriptum-cliinto/usr/local/bin; - starts at boot, and stops and starts from Package Center like anything else.
What the package does not do
Five things worth knowing before they surprise you.
- It cannot bind port 69 on its own. DSM 7 does not let an unsigned package run as root, so TFTP takes one root command from you, once — see TFTP needs one root command. Until it is given, the server warns, keeps answering and keeps serving media, and only the loader handoff is down.
- It does not open the firewall. Registering the port makes rescriptum appear by name in the rule editor instead of you typing a number. If your firewall is on with a default-deny rule, you still have to create the rule.
- It does not tell you about updates. There is no package source to poll — the
distribution model is: download the new
.spkfrom the releases page and install it by hand, for an upgrade as much as for a first install. Watch the releases. - A custom answers path is yours to permission. The package runs unprivileged and
cannot grant itself access to a folder you name; if you point it outside the
rescriptumshare, give therescriptumuser read access yourself. - The share’s permission is reapplied at every start. If you deliberately narrow it, you will find it restored the next time the package starts.
Where everything lives
| What | Where | Survives an upgrade | Survives uninstall |
|---|---|---|---|
binary, rescriptum-cli, the example env file | /var/packages/rescriptum/target/ | no — replaced | no |
| the env file | /var/packages/rescriptum/etc/rescriptum.env | yes | yes — see below |
| log, pidfile, captures | /var/packages/rescriptum/var/ | yes | yes |
| answers | /var/packages/rescriptum/shares/rescriptum/answers/ | yes | yes — always |
| the SQLite database, if you use one | beside the answers, in the same share | yes | yes — always |
Use the shares/ path rather than /volume1/…: it is a symlink DSM maintains, so it keeps
working on a NAS whose data is not on volume 1.
Uninstalling leaves the shared folder and everything in it alone. That is both DSM’s own behaviour and ours: when the store is SQLite, the database is your answers.
Uninstalling also leaves your configuration behind, and that is worth knowing.
etc/ and var/ are symlinks into /volume1/@appconf/rescriptum and
/volume1/@appdata/rescriptum, which DSM keeps — so the env file stays on the volume after
the package is gone, with whatever tokens are in it. Reinstalling picks it back up,
which is usually what you want. If you are removing rescriptum for good and it held a
token, delete /volume1/@appconf/rescriptum yourself.
The desktop application
The package installs an application on the DSM desktop — the icon is in the main menu, and Package Center’s Open button leads to it. It is a real DSM application, built on the desktop’s own UI framework, so it is in the DSM theme and in the DSM language; the French of a French DSM is the application’s French too.
It has three tabs:
- Settings — every configuration variable, as a form. Each field says where its value comes from, and a value the environment sets is shown but locked, because editing the file would not change it. Saving writes the file and offers to restart the package, since the server reads its configuration once, at startup.
- Status — the version, whether the package is running, the answers folder and whether
it is really readable by the service’s own user, and the output of
check. - Log — the last lines of the request log and of
startup.log.
Three properties are worth knowing rather than discovering:
- It edits the file, not the running server. So it still works when the server will not start, which is exactly when a settings panel earns its place. A change that would leave the server unable to start is refused before anything is written, with the reason shown.
- It never shows you a token.
RESCRIPTUM_ANSWER_TOKENandRESCRIPTUM_ADMIN_TOKENappear as set or not set, and an empty box means “leave it alone” rather than “clear it”. Typing a new one replaces it. - It requires a DSM administrator. Being signed in to DSM is not enough. See security for why that check is the whole door.
Restart now stops and starts the package through DSM itself, so DSM closes the window while it does — open it again to see the new state. The application says so next to the button rather than letting it surprise you.
It needs DSM 7.1 or newer (os_min_ver="7.1-42661"). It is built on DSM’s ExtJS
framework, which is present on 7.1.1 and on 7.2.2 — both measured. DSM 7.2 ships a newer
Vue framework and Synology’s current guide documents only that one; the DS416j this project
exists for is capped at 7.1.1, where Vue is undefined, so ExtJS is what covers every DSM
this package supports rather than only the recent ones. 7.0 is not claimed because nothing
has been run there.
Configuring it
The application above is the comfortable way. Everything it does can also be done from a shell, and on a machine where the desktop is not to hand that is the faster route:
$ sudo rescriptum-cli config
env file: /var/packages/rescriptum/etc/rescriptum.env
RESCRIPTUM_STORE files default
RESCRIPTUM_ANSWERS_DIR /var/packages/rescriptum/shares/rescriptum/answers file
RESCRIPTUM_LISTEN_ADDR 0.0.0.0:8000 file
…
$ sudo rescriptum-cli config set RESCRIPTUM_LOG=problems
wrote /var/packages/rescriptum/etc/rescriptum.env
config set keeps the file’s comments, uncomments a setting rather than duplicating it,
and refuses a change that would stop the server starting. Its exit code says whether
the configuration is one the server would start on, which makes it usable from a script.
Underneath both is the same file, and editing it by hand is still perfectly reasonable:
$ sudo vi /var/packages/rescriptum/etc/rescriptum.env
postinst writes it complete on a fresh install, with the variables in use uncommented and
the rest commented with a line saying what they do. Stop and start the package from
Package Center to apply a change — the server reads the file at every start.
An upgrade never touches it. The complete example for the version you have is at
/var/packages/rescriptum/target/etc/rescriptum.env.example, rewritten on every install
and upgrade, which is how a new variable becomes visible without disturbing your live file.
Every variable is in the configuration reference.
The file is chmod 600 and owned by the package user. It is where
RESCRIPTUM_ANSWER_TOKEN and RESCRIPTUM_ADMIN_TOKEN live, and — being under etc/ — it
is a plausible passenger in a DSM configuration backup. Worth knowing rather than
discovering.
The admin API is off by default and, when you enable it, should stay on
loopback and be reached through an SSH tunnel; it is deliberately not registered with the
firewall. It also requires RESCRIPTUM_STORE=sqlite and a token of at least 16 characters,
both of which are startup errors — so getting them wrong shows up as a package that
will not start, with the reason in /var/log/packages/rescriptum.log.
Putting answers in place
Drop files into the rescriptum shared folder’s answers directory, over File Station or
over SSH, exactly as you would anywhere else — see writing answers.
Then validate them as the package user:
$ sudo -u rescriptum rescriptum-cli check
The sudo -u matters. Run as root it succeeds whatever the shared folder’s permissions
say, which makes a successful run meaningless. rescriptum-cli is the packaged wrapper: it
names the env file, so check and render look at this machine’s answers rather than at
/srv/answers.
The firewall
Control Panel → Security → Firewall — create a rule allowing rescriptum from your provisioning network. The service appears by name because the package registered its port.
DSM’s firewall is the single most common reason a machine “never contacts the server”.
If you change the port later, edit RESCRIPTUM_LISTEN_ADDR in the env file and then move
the firewall entry, which does not follow by itself:
$ sudo /usr/syno/sbin/synopkghelper update rescriptum port-config
Serving installer media, and PXE
The package can also serve the installer itself — kernels, initrds and images — from the same NAS that decides the answer. It is off until you turn it on:
- Uncomment
RESCRIPTUM_MEDIA_DIRin the env file and restart the package. - Drop an ISO into the
rescriptumshare’smediafolder, over File Station or SMB. - Register it, so it is verified and probed once rather than per request:
$ rescriptum-cli media add /volume1/rescriptum/media/proxmox-ve_8.4-1.iso \
--sha256 9f86d081884c7d65…
$ rescriptum-cli media list
The media listener is on port 8001, already registered with the firewall alongside the answer port — you still have to create the rule.
No image ships with the package, and none ever will: an ISO is somebody else’s artefact, gigabytes, on its own schedule. That folder is where you keep them, and it is the archive — nothing here modifies an image after it lands. Preparing a Proxmox image produces a two-hundred-byte sidecar and an injection applied on the wire, so the bytes on disk stay exactly what Proxmox published and their checksum stays verifiable against Proxmox’s own. See Serving boot media.
TFTP needs one root command
rescriptum is the TFTP server here, not DSM. Port 69 is privileged and DSM 7 refuses to let an unsigned package run as root, so the package cannot grant itself the port — but it does not need root to use it, only to be given permission once:
$ sudo setcap cap_net_bind_service=+ep /volume1/@appstore/rescriptum/bin/rescriptum
$ sudo synopkg restart rescriptum
After that the package binds udp/69 as its own unprivileged rescriptum user, alongside
8000 and 8001. All three are registered with the firewall.
Make it durable, because an upgrade drops it. Installing a new version replaces the
binary, and file capabilities belong to the file — so the capability goes with the old one.
Control Panel → Task Scheduler → Create → Triggered Task → User-defined script, user
root, event Boot-up, with the setcap line as the script. Run it once from that page
after every upgrade, or reboot.
Nothing else breaks while it is missing. A TFTP port that cannot be bound is the one
listener in this server whose failure is not fatal, deliberately: answers are the product,
and an upgrade must not take a fleet’s installs down to report that a second port could not
be opened. What you get instead is a warning in the log, a tftp: line in the settings
panel’s Status tab, and:
$ rescriptum-cli boot check
BROKEN nothing answers on 0.0.0.0:69 and it cannot be bound either: Permission denied.
Port 69 is privileged: run as root and set RESCRIPTUM_USER to drop afterwards, or grant
the binary cap_net_bind_service with setcap — the server still answers and still serves
media, but a machine sent here by DHCP asks for a loader and gets nothing
Note it asks the port for a loader rather than trying to bind it. Binding proves the opposite of what it looks like: a bind that succeeds means nothing is listening.
The loaders are in the package. The share’s boot folder arrives filled the first
time you start it, and an upgrade refreshes them — there is no second download. They are
iPXE, GPLv2, separate files served alongside rather than linked into anything, and the
NOTICE beside them names the exact upstream commit they were built from.
$ rescriptum-cli boot check
ok 0.0.0.0:69 handed over ipxe-undionly.kpxe
Replacing them is possible but not by editing that folder — an upgrade rewrites the
filenames this package ships. Point RESCRIPTUM_BOOT_DIR somewhere else instead, and
nothing here will ever write to it.
Then point DHCP at this NAS — Control Panel → DHCP Server → PXE if the NAS serves DHCP, or your own server with what this prints:
$ rescriptum-cli boot dhcp-snippet --format dnsmasq
If you would rather not use setcap
RESCRIPTUM_TFTP_ADDR takes an unprivileged port, which needs no capability at all — your
DHCP server has to be told, since a PXE ROM has 69 burned into it and only a chainloading
first stage can be redirected. Or set it to off and let another daemon on this NAS hand
the loader over; DSM has its own TFTP server under Control Panel → File Services →
Advanced, pointed at the share’s boot folder. Both are workarounds for a deployment that
wants them, not what the package expects.
The Images tab
The application has a fourth tab, and it is where installer images are managed without touching a terminal: what is held, a catalogue to pick from, and a URL field for anything the catalogue does not offer.
The catalogue is not a list this package ships. Each entry names the checksum index the vendor already publishes beside its own images; picking one reads that index over the network, so the versions offered are whatever the vendor has today and the digest that gets verified is theirs. That also means the tab needs the NAS to reach the internet — the only part of this package that does.
A download of a 1.5 GB image cannot be held open by a web request, so the tab starts it and
follows it: media add writes into a .part file beside its destination and renames it
only once the digest checks out, so the partial file’s size is the progress and its
disappearance is the completion. Closing the window does not stop the download.
Preparing a Proxmox image is a button there too. Every other family takes its answer’s URL on the kernel command line, so there is nothing to prepare and the tab says so rather than offering a step that would do nothing.
One setting worth filling in
RESCRIPTUM_PUBLIC_HOST=192.168.1.10
Every generated script names this address. Left empty it is derived by asking the routing table, and the settings panel shows what that came out as rather than an empty box — so on a NAS with one interface there is nothing here to fill in.
A NAS with two is the case worth reading. The derived answer is one of them, and the startup log names the others beside it:
warning: RESCRIPTUM_PUBLIC_HOST is not set — derived 192.168.1.10, which is what every
generated URL will name. This host also has 10.0.0.10. If the machines reach it on one of
those instead, set it explicitly.
Getting it wrong produces a machine that boots, chains, and hangs on an address that does not exist, which is a slow thing to diagnose from the machine’s end.
The log
RESCRIPTUM_LOG_FILE points the server at /var/packages/rescriptum/var/rescriptum.log,
and the package installs a logrotate stanza for it — weekly, eight kept, copytruncate
(the server opens its log once and never reopens it, so anything else would silently end
logging). Beside it, var/startup.log holds what the server says before it knows where its
log lives: a configuration error, a malformed env file.
Once a rollout is routine, RESCRIPTUM_LOG=problems keeps the failures and drops the
successful answers, which are the only high-volume thing in there.
When it will not start
Three places say why, in this order:
$ cat /var/log/packages/rescriptum.log # the package scripts' own output
$ cat /var/packages/rescriptum/var/startup.log # what the server said before it had a log
$ cat /var/packages/rescriptum/var/rescriptum.log
$ systemctl status pkgctl-rescriptum # what DSM's service manager saw
A refused configuration — an admin token under 16 characters, a store that cannot be
opened — is reported after the server knows where its log lives, so it lands in
rescriptum.log; a malformed env file is reported before, and lands in startup.log. The
package’s start prints the tail of both when the server exits immediately, so Package
Center shows you the reason rather than only the failure.
DSM does not restart the process if it dies. The unit it generates is Type=oneshot
with RemainAfterExit=yes and no Restart=, so a server that exits stays stopped until you
start it from Package Center. That is not a regression — the Task Scheduler route did not
restart it either — but it is worth knowing before you rely on it.
A package that installs, starts, and then answers 404 to everything is almost always the
answers directory: check sudo -u rescriptum rescriptum-cli check. On a NAS with an
encrypted shared folder, that is also what a boot before the volume is unlocked looks like
— unlock it and restart the package.
Verify
$ curl http://NAS_IP:8000/health
OK
Without the package
The manual route still works, and is the honest choice if you would rather not install a package at all.
Use the armv7-unknown-linux-gnueabihf build (or x86_64-unknown-linux-musl, or
aarch64-unknown-linux-musl for a newer ARM model) from the
releases page, or cross-compile one yourself
(see building).
$ scp rescriptum admin@nas:/volume1/netboot/rescriptum
$ ssh admin@nas chmod +x /volume1/netboot/rescriptum
$ ssh admin@nas mkdir -p /volume1/netboot/answers
If ARMv7 misbehaves, confirm the real architecture before assuming:
$ ssh admin@nas uname -m
armv7l
Take the ARMv7 build, not a musl one you built yourself. The published armv7 binary
is linked against glibc 2.17, which DSM has; a musl build of the same code installs, answers
--version, and then dies the moment it wants the time. Synology’s 3.10 kernels answer the
time64 syscalls with EINVAL rather than ENOSYS, and musl 1.2 only falls back on
ENOSYS — the build page
has the measurement. The x86_64 and aarch64 builds are static musl and unaffected.
$ file rescriptum
ELF 32-bit LSB pie executable, ARM, EABI5 version 1 (SYSV), dynamically linked, ...
RESCRIPTUM_ANSWERS_DIR defaults to /srv/answers, which does not exist on DSM, so set it
explicitly. The env file below is the tidiest place to do that.
Control Panel → Task Scheduler → Create → Triggered Task → User-defined script
| Field | Value |
|---|---|
| Event | Boot-up |
| User | root |
| Command | see below |
If you use a token, do not put it in that box. Anything in a process’s arguments — and
in DSM’s case, in the task definition — is readable by every user on the machine through
ps. Put the configuration in a root-only file and name it instead:
# /volume1/netboot/rescriptum.env (chmod 600, owned by root)
RESCRIPTUM_ANSWERS_DIR=/volume1/netboot/answers
RESCRIPTUM_LOG_FILE=/volume1/netboot/rescriptum.log
RESCRIPTUM_STORE=sqlite
RESCRIPTUM_DB_PATH=/volume1/netboot/answers.db
RESCRIPTUM_ADMIN_ADDR=127.0.0.1:8001
RESCRIPTUM_ADMIN_TOKEN=…
RESCRIPTUM_ANSWER_TOKEN=…
# the Task Scheduler entry runs this
RESCRIPTUM_ENV_FILE=/volume1/netboot/rescriptum.env exec /volume1/netboot/rescriptum
Prefer this to sourcing it. The older form —
. /volume1/netboot/rescriptum.env && exec … — works, and still does, but it fails
silently: drop the leading ., mistype a line, or get the permissions wrong, and the
shell sources nothing while the server comes up on its defaults — the default answers
directory, no admin token, and not a word about it in the log. With RESCRIPTUM_ENV_FILE
the binary reads the file itself and refuses to start if it cannot. It also warns if
the file is readable by anyone but root, and names any key it does not recognise, so a
RESCRIPTUM_ADMIN_TOKENN is caught rather than quietly ignored.
Details of the format are in the configuration reference.
Run the task once by hand from the Task Scheduler rather than waiting for a reboot to find out it does not work. Then open the port in the firewall by number, and rotate the log yourself — the server does not, and nothing else will either.
Replacing a running instance
$ ./deploy.sh admin@nas
It builds for ARMv7, checks the answers first, copies the
binary under a temporary name so a half-copied file is never executed, restarts it, and
confirms /health responds. Details in
deployment.
The Task Scheduler entry is still what starts it after a reboot — deploy.sh only replaces
what is running now. On a packaged install, use Package Center instead.
Shutdown
Both routes send SIGTERM, which the server handles: it stops accepting and exits. There
is no state to lose either way.
What to expect from a DS416j
512 MB and an ARMv7 core is not much, and it does not need to be. Measured on a DS416j running the package, over the LAN: 3–4 ms to compose and serve an answer, network round trip included, for a machine claimed by a group and merged with its own file. A connection costs kilobytes rather than a thread, the directory listing is cached and invalidated by mtime rather than walked per request, and a group with no per-machine overrides is rendered once at load and served afterwards as a prepared string.
The one thing worth knowing: filesystem work happens on a blocking thread pool, because
read_dir on a NAS with a sleeping disk is not a fast call, and blocking an async worker
would stall every other connection it was driving.