Recently Written · git

9p-store

Small disk-backed 9P file server (diod) for a private network

git clone https://github.com/equwal/9p-store

Log | Files | Refs


commit e3b11be0cb5f58ffdf99a3a3fbcf9c4991a45d40
equwal <13551856+equwal@users.noreply.github.com>
2026-10-04 03:27:43 -0700

Initial version

 9pstore.service.in | 17 ++++++++++++++
 LICENSE            | 21 +++++++++++++++++
 docs/USAGE.md      | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 install.sh         | 27 ++++++++++++++++++++++
 4 files changed, 131 insertions(+)
diff --git a/9pstore.service.in b/9pstore.service.in
new file mode 100644
index 0000000..b9ca2a7
--- /dev/null
+++ b/9pstore.service.in
@@ -0,0 +1,17 @@
+# 9P file store served by diod. install.sh fills in the @...@ values.
+# No 9P auth: the private-address bind and the firewall are the gate.
+# Every client user maps to @SQUASH_USER@.
+[Unit]
+Description=9P file store (diod)
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+ExecStart=/usr/sbin/diod -f -n -S -U @SQUASH_USER@ -e @EXPORT_DIR@ -l @LISTEN_ADDR@:@PORT@ -l 127.0.0.1:@PORT@ --nwthreads 2 -L stderr
+Restart=on-failure
+RestartSec=5
+MemoryMax=64M
+MemoryHigh=48M
+
+[Install]
+WantedBy=multi-user.target
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..777c6b0
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 equwal
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/docs/USAGE.md b/docs/USAGE.md
new file mode 100644
index 0000000..07a9a82
--- /dev/null
+++ b/docs/USAGE.md
@@ -0,0 +1,66 @@
+# 9p-store
+
+A small, disk-backed 9P file server for a private network. It uses diod
+(Debian package) because plan9port has no persistent disk server: its ramfs
+keeps files in RAM only. diod idles at under 1 MB of memory.
+
+## Server
+
+```sh
+LISTEN_ADDR=10.0.0.1 ./install.sh
+```
+
+The unit listens on `$LISTEN_ADDR` and on 127.0.0.1 (for SSH tunnels), with a
+64 MB memory cap and restart on failure.
+
+### Security
+
+diod runs with no 9P authentication (`-n`). Access control is the network:
+
+- Bind only to a private address, for example a WireGuard interface.
+- Drop traffic to that address unless it arrives on the private interface:
+
+  ```sh
+  nft add rule inet filter input ip daddr 10.0.0.1 iifname != "wg0" drop
+  ```
+
+- Check that nothing listens on a public address: `ss -ltnp | grep :564`.
+- Never store secrets in the export.
+
+Hosts outside the private network can reach the loopback listener through an
+SSH forward limited with `permitopen="127.0.0.1:564"` in `authorized_keys`.
+
+## Clients
+
+### Linux kernel (v9fs)
+
+Needs `CONFIG_9P_FS` and `CONFIG_NET_9P` (`modprobe 9p`).
+
+```sh
+mount -t 9p -o trans=tcp,port=564,aname=/srv/9pstore,version=9p2000.L,access=any 10.0.0.1 /mnt/9pstore
+```
+
+Through an SSH tunnel:
+
+```sh
+ssh -fN -L 127.0.0.1:5640:127.0.0.1:564 tunnel@server
+mount -t 9p -o trans=tcp,port=5640,aname=/srv/9pstore,version=9p2000.L,access=any 127.0.0.1 /mnt/9pstore
+```
+
+### plan9port
+
+```sh
+9p -a tcp!10.0.0.1!564 -A /srv/9pstore ls /
+9p -a tcp!10.0.0.1!564 -A /srv/9pstore read /file.txt
+echo hi | 9p -a tcp!10.0.0.1!564 -A /srv/9pstore write /file.txt
+9pfuse -A /srv/9pstore 'tcp!10.0.0.1!564' /mnt/9pstore
+```
+
+### diod tools
+
+```sh
+diodls -s 127.0.0.1:564 -a /srv/9pstore
+diodcat -s 127.0.0.1:564 -a /srv/9pstore file.txt
+```
+
+Windows has no built-in 9P TCP client; use a Linux host or plan9port.
diff --git a/install.sh b/install.sh
new file mode 100755
index 0000000..9a1d3f1
--- /dev/null
+++ b/install.sh
@@ -0,0 +1,27 @@
+#!/bin/sh
+# Install a disk-backed 9P file server (diod) on Debian/Ubuntu.
+# Usage: LISTEN_ADDR=<private-ip> ./install.sh
+#   LISTEN_ADDR  private address to serve on (for example a WireGuard address). Required.
+#   EXPORT_DIR   directory to serve (default /srv/9pstore)
+#   SQUASH_USER  account that owns every file (default 9pstore)
+#   PORT         TCP port (default 564)
+set -eu
+: "${LISTEN_ADDR:?set LISTEN_ADDR to a private address, never a public one}"
+EXPORT_DIR=${EXPORT_DIR:-/srv/9pstore}
+SQUASH_USER=${SQUASH_USER:-9pstore}
+PORT=${PORT:-564}
+
+DEBIAN_FRONTEND=noninteractive apt-get install -y diod
+systemctl disable --now diod 2>/dev/null || true   # Debian ships a sysv unit; use ours.
+id "$SQUASH_USER" >/dev/null 2>&1 ||
+  useradd -r -s /usr/sbin/nologin -d "$EXPORT_DIR" "$SQUASH_USER"
+mkdir -p "$EXPORT_DIR"
+chown "$SQUASH_USER": "$EXPORT_DIR"
+chmod 770 "$EXPORT_DIR"
+
+sed -e "s|@LISTEN_ADDR@|$LISTEN_ADDR|" -e "s|@EXPORT_DIR@|$EXPORT_DIR|" \
+    -e "s|@SQUASH_USER@|$SQUASH_USER|" -e "s|@PORT@|$PORT|" \
+    "$(dirname "$0")/9pstore.service.in" > /etc/systemd/system/9pstore.service
+systemctl daemon-reload
+systemctl enable --now 9pstore
+ss -ltn "sport = :$PORT"