commit f3a795ae57f8f568711b31d977aa777ee34f1187 from: Omar Polo date: Tue Aug 03 11:31:38 2021 UTC new posts commit - bb54dbe9255178380f815ef48bd279bd8a75cafd commit + f3a795ae57f8f568711b31d977aa777ee34f1187 blob - a29a4fecd7372fd1bfa72296b86ab3c576181d30 (mode 644) blob + /dev/null --- resources/posts/cgit-gitolite.md +++ /dev/null @@ -1,180 +0,0 @@ -I've just finished to configure gitolite and cgit to manage some git -repos of mine (and friends), so I'm posting here the setup before -forgetting the details. - -The final result is a git server with both a web view, HTTP clone and -ssh for you and your users. - -It requires more work than, say, gitea or gitlab, and has a few -moving parts. Nevertheless, it's a modular solution (you can replace -cgit with gitweb for instance) and it does not have obnoxious web guis -to manage things. The whole gitolite config is itself a git -repository, so you can use the tools you're familiar with (a bit of -ssh, git and your preferred `$EDITOR`) to build and maintain your own -git server. - -## gitolite - -Install gitolite, it's easy. Follow the [installation -guide](https://gitolite.com/gitolite/quick_install#distro-package-install). -I've done that on a new user called "git". This will create two repos -in `~git/repositories`: `gitolite-admin` and `testing`. With the -default configuration testing will be read-write for all users (in the -gitolite sense). - -You should import your own ssh public key. Try to clone the -`gitolite-admin` repo with `git clone -git@your.own.host:gitolite-admin` to test the setup and, eventually, -add more users and repos. - -## cgit - -I'm using nginx plus fcgiwrap on a FreeBSD system, but other options -are available. (For instance, if you're using OpenBSD than you have -httpd and slowcgi already in base.) - -For reference, my configuration file is `/usr/local/etc/cgit-op.conf` -and contains: -```conf -css=/mine.css -logo=/logo.png - -head-include=/usr/local/lib/cgit/theme/head.html - -enable-http-clone=1 -enable-index-links=1 -remove-suffix=1 -enable-commit-graph=1 -enable-log-filecount=1 -enable-git-config=1 - -source-filter=/usr/local/lib/cgit/filters/syntax-high.py -about-filter=/usr/local/lib/cgit/filters/about-formatting.sh - -virtual-root=/ -enable-index-links=1 -enable-index-owner=0 -snapshots=tar.gz tar.bz2 -root-title=Stuff -root-desc=some git repos of mine -local-time=1 - -# path to the root about file -#root-readme=/usr/local/lib/cgit/theme/about.html - -# search for these files in the root fo the default branch -readme=:README.md -readme=:readme.md -readme=:README.mkd -readme=:readme.mkd -readme=:README.rst -readme=:readme.rst -readme=:README.html -readme=:readme.html -readme=:README.htm -readme=:readme.htm -readme=:README.txt -readme=:readme.txt -readme=:README -readme=:readme -readme=:INSTALL.md -readme=:install.md -readme=:INSTALL.mkd -readme=:install.mkd -readme=:INSTALL.rst -readme=:install.rst -readme=:INSTALL.html -readme=:install.html -readme=:INSTALL.htm -readme=:install.htm -readme=:INSTALL.txt -readme=:install.txt -readme=:INSTALL -readme=:install - -scan-path=/home/git/repositories -``` - -The important bits of all of these are only: -```conf -enable-git-config=1 -``` -and -```conf -scan-path=/home/git/repositories -``` - -The first let us configure per-repo cgit options via the standard git -config file, while the second lets cgit discovers the repos by -searching in that path. - -If you're curious, I used `head-include` to add some `` tags and -modified the default CSS to render the pages *decently* on mobile -screens. More work is needed. - -### Note about permissions - -You are probably running cgit with the `www` user and gitolite with -the `git` user so you have a permission problem. While you can do -fancy stuff with `mount_nullfs`/`mount --bind` and whatnot or by -changing the default path for the repositories, I didn't want to. - -I'm still not sure if this is the best way to handle things, but I -made fcgiwrap use the `git` user with -```conf -fcgiwrap_user="git" -``` -in `/etc/rc.conf` plus a manual `chown(8)` on the socket. Now cgit -and gitolite are run by the same user. Problem solved. - -## hide some repositories! - -This was the basic setup to have cgit display the repositories managed -by gitolite, as well as having both public HTTP and authenticated ssh -clone. Pretty neat. - -But, you have no way (still) to hide some repositories. For instance, -the `gitolite-admin` repositorie is public readable (not writable). -It may be fine for you, but I wanted a way to have *private* -repositories, while still having the repos managed by gitolite. - -If you set `enable-git-config` in cgit configuration file, now you can -control some cgit per-repo options via -`~git/repositories/$REPO/config`. You can create a section that looks -like this: -```conf -[cgit] - ignore = 1 -``` -to make cgit ignore that repo. Check the documentation of cgit for -the list of parameters you can set. - -But it's tedious and needs manual work per-repo. That's something -that needs to be automatized. - -Fortunately, gitolite lets us set git configurations via the -`gitolite.conf` file. You first need to set `GIT_CONFIG_KEYS` to -`'.*'` in `~git/.gitolite.rc`. (`.*` is the broader, probably -`cgit.*` is enough, haven't tested tho). - -Now, in your `gitolite.conf` you can -```conf -repo gitolite-admin - config cgit.ignore=1 -``` -and BOOM, it's hidden and unreachable via cgit (both via web and http -clone). - -But (there are too many 'but' in this section, hu?) we can do even -better: -```conf -@hiddenrepos = gitolite-admin -@hiddenrepos = private-stuff -@hiddenrepos = next-gen-revolutionary-stuff - -repo @hiddenrepos - config cgit.ignore=1 -``` -to bulk-hide repositories. - -Neat. blob - /dev/null blob + 4881dfd80723a92d6938a4481ff6e04033351a0e (mode 644) --- /dev/null +++ resources/posts/cgit-gitolite.gmi @@ -0,0 +1,153 @@ +I've just finished to configure gitolite and cgit to manage some git repos of mine (and friends), so I'm posting here the setup before forgetting the details. + +The final result is a git server with both a web view, HTTP clone and ssh for you and your users. + +It requires more work than, say, gitea or gitlab, and has a few moving parts. Nevertheless, it's a modular solution (you can replace cgit with gitweb for instance) and it does not have obnoxious web guis to manage things. The whole gitolite config is itself a git repository, so you can use the tools you're familiar with (a bit of ssh, git and your preferred $EDITOR) to build and maintain your own git server. + +## gitolite + +Install gitolite, it's easy, just follow the installation guide. I've done that on a new user called "git". This will create two repos in ~git/repositories: gitolite-admin and testing. With the default configuration testing will be read-write for all users (in the gitolite sense). + +=> https://gitolite.com/gitolite/quick_install#distro-package-install Gitolite install guide + +You should import your own ssh public key. Try to clone the “gitolite-admin” repo with: + +> git clone git@your.own.host:gitolite-admin + +to test the setup and, eventually, add more users and repos. + +## cgit + +I'm using nginx plus fcgiwrap on a FreeBSD system, but other options are available. (For instance, if you're using OpenBSD than you have httpd and slowcgi already in base.) + +For reference, my configuration file is /usr/local/etc/cgit-op.conf and contains: + +``` cgit configuration file +css=/mine.css +logo=/logo.png + +head-include=/usr/local/lib/cgit/theme/head.html + +enable-http-clone=1 +enable-index-links=1 +remove-suffix=1 +enable-commit-graph=1 +enable-log-filecount=1 +enable-git-config=1 + +source-filter=/usr/local/lib/cgit/filters/syntax-high.py +about-filter=/usr/local/lib/cgit/filters/about-formatting.sh + +virtual-root=/ +enable-index-links=1 +enable-index-owner=0 +snapshots=tar.gz tar.bz2 +root-title=Stuff +root-desc=some git repos of mine +local-time=1 + +# path to the root about file +#root-readme=/usr/local/lib/cgit/theme/about.html + +# search for these files in the root fo the default branch +readme=:README.md +readme=:readme.md +readme=:README.mkd +readme=:readme.mkd +readme=:README.rst +readme=:readme.rst +readme=:README.html +readme=:readme.html +readme=:README.htm +readme=:readme.htm +readme=:README.txt +readme=:readme.txt +readme=:README +readme=:readme +readme=:INSTALL.md +readme=:install.md +readme=:INSTALL.mkd +readme=:install.mkd +readme=:INSTALL.rst +readme=:install.rst +readme=:INSTALL.html +readme=:install.html +readme=:INSTALL.htm +readme=:install.htm +readme=:INSTALL.txt +readme=:install.txt +readme=:INSTALL +readme=:install + +scan-path=/home/git/repositories +``` + +The important bits of all of these are only: +``` enable git configuration +enable-git-config=1 +``` + +and + +``` set the parameter “scan-path” to repositories inside the git user home. +scan-path=/home/git/repositories +``` + +The first let us configure per-repo cgit options via the standard git config file, while the second lets cgit discovers the repos by searching in that path. + +If you're curious, I used ‘head-include’ to add some meta tags and modified the default CSS to render the pages *decently* on mobile screens. More work is needed. + +### Note about permissions + +You are probably running cgit with the www user and gitolite with the git user, so you have a permission problem. While you can do fancy stuff with mount_nullfs, ‘mount --bind’ and whatnot or by changing the default path for the repositories, I didn't want to. + +I'm still not sure if this is the best way to handle things, but I made fcgiwrap use the `git` user with + +```set fcgiwrap user to git +fcgiwrap_user="git" +``` + +in `/etc/rc.conf` plus a manual `chown(8)` on the socket. Now cgit and gitolite are run by the same user. Problem solved. + +## hide some repositories! + +This was the basic setup to have cgit display the repositories managed by gitolite, as well as having both public HTTP and authenticated ssh clone. Pretty neat. + +But, you have no way (still) to hide some repositories. For instance, the ‘gitolite-admin’ repository is public readable (not writable). It may be fine for you, but I wanted a way to have *private* repositories, while still having the repos managed by gitolite. + +If you set ‘enable-git-config’ in cgit configuration file, now you can control some cgit per-repo options via `~git/repositories/$REPO/config`. You can create a section that looks like this: + +```conf +[cgit] + ignore = 1 +``` + +to make cgit ignore that repo. Check the documentation of cgit for the list of parameters you can set. + +But it's tedious and needs manual work per-repo. That's something that needs to be automatized. + +Fortunately, gitolite lets us set git configurations via the gitolite.conf file. You first need to set ‘GIT_CONFIG_KEYS’ to ‘.*’` in ~git/.gitolite.rc. (‘.*’ is the broader, probably ‘cgit.*’ is enough, haven't tested tho). + +Now, in your `gitolite.conf` you can + +```conf +repo gitolite-admin + config cgit.ignore=1 +``` + +and BOOM, it's hidden and unreachable via cgit (both via web and http clone). + +But (there are too many “but” in this section, hu?) we can do even better: + +```conf +@hiddenrepos = gitolite-admin +@hiddenrepos = private-stuff +@hiddenrepos = next-gen-revolutionary-stuff + +repo @hiddenrepos + config cgit.ignore=1 +``` + +to bulk-hide repositories. + +Neat. blob - /dev/null blob + 05a4f4f10a52c78f61a29cd7b8741505d5c8521d (mode 644) --- /dev/null +++ resources/posts/change-tty-colors.gmi @@ -0,0 +1,52 @@ +Last week I spent a couple of days at my relative house. The only thing I took with me was a raspberry, plugged to the TV. + +The raspberry was running void linux, with only a small selection of software installed on it (Emacs, git, a C toolchain and nothing more, neither X11), and it was refreshing! Most of the time the system was offline so I could focus on writing code, with only some occasional trips to man.openbsd.org with w3m to read some decent manpages. + +(w3m is quite a fine browser, maybe I’ll try to create something akin to it for Gemini.) + +The thing is, I am one of those strange people who doesn’t like dark colorschemes. “Black print on white paper, as God and Gutenberg intended.” Unfortunately the linux ttys are white text on a black background. Let’s fix that! + +I found (and forgot the link) that linux allows one to customize the colors via ANSI escape codes and also set the default foreground and background. + +The “template” for these codes are: + +```sh +\033 ] P +``` + +(spaces only for readability) + +where ‘\033’ is the escape character, index is a one hex digit (0-F) and ‘html-hex-color’ is the familiar “HTML-style” six hexadecimal digit color. + +I’m using the following colors, but you can customize them to match your preferred scheme: + +```sh +printf "\033]P0000000" #black +printf "\033]P1803232" #darkred +printf "\033]P25b762f" #darkgreen +printf "\033]P3aa9943" #brown +printf "\033]P4324c80" #darkblue +printf "\033]P5706c9a" #darkmagenta +printf "\033]P692b19e" #darkcyan +printf "\033]P7ffffff" #lightgrey +printf "\033]P8222222" #darkgrey +printf "\033]P9982b2b" #red +printf "\033]PA89b83f" #green +printf "\033]PBefef60" #yellow +printf "\033]PC2b4f98" #blue +printf "\033]PD826ab1" #magenta +printf "\033]PEa1cdcd" #cyan +printf "\033]PFdedede" #white +``` + +Then, it’s possible to change the foreground and background as usual, but there’s an extra escape code to “persist” the combination: + +```sh +# set the default background color (47, aka white) and the default +# foreground color (30, aka black), then store it (aka [8]) +printf '\033[47;1;30m\033[8]' +``` + +Here’s the outcome: + +=> /img/linux-bright-colorscheme.jpg Bright tty, screenshot (282K) blob - /dev/null blob + f8bc915b1f906d21bba9ce0fad9878af1bac05a8 (mode 644) --- /dev/null +++ resources/posts/gemtext-clojure.gmi @@ -0,0 +1,44 @@ +Some time ago I wrote a text/gemini parser for Clojure. More than a real parser was a hack, an extremely verbose one, just to play with this transducer thing. + +Well, I got tired of that and rewrote it as a standalone library that is now available on clojars. + +=> https://clojars.org/com.omarpolo/gemtext/ com.omarpolo/gemtext on Clojars. + +There’s still a transducer at the heart of the library, but it’s a more reasonable one this time. It allows to build up pipelines where parsing text/gemini is only one of the steps, or to parse streaming text/gemini, which is a cool thing (even tho not widespread.) + +Since this is a library and not a hack inside this blog codebase, ‘parse’ is now a multimethod (Clojure own generic functions if you came from CL-land) with default implementations for strings, sequence of strings and java Reader. + +Since the parser is built using nothing more than the clojure stdlib, I thought “why not” and called the file ‘core.cljc’, so it’s available also for ClojureScript. (The beforementioned multimethod is available also there, with a default implementation for vectors, lists and strings.) + +The library emits “almost usual” hiccup: + +```clojure +user=> (gemtext/parse "some\nlines\nof\ntext") +[[:text "some"] [:text "lines"] [:text "of"] [:text "text"]] + +user=> (gemtext/parse (repeat 3 "* test")) +[[:item "test"] [:item "test"] [:item "test"]] +``` + +and is able to turn ’em back to strings: + +```clojure +user=> (gemtext/unparse [[:link "/foo" "A link"]]) +"=> /foo A link\n" +``` + +but also to return “HTML” hiccup + +```clojure +user=> (gemtext/to-hiccup [[:header-1 "text/gemini"] [:text "..."]]) +[[:h1 "text/gemini"] [:p "..."]] +``` + +so you can use it with other Clojure/script libraries, and to convert text/gemini to HTML. + +It was fun: I use clojure a lot, but never actually wrote a library, so this was a chance to play with different things. First of, the (small) documentation is available also on cljdoc, and second I played with ‘seancorfield/readme’, a Clojure library that transforms your README to a REPL session! + +A final note: the design is done, but in the following weeks I may slighly change something here and there (for instance, only now I realize that you can parse text/gemini on the fly, but not convert it to HTML one bit at a time, i.e. “convert text/gemini to html streamingly” (?) which can be useful, or unparse into anything other than a string.) + + +(P.S. I took the chance to also to restyle the capsule. I removed the ASCII banners and followed the subscription spec, yay!) blob - /dev/null blob + 9450975fa21b2130170a9fcf62d4c13995f12690 (mode 644) --- /dev/null +++ resources/posts/gmid-1.6.gmi @@ -0,0 +1,52 @@ +It took me a while to release this 1.6 version compared to the previous ones, but we’re finally here! + +The headlines for this version are an improved CGI implementation and performance, but you’ll find the full changelog at the end of this entry. + +libevent is now a dependency of gmid: the new event-loop should be faster than the old poll(2)-based one. + +Thanks to a clear design and privilege-separation, it was easy to spawn multiple server processes: this increases the performance and prevents delays. Three server processes are run by default, but the actual number it’s tunable via the new global ‘prefork’ option. + +The configuration file was enriched also with some other additions: +* ‘block return’ and ‘strip’ allows to define dynamic redirects and/or error pages +* ‘entrypoint’ forwards every request for a virtual host to a CGI script +* ‘log’ allows to control logging per virtual host +* ‘require client ca’ allows to restrict part of a virtual-host only to clients that provides a certificate signed by a specific CA + +Unfortunately, there are also a couple of breaking changes. I had to change the CGI environment variables so they match the CGI specification. The good news is that now CGI scripts are a bit more portable and that these breaking changes were done early in this release cycle, so if you started using gmid after the 1.5 release chances are that you’re already using these new variables. + +In particular: +* QUERY_STRING is always percent-encoded +* PATH_INFO and PATH_TRANSLATED always starts with a forward slash (/) +* some variables have been renamed. + +I set up a testing page that shows the various variables: + +=> gemini://gemini.omarpolo.com/cgi/env CGI test page + + +## v1.6 “Stargazers” Changelog + +### New features + +* reload configuration on SIGHUP without disconnecting the clients +* added ‘block return’, ‘strip’ and ‘entrypoint’ options +* added a ‘prefork’ option to control the number of server processes (default: 3) +* added ‘require client ca’ option to require client certificates signed by a specific CA +* added ‘log’ option to enable/disable logging per-vhost +* define TLS_CLIENT_NOT_BEFORE and TLS_CLIENT_NOT_AFTER for CGI scripts + +### Improvements + +* improved the directory listing: print the path of the current directory +* for CGI scripts, split the query in words and pass each of them via argv too +* [FreeBSD] add capsicum to the logger process + +### Bug fixes + +* CGI scripts now have only std{in,out,err} open +* change some CGI variables to match the correct behaviour + +### Breaking changes + +* relative paths are not allowed in the configuration file +* some environment variables for CGI script were changed. blob - /dev/null blob + 84a3d40f1acc8e7940cc5861744122367e9d1299 (mode 644) --- /dev/null +++ resources/posts/gmid-1.7.gmi @@ -0,0 +1,66 @@ +Early today, this hot Saturday morning, I tagged a new version of gmid. As always, this release is dubbed after a song, this time it’s one of my favourites by Dream Theater: “Space-dye Vest”. It has nothing to do with space other than the title, but it’s a really good song. It’s also one of their saddest songs, you have been warned. + +This 1.7 brings in a lot of new stuff, improvements and bugfixes. One of the most interesting things is, in my opinion, the initial FastCGI work. I think FastCGI could work really well in a Gemini context, as it’s a easy way to have servers that acts like reverse proxies and forward the requests to backends application. It’s better than a TLS-relay because it can forward information about client certificates to the application, something that’s impossible otherwise, and it’s lighter too! + +Another interesting feature is that it’s now possible to specify the ‘root’ directory per-location block, that along with the improved handling of ‘strip’ allows really flexible setups like ‘~user’ directories, for instance. This feature in particular was inspired by a concern raised by cage on #gemini-it over at libera.chat, thanks! + +The new macro support is also pretty cool IMHO. It allows to define variables in the configuration file or from the cli with the ‘-D’ flag to simplify the configuration file and cut some repetitions. It’s known to be used in a systemd setup with the LoadCertificates option in order to start gmid with non-root privileges but still letting it read the keys. + +(To be honest, there’s nothing wrong with starting gmid as root, but please do use the ‘chroot’ and ‘user’ rules to drop priviledges and chroot into a safe sandbox. Also, self-signed certs are cool! But this is just my opinion.) + +The last thing I’d like to mention (the whole changelog is at the end of this entry) is the pidfile support. The new (optional!) ‘-P pidfile’ flag makes gmid write its pid at the given location, that it’s also used as a lockfile to avoid spawning multiple instances by accident. This was a feature request, and from what I can see it was already included in the Gentoo overlay GURU package. + +## Future plans + +Recently I’ve started a new secret project. It’s yet another daemon, for a not-so-famous (but pretty) protocol. While working on this, instead of starting from scratch I cannibalised a lot of code from the OpenBSD’ rad(8) daemon. I chose it because it’s rad (sorry, I just had to make a stupid pun) and because it seems pretty simple, so I can easily swap out the code that implements the logic and write my own stuff. + +Oh my, I was impressed. It’s well known that the OpenBSD project produces simple, solid code that’s secure by default and so on; but it’s not something you can fully understand if you don’t look at the sources. Just by inheriting that code, I had for free a complete privsep framework, where every child process is re-exec’ed to gain a completely new and fresh address space, a solid imsg infrastructure to send messages around (also used to reload the configuration on-the-fly), the glorious parse.y, and a socket to control the daemon via a cli tool. And did I mention that all the messages via imsg are completely 100% asynchronous?! + +So, for the next version I’d like to replicate some of this. It’ll require some changes under the hood, so probably the next changelog won’t be as rich as this, but it’s worth. + +I’d also like to improve the log management. To be honest, it was one of those things that I intended to do for this release, but failed to do so. I have a local diff to allow logging to custom files, but I don’t like the implementation and so I dropped it; we’ll see for the next release. Patches are always welcome :P + + +## Changelog + +### New features + +* initial fastcgi support! (it's still young!) +* added user-defined macros, either via ‘-Dname=val’ or directly in the configuration file. +* new ‘include’ keyword to load additional configuration files. +* new ‘env’ rule to define environment vars for CGI scripts. +* new ‘alias’ rule to define hostname aliases for a server. +* allow ‘root’ to be specified per-location block. +* pidfile support with the new ‘-P’ cli flag. +* define ‘TLS_VERSION’, ‘TLS_CIPHER’ and ‘TLS_CIPHER_STRENGTH’ for CGI scripts. + +### Improvements + +* remove limits on the number of virtual hosts and location blocks that can be defined. +* print the datetime when logging to stderr. +* use ‘text/x-patch’ for ‘.patch’ and ‘.diff’ files. +* sort the auto index alphabetically. +* various improvements to the log management. +* drop the dependency on lex. +* added ‘--help’ as synonym of ‘-h’ and ‘-V’/‘--version‘ to print the version. +* c-like handling of strings in the configuration file: when two or more strings are next to each-others, are automatically joined into a single string. This is particularly useful with $-macros. + +### Bug fixes + +* correctly handle CGI scripts that replies with the maxium header length allowed. +* fixed the static target. +* fixed recursive mkdirs for configless mode (i.e. create ‘~/.local/share/gmid’) +* logs sent to syslog now have proper priority (before every message ended up as LOG_CRIT). Found by Anna “CyberTailor”, thanks! +* ensure ‘%p’ (path) is always absolute in ‘block return’ rules. +* fix automatic certificate generation, it caused problems on some adroid devices. Found by Gnuserland, thanks! +* document the ‘log’ rule. +* the seccomp filter was reworked and now it's known to work properly on a vast range of architectures (to be more specific: all the architectures supported by alpine linux), see github issue #4. Prompted and tested by @begss, thanks! +* various improvements to the configure script, notified and fixed by Anna “CyberTailor”, thanks! +* added a timeout to the regression tests. + +### Breaking changes + +* if duplicate rules are found in the configuration file, an error is now raised instead of silently using only the last value. +* (sort of) ‘gg’ moved to ‘regress’ as it's only used in the regression suite. +* (notice) the “mime "mime-type" "extension"” rule was deprecated and replaced by the new “map "mime-type" to-ext "extension"”. The ‘mime’ rule will be removed in a future version because its syntax is incompatible with the new string auto-concat mechanism. + blob - /dev/null blob + 390605cfd9bafc9cf08912d2cb114938c1fe54c2 (mode 644) --- /dev/null +++ resources/posts/opensmtd-dovecot-virtual-users.gmi @@ -0,0 +1,340 @@ +I just switched my mailserver from a setup with a single UNIX user to a slightly more complex one with virtual users. I don’t know how other admins manages their virtual users, but in this entry I’m going to discuss the method I’m using. + +This is *not* a tutorial on how to install and configure OpenSMTPD or Dovecot or anything else, as I don’t feel like I’m the most qualified to do so. Instead, if you’re looking on how to deploy your own mail server, I’m going to recommend the tutorial from Gilles Chehade: + +=> https://poolp.org/posts/2019-09-14/setting-up-a-mail-server-with-opensmtpd-dovecot-and-rspamd/index.html Setting up a mail server with OpenSMTPD, Dovecot and Rspamd + +In the past I’ve used a shared SQLite database to store users authentication data, but this time I wanted to manage the data differently. I don’t need to handle hundreds of users, and every user needs to be manually added by me, so a database is overkill. + +A more text-centric approach requires five configuration files: +* a passwd-like file +* an aliases table +* a domains table +* an authentication table +* a virtuals table + +The tables are needed to load data into OpenSMTPD, while for Dovecot a single ‘/etc/passwd’-like file is enough. + +Keeping the information in sync between these five files definitely not hard, but I’m particularly lazy, so I’ve wrote a simple AWK script to parse a custom ‘userdb’ file and populate all those files. But before going into that, let’s see an excerpt from my OpenSMTPD configuration: + +``` +# these are the paths on a FreeBSD host, on OpenBSD they’re +# just /etc/mail. +table aliases file:/usr/local/etc/mail/aliases +table domains file:/usr/local/etc/mail/domains +table passwd file:/usr/local/etc/mail/passwd +table virtuals file:/usr/local/etc/mail/virtuals + +# pki, filters and listen directives omitted + +action "remote_mail" lmtp "/var/run/dovecot/lmtp" rcpt-to virtual +action "local_mail" lmtp "/var/run/dovecot/lmtp" rcpt-to alias +action "outbound" relay helo example.com + +match from any for domain action "remote_mail" +match from local for local action "local_mail" +match from any auth for any action "outbound" +match for any action "outbound" +``` + +The four ‘match’ rules matches in order +* incoming emails for the domains we’re serving +* local emails from one UNIX user to another +* outgoing emails from authenticated users +* outgoing emails from local UNIX users (there’s an implicit ‘from local’, to turn your server into an open relay you need to be really, really explicit!) + +Two of the three actions deliver the mail over LMTP to Dovecot. An important bit there that I was missing on my first try was the ‘rcpt-to’ keyword: as we’ll see in a moment, all the mail are handled by a local user, but we need to use the recipient email address instead of the local user in the LMTP session, so Dovecot can save the email in the correct maildir. + +### passwd + +Dovecot needs only a single file for the authentication. One of the supported format, and the one I’m using, is a ‘passwd’-like format, like the following: + +``` +op@example.com::::::: +``` + +On the Dovecot site, things are a bit easier because there is no aliasing, resolving or expansions to do on the received emails. + +### alias table + +An alias table looks like this: + +```example of an alias table file +root: op +op: op@example.com +``` + +It maps *local* users to other local or remote users. In the example above, mail for the UNIX root user are forwarded to the user op, that in turns redirects his mail to op@example.com. + +### domains table + +Holds all the domains we’re accepting mails from. It can be specified in-line in the configuration file: + +```example of a domains table in smtpd.conf +table domains { "example.com", "foo.bar.net", … } +``` + +or in a file with one domain name per line + +```example of a domains table as plain file +example.com +foo.bar.net +``` + +### Credentials table + +A credentials table file looks like this: + +```example of a credentials table +user@doma.in password-hash +user2@example.com password-hash +``` + +just a simple user ↔ hash mapping. Hashes can be computed with the encrypt subcommand of smtpctl + +```example on how to hash a password +$ smtpctl encrypt +p4ssw0rd +$2b$10$jpdOj8WPIMABsMs.LzFbiuSpgZ1TlGUj2ztBxEimoaQylQD/jhelS +^D +``` + +NB: on OpenBSD-CURRENT (and as of a couple of releases already at least) the ‘smtpctl encrypt’ computes the BLF-CRYPT hash of the password, but for some reason on FreeBSD it uses SHA512-CRYPT. Dovecot needs to be told the default hashing scheme in ‘conf.d/auth-passwdfile.conf.ext’. Here’s mine + +``` example of auth-passwdfile.conf.ext file for Dovecot +passdb { + driver = passwd-file + # adjust SHA512-CRYPT eventually! + args = scheme=SHA512-CRYPT username_format=%u /usr/local/etc/dovecot/users +} + +userdb { + driver = passwd-file + args = username_format=%u /usr/local/etc/dovecot/users + override_fields = home=/var/vmail/%d/%n +} +``` + +Refer to the Dovecot documentation: +=> https://doc.dovecot.org/configuration_manual/authentication/password_schemes/ “Password Schemes” in the Dovecot documentation. + +### virtuals + +The virtual table is used to map address to other addresses (i.e. alias) or addresses to local users (to allow the delivery.) It looks like this + +```example of a virtuals table +postmaster@example.com: op@example.com +aaa@example.com: op@example.com +op@example.com: vmail +otheruser@example.com: vmail +``` + +## Making it painless + +Since maintaining this whole bunch of files may not be the easiest thing ever. To be a bit more declarative, I’ve come up with the following ‘userdb’ file. It’s an invented syntax that gets parsed by a super-simple AWK script and generates all the other files. Here’s an example: + +```example of syntax of userdb +# local alias +alias root op +alias op op@example.com + +# per virtual-domain config +example.com: + # Indentation is optional, but improves legibility. + # The following defines the user op@example.com; + # is the hash of the password computed + # with `smtpctl encrypt` + user op + # and define an arbitrary number of aliases + alias service1 + alias other-alias + + user otheruser + + # aliases can be to virtual users on other hosts + alias abuse someone@example2.com + +example2.com: + user someone + # … +``` + +The syntax is as simple as possible, to make the parsing easier. It’s also open for additions: for instance, adding a ‘quota’ keyword to define custom quotas shouldn’t be too hard. + +=> //git.omarpolo.com/vuserctl/ All the code examples are available in a git repository. + +The AWK implementation that parses the file is also pretty simple: + +``` userctl.awk +#!/usr/bin/env awk + +# expects action to be defined, like -v action=aliases + +/^[[:space:]]*$/ { next } +/^[[:space:]]*#/ { next } + +/:$/ { + # drop the : + gsub(":", "", $1); + domain = $1; + domains[domainslen++] = domain; + next; +} + +$1 == "user" { + user = sprintf("%s@%s", $2, domain); + users[user] = $3 + + # change “vmail” to match the local user that + # delivers the mail + aliases[user] = "vmail"; + next; +} + +$1 == "alias" { + if ($3 != "") { + target = $3; + } else { + target = user; + } + + if (domain != "") { + alias = sprintf("%s@%s", $2, domain); + } else { + alias = $2; + } + aliases[alias] = target; +} + +# output in the correct format +END { + if (action == "aliases") { + for (alias in aliases) { + if (match(alias, "@")) + continue; + printf("%s: %s\n", alias, aliases[alias]); + } + } else if (action == "virtuals") { + for (alias in aliases) { + if (!match(alias, "@")) + continue; + printf("%s %s\n", alias, aliases[alias]); + } + } else if (action == "domains") { + for (domain in domains) { + printf("%s\n", domains[domain]); + } + } else if (action == "users") { + for (user in users) { + printf("%s %s\n", user, users[user]); + } + } else if (action == "users.passwd") { + for (user in users) { + # user@doma.in:hash:::::: + # user@doma.in:hash::::::userdb_quota_rule=*:storage=1G + printf("%s:%s::::::\n", user, users[user]); + } + } else if (action == "users.mdirs") { + for (user in users) { + split(user, m, "@"); + # adjust the maildir path + printf("/var/vmail/%s/%s/Maildir\n", m[2], m[1]); + } + } else { + print "unknown action!\n" > "/dev/stderr" + exit 1 + } +} +``` + +The AWK script needs the variable ‘action’ to be defined to dump the correct information. It can be provided with the ‘-v’ flag, but for extra-comfort I wrote also the following wrapper script: + +```userctl the wrapper script +#!/bin/sh + +if [ ! -f "userctl.awk" ]; then + echo "Can't find userctl.awk!" >&2 + exit 1 +fi + +if [ ! -f "userdb" ]; then + echo "Can't find userdb!" >&2 + exit 1 +fi + +# run +run() +{ + awk -f userctl.awk -v action="$1" userdb +} + +case "$1" in + aliases) run "aliases" ;; + virtuals) run "virtuals" ;; + domains) run "domains" ;; + users) run "users" ;; + users.passwd) run "users.passwd" ;; + users.mdirs) run "users.mdirs" ;; + help) + echo "USAGE: $0 " + echo "where action is one of" + echo " - aliases" + echo " - virtuals" + echo " - domains" + echo " - users" + echo " - users.passwd" + echo " - users.mdirs" + ;; + *) + echo "Unknown action $1" >&2 + exit 1 + ;; +esac +``` + +Now that the framework is in place, the only missing piece is to use it to generate the files. I wrote yet another script to (re-)generate the tables and to create the maildir when a user is added. + +```sync-userdb +#!/bin/sh + +set -e + +# On OpenBSD these are only /etc/mail/… +./userctl aliases > /usr/local/etc/mail/aliases +./userctl virtuals > /usr/local/etc/mail/virtuals +./userctl domains > /usr/local/etc/mail/domains +./userctl users > /usr/local/etc/mail/passwd + +./userctl users.passwd > /usr/local/etc/dovecot/users + +m() +{ + if [ ! -d "$1" ]; then + mkdir "$1" + chown vmail:vmail "$1" + fi +} + +# ensure the maildirs exists +for dir in $(./userctl users.mdirs); do + homedir=$(dirname "$dir") + domdir=$(dirname "$homedir") + + m "$domdir" + m "$homedir" + m "$dir" +done + +# eventually add something like +# service dovecot restart +# service smtpd restart +# for FreeBSD or +# rcctl restart dovecot smtpd +# for OpenBSD. +``` + +## Conclusion + +I don’t have a proper conclusion for this entry. Tools like this are usually almost always “work in progress”, as they are changed/extended over the time depending on what I need to do. One thing for sure, designing simple database files and managing them with AWK is lots of fun. + +As always, if you have comment, tips or noticed something that’s missing or not explained properly, don’t refrain from notifying me, so I can update this entry accordingly. blob - /dev/null blob + ce7b6d2152d8b6f63348eea3131da67fad2fd54a (mode 644) --- /dev/null +++ resources/posts/state-of-theme-switching.gmi @@ -0,0 +1,54 @@ +Two years ago I decided to switch to a light-theme and I was really happy with that. Maybe it’s just me, but black text on white background is perfectly readable, and it didn’t give me problems or eye tiredness, not even when it’s 1 am and I’m still hacking together something. + +Fast forward two years, and I’m still happily using only light themes. Sometimes, I get some comments like “you’ve burnt my eyes” when I share a screenshot with someone for the first time, but that’s all. + +However, this summer I’ve started to (temporary) switch to a dark theme on some occasions (it’s really hot, even at night; I need keep the windows open to catch a brief breeze and not being eaten alive by mosquitoes) and I’m particularly disappointed with the current state of theme switching and dark mode in general. + +My goal would be a single *painless* action to switch my entire environment from a light to a dark theme and vice versa. How hard can it be? + +## The current state of the affairs + +These are the applications I have usually open + +* lots of Emacs frames +* lots of xterm windows +* a firefox window +* a gajim window (GTK XMPP client) + +all running on cwm, my favourite window manager. + +Switching the background image to something more dark-y (yes, my current wallpaper is mostly white too) is painless. Open an xterm, ‘C-r feh’ and ksh’ i-search leads me to the correct command. Cool. + +Emacs is the easiest to tame. M-x disable-theme followed by a M-x load-theme and you’re set. I could even automatize this and bind a key. + +xterm is sort of okay-ish to tame. I have a .Xdefaults with the colors; yesterday I’ve extracted my theme to .Xdefaults-light and selected a random dark theme in .Xdefaults-dark. In .Xdefaults I’ve added + +```excerpt of my .Xdefaults +#include ".Xdefaults-light" +! #include ".Xdefaults-dark" +``` + +and when I want to switch theme I can simply switch the comments. Yes, I have to close and re-open all my xterms, but it’s not a real issue. All my terminal windows are usually volatile: if I need to run something that takes more than a couple of seconds, I usually run it in a tmux session, so no problem with closing and re-opening xterms, really. + +Now the difficult part: firefox and other GTK programs. + +Firefox is extremely hard to tame in this regard, and even really buggy. I’m using a custom ACME-y light theme built with the “firefox colors” extension, which overrides the current theme at startup. Go wonders. Switching theme is clunky. On firefox 90 I have to go “hamburger menu” → “more tools” → “customize toolbar” and then switch the theme in the bar at the bottom. But this is only one part: then you have to go to about:config and set ui.systemUsesDarkTheme to 1 to set the theme preference for websites. I’m honestly surprised nobody wrote an extension to automatize this. I suspect I can automatize all of this by changing some files in ~/.mozilla/firefox, but… + +And then there’s the web. (WARNING: rant ahead) + +Seriously, it’s that difficult to build a website that adapts to the prefers-color-scheme media query? I didn’t realized, given that all my sites (well, except my cgit instance) adapts to the user preferred theme, without a line of javascript involved. Out of all the website I tried yesterday night (admittedly a short number) only duckduckgo was adapting to my preference. Props to them. + +The instance of pleroma I’m using sometimes to waste time in the fediverse has a theme switcher icon. Meh. But at least is always visible in the navbar at the top. Simple 0-css-or-so websites have of course this bright white background. I love white background, but when you’re in a dark-themed environment switching to a bright page is painful. + +I guess that this mess is due to allowing page authors to decide how pages are rendered on the users’ end. One more reason to ditch the web. + +(end of rant) + +My solution to the problem is to use firefox even less. The duckling-proxy does a *tolerable* job when thrown at a wikipedia page, and other simple pages are OK-ish to read. Sure, a terminal web browser like w3m are best suited for this, but I like the experience of never having to leave Telescope. + +GTK applications (or gajim, since it’s the only one I routinely use) are also meh, but tame-able. I have a solarized dark theme (which I personally hate, but I’m too lazy to find something else) installed and I can switch to that with lxappearance. Then restart gajim. + + +## Wrapping up + +I wrote this rant^W entry in a hurry, so I hope it makes sense. What I’m gonna do from now on? I’ll try to automatize some manual steps: for instance, switching theme on Emacs could also change the wallpaper and patch the .Xdefaults. For firefox and other GTK applications I guess manual intervention is still needed, or maybe I’ll be able to ditch them, who knows? blob - /dev/null blob + 13ac0ff75d8466b1168c901f4591e84b26d47049 (mode 644) --- /dev/null +++ resources/posts/suspend-unix-process.gmi @@ -0,0 +1,58 @@ +On UNIX when you type C-z (hold control and press ‘z’) at your terminal the program you have opened, let’s say ed(1), will be suspended and put into the background. Later, with ‘fg’ it can be bring back into foreground. + +But how it works? + +When the tty driver sees that you’ve typed control-z it’ll send a SIGTSTP signal to the process in the foreground. Then, the shell should do its part and handle the situation, usually by printing something like “Suspended $progname” and the prompt. + +Actually, the exact key binding is probably customizable via stty(1), but that’s not important. + +The tty driver is not the only one that’s entitled to send signals. Any program can send a SIGTSTP to any other program (OK, as long as they are running as the same user, or you’re root); so, for instance, you can kill ed with ‘kill -TSTP $pid’ from another terminal to achieve the same thing as pressing C-z. + +## ncurses + +In a ideal world a program shouldn’t know anything about SIGTSTP. This is not an ideal world though. If you don’t trust me, the proof is that ed is not the most used text editor. Most interactive terminal applications uses ncurses (or similar). + +By calling ‘raw()’ when initialising ncurses, the program won’t receive signals for special keys (like control-c, control-z, …), so it can handle those keys by itself. (Think how funny must be running emacs in a terminal and seeing it being killed every time you press control-c.) + +So, how can ncurses applications (like vi, tmux & co) suspend themselves? The answer is pretty easy given the context, yet it took me a while to figure it out: + +```C snippet that shows how to suspend the current program and give the control back to the shell. +#include +#include +#include + +/* if you’re using ncurses: */ +endwin(); + +/* kill the current program */ +kill(getpid(), SIGSTOP); + +/* if you’re using ncurses, redraw the UI */ +refresh(); +clear(); +redraw_my_awesome_ui(); +``` + +Yes, by killing yourself. (UNIX terminology leads to funny sentences, see?) + +But why SIGSTOP and not SIGTSTP as previously stated? Well, the manpage for signal(3) says that SIGSTOP and SIGTSTP have the same default action, but while a SIGTSTP can be caught or ignored, SIGSTOP cannot. + +The astute reader may have read the kill(2) man page and saw that if pid is zero, the signal is sent to the current process; so we could have wrote ‘kill(0, SIGSTOP)’. Unfortunately, what kill(2) *exactly* says is + +> If pid is zero: +> sig is sent to all processes whose group ID is equal to the process group ID of the sender, and for which the process has permission; this is a variant of killpg(3). + +If your program is made by multiple process, you’d stop all of them! This may be preferred, or it may not; I chosen to explicitly kill the current process (and only that!) for this very reason. + +## Wrapping up + +It’s fun to see how things works, especially when you are able to figure it up by yourself. I was trying to handle C-z in telescope, a gemini client I’m writing, and it took me a while to understand how suspend the current process. I searched a bit the internet but everything I found boiled down to “control-z, bg/fg or kill it with SIGTSTP”; which turns out was the correct solution, but wasn’t clear to me at first. I tend to forget that I can send signals to myself. + +## Some links + +=> gemini://gemini.omarpolo.com/cgi/man/stty stty(1) manpage +=> gemini://gemini.omarpolo.com/cgi/man/2/kill kill(2) manpage +=> gemini://gemini.omarpolo.com/cgi/man/curs_inopts curs_inopts(3) manpage — talks about cbreak, raw and other interesting stuff +=> gemini://gemini.omarpolo.com/cgi/man/3/signal signal(3) manpage + +=> //telescope.omarpolo.com Telescope blob - /dev/null blob + b466d9b91380256f41d7583e22043966f7ccd408 (mode 644) --- /dev/null +++ resources/posts/taking-about-9p-intro.gmi @@ -0,0 +1,68 @@ +These days I’m hacking on 9P. In case you don’t know, 9P is a protocol for a network file system that was developed as part of the plan9 operating system. + +Now, to make it clear, I’ve never — sigh! — used 9P nor plan9 before. I’m just starting to explore the 9P protocol, hacking something together (for a secret project), and writing some notes here on my blog. + +If you find some errors please be kind and notify me. The contacts are at the end of every entry in the Gemini version of this blog. + +The 9P protocol is pretty simple: the client sends requests (called T-messages) and the server replies (with R-messages). Replies can be delayed or received out of order. A transaction of some type is completed when the server replies with the matching R-message (or with an error). + +I’m going to use the same syntax used in the plan9 manpages to describe the packets. Fields are written as name[n] where ‘name’ represents the name of the field and ‘n’ (which is either 1, 2, 4, 8 or 13) represents the number of byte. An exception to this rule are strings and other variable-width fields: they are represented by a two-byte integer counter followed by the actual data. Strings in particular are denoted as name[s] (where ‘s’ is a literal s character.) + +Integers are transmitted in little-endian format, and strings are encoded in UTF-8 without the NUL-terminator. The NUL byte is illegal in strings transmitted over 9P and thus excluded by paths, user login names etc. + +Both the requests and the replies share a common structure, the header, which looks like this + +``` 9P header +size[4] type[1] tag[2] +``` + +Size, the first field, is a 32 bit field that indicates the length of the message (including ‘size’ itself!). Type is a one-byte integer that specifies the type of the requests and tag is an arbitrary client-chosen integer that uniquely represents this transaction. The client cannot issue two or more ongoing transaction with the same tag. + +Following the header there is an optional body whose structure depends on the type of message. + +Clients can only send T-messages, and the server can only reply with a R-message. + +The available messages are: + +* Tversion/Rversion +* Tauth/Rauth +* Rerror (Terror does not exist) +* Tflush/Rflush +* Tattach/Rattach +* Twalk/Rwalk +* Topen/Ropen +* Tcreate/Rcreate +* Tread/Rread +* Twrite/Rwrite +* Tclunk/Rclunk +* Tremove/Rremove +* Tstat/Rstat +* Twstat/Rwstat + +There are some extension (or “dialects” should I say) of 9P which adds (and slightly change) these messages, but at least for now I’m trying to stick to 9P2000 “vanilla”. + +An important role in 9P is played by “qid”s and “fid”s. A fid is a 32bit integer chosen by the client that identifies a “current file” on the server. They are similar, albeit different, to UNIX file descriptors. Qids are the server idea of a file: they are a jumbo object of a whopping 13 bytes: the first one identifies the type (whether is a file, directory, and so on), a four byte integer unique among all files in the hierarchy called “path” and a four byte “version” field that should get incremented every time the file is modified. + +Fids are often present in T-messages and qids in R-messages. + +The first message that a client should issue after it has established a connection to a file server is Tversion to negotiate the version used and the maximum size of the packets. The Tversion signature is + +``` +size[4] Tversion tag[2] msize[4] version[s] +``` + +(from now on I’ll omit the three header fields when describing the structure of a packet) + +The msize is the maximum size of a packet that the client is willing to accept, and the version is a string the identifies the protocol version used. It MUST start with the “9P” characters. The client can’t issue further requests until the server replies with a Rversion, which has the same structure. The msize replied by the server has to be smaller or equal to the one proposed by the client. + +Then there is an optional authentication using Tauth. I’m not interested in how “normal” authentication works in 9P in my project (for now at least), but the idea is that the server provides to the client a special “authentication file” that an unauthenticated client can read and write to. This is used to implement a custom auth protocol which is external to 9P. + +At this point the client can “attach” a file tree using Tattach: + +``` +fid[4] afid[4] uname[s] aname[s] +``` + +If successful, fid will represent the file tree accessed by aname. ‘afid’ is the authentication fid, which can be -1 (i.e. 0xFFFFFFFF) for the no-authentication case. ‘uname’ is the user name. + +If successful, the client has access to a file tree and can start moving around (by means of Twalk) and messing with files (Topen, Tremove, …). This is also all for the introduction: future entries will focus in particular about the other kinds of messages. blob - /dev/null blob + 52ea17edf2a960ba9bb54441d4e7e8c936c24d77 (mode 644) --- /dev/null +++ resources/posts/taking-about-9p-open-and-walk.gmi @@ -0,0 +1,69 @@ +=> taking-about-9p-intro.gmi Taking ’bout 9P: intro + +The Topen request at first looks weird. Here’s its signature + +```bytewise description of a Topen request +size[4] Topen tag[2] fid[4] mode[1] +``` + +It’s strange, isn’t it? For comparison, here’s the C signature for the open(2) system call: + +```C prototype of the open(2) system call +int open(const char *path, int flags, ...); +``` + +Where is the path in the Topen call? + +The description for the Topen request says: + +> The open request asks the file server to check permissions and prepare a fid for I/O with subsequent read and write messages. + +Which implies that to write or read a fid you must open… an existing fid? + +This morning (well, actually some days ago since this entry got published later) cage explained the mystery to me: Twalk. + +When I first skimmed through the 9P documentation I thought that the walk request was basically a chdir(2), which it is, but also is not! + +The Twalk requests allows one to navigate from a fid (usually representing the starting directory) through some path components and reach a destination file that will be associated with a new fid. + +So, if I connect to a 9P file server and write something to ~/notes.txt (supposing that my home is on that file server) the 9P session could look like this: + +``` +# establish a connection and negotiate the version +→ Tversion 9P2000 +← Rversion 9P2000 + +# mount the home +→ Tattach 1 "op" "/home/op" +← Rattach + +# walk from fid #1 (/home/op) to “notes.txt” (a file!) +# and associate to it fid #2 +→ Twalk 1 2 "notes.txt" +← Rwalk + +# prepare fid 2 (/home/op/notes.txt) for +# reading and writing +→ Topen 2 OWRITE|OREAD +← Ropen + +# read/write fid 2… + +# close the fid 2 since it’s no longer used +→ Tclunk 2 +← Rclunk +``` + +(note that as always this is entirely my speculation from reading the documentation. I never used — sigh! — plan9 nor 9p) + +So it actually makes sense for Topen to accept a fid and not a path, and Twalk is a general purpose request that can be used to implement various system calls (dup2, chdir, open…) + +Then I started to think why it was like this. I mean, everything is finally starting to make sense in my head, but why the 9P people decided to implement Topen and Twalk this way? + +Well, I can’t say for sure, but I’m starting to noticing that a fid is something more than a UNIX file descriptor: it’s both a file descriptor AND a path. + + *my mind blows* + +Which is actually a pretty clever solution. The client can get new fids by mean of Twalk, which then can be passed to Tremove (for removal), or Tstat/Twstat, or being opened and then written/closed. It’s also probably more efficient than passing string around on every request. + +This has also some drawbacks probably. For one, it’s not clear at glance if a fid was prepared for I/O or not. On UNIX there is a clear distinction between a file descriptor (a number that references an object in the kernel) and a path (a mere sequence of bytes NUL-terminated.) But since this is an underlying mechanism, it seems pretty clever. It shouldn’t be too much difficult to map the usual UNIX syscalls on top of 9p.