A friend recommended Dwarf Fortress as a
quality real-time strategy game with a good community and active
development. Sadly, I found it hard to run the game since it's
distributed as 32-bit binaries and like most games has many library
dependencies. Gentoo distributes pre-baked 32-bit libs for these
cases but they didn't work for me. I then launched a 32-bit Ubuntu
VM. The game worked this way, but it's unsatisfying to play through
an extra window, with emulation latency, and the audio broke up.
Since brushing up on LXC 1.0, I realized that this could be the
perfect solution for Dwarf Fortress. I'm all set up for unprivileged
containers, so I pulled up Graber's
article on how to run X apps in a container. I was successful
after encountering several problems, so here's how I did it.
The Plan
Graber's technique for trasferring host-unprivileged actions
(e.g. normal user can run X on host) to a container-unprivileged user
can be summarized:
- Bind-mount any required special files/devices into the
container.
- Thoughtfully craft your user ID map so that the
container-unprivileged uid/gid map directly to the host-unprivileged
uid/gid
So, these went into my container config, taken directly from
Graber's article (except /dev/video0 which I don't have and apparently
don't need):
lxc.mount.entry = /dev/dri dev/dri none bind,optional,create=dir
lxc.mount.entry = /dev/snd dev/snd none bind,optional,create=dir
lxc.mount.entry = /tmp/.X11-unix tmp/.X11-unix none bind,optional,create=dir
And also this user mapping (my host-unprivileged user is 1000):
lxc.id_map = u 0 100000 1000
lxc.id_map = g 0 100000 1000
lxc.id_map = u 1000 1000 1
lxc.id_map = g 1000 1000 1
lxc.id_map = u 1001 101001 64535
lxc.id_map = g 1001 101001 64535
The effect of this is that uid/gid 1000 on the guest has all the
same privileges as uid/gid 1000 on the host in terms of bind-mounted
resources. There is an unexpected consequence of this mapping: user
'erik' on the guest will be able to interact with the bind-mounted
resources but not user 'root' on the guest. This strange inversion of
privileges applies only to those bind mounts (X socket, sound device);
guest-root still trumps guest-'erik' when dealing with all other
permissions in the container.
Building the system
I can now create the container. It is configured to bridge to br0.
lxc-create -f dwarf.conf.init -t download -n dwarf -- -d gentoo -r current -a i386
I start the container. It's necessary to fix up the networking
manually at least the first time since the Gentoo download image
doesn't DHCP.
lxc-start -n dwarf -d
lxc-attach -n dwarf ip addr add 172.21.117.80/24 dev eth0
# also add default route and fix /etc/resolv.conf
lxc-attach -n dwarf wget google.com # Prove networking success
I now enter the container through SSH instead of lxc-attach, since
some things don't quite work correctly under lxc-attach (no hostname,
~ isn't expanded correctly).
Prove X
I installed xclock, so I could isolate the "use X in a container"
problem from the "satisfy binary linker dependencies" problem. I got
an error about "no protocol specified", and the search engines told me
that I needed the xauth program. I installed xauth and imported the
cookie from the host environment. On the host:
msi erik # xauth list $DISPLAY
msi/unix:0 MIT-MAGIC-COOKIE-1 029beac9a3cecde93ffbf0007ea7a4b6
And in the guest:
erik@dwarf ~ $ export DISPLAY=:0
# Note I omitted the hostname before :0
erik@dwarf ~ $ xauth add :0 MIT-MAGIC-COOKIE-1 029beac9a3cecde93ffbf0007ea7a4b6
erik@dwarf ~ $ xclock
And behold, a big clock appears!
Summoning dwarves
In the guest, I download the binary tarball for the game and
extract it. It won't run, of course, until dependencies are
satisfied.
dwarf portage # ldd /usr/src/df_linux/libs/Dwarf_Fortress
linux-gate.so.1 (0xf7732000)
libSDL-1.2.so.0 => not found
libgraphics.so => /usr/src/df_linux/libs/libgraphics.so (0xf7310000)
libstdc++.so.6 => /usr/src/df_linux/libs/libstdc++.so.6 (0xf7233000)
libm.so.6 => /lib/libm.so.6 (0xf71ee000)
libgcc_s.so.1 => /usr/src/df_linux/libs/libgcc_s.so.1 (0xf71d2000)
libc.so.6 => /lib/libc.so.6 (0xf702c000)
libpthread.so.0 => /lib/libpthread.so.0 (0xf7011000)
libgtk-x11-2.0.so.0 => not found
libgobject-2.0.so.0 => /usr/lib/libgobject-2.0.so.0 (0xf6fbf000)
/lib/ld-linux.so.2 (0xf7733000)
libSDL-1.2.so.0 => not found
libSDL_image-1.2.so.0 => not found
libGLU.so.1 => not found
libSDL_ttf-2.0.so.0 => not found
libglib-2.0.so.0 => /usr/lib/libglib-2.0.so.0 (0xf6e83000)
libffi.so.6 => /usr/lib/libffi.so.6 (0xf6e7c000)
It should be possible to iteratively identify and install missing
packages until "not found" is "not found" in the ldd output. However,
I took a shortcut by consulting an ebuild in the sunrise overlay.
less /var/lib/layman/sunrise/games-roguelike/dwarf-fortress/dwarf-fortress-0.34.05.ebuild
...
media-libs/fmod:1
media-libs/freetype
media-libs/libsdl[opengl,video,X]
media-libs/libsndfile[alsa]
media-libs/openal
media-libs/sdl-image[png,tiff,jpeg]
media-libs/sdl-ttf
sys-libs/zlib
x11-libs/cairo[xcb,X]
x11-libs/gtk+:2[xinerama]
x11-libs/libXcomposite
x11-libs/libXcursor
x11-libs/pango[X]
I tweaked some package.use settings until they installed. There
was also a dependency failure that required me to manually install
dev-perl/XML-Parser. At this point dynamic linking looks clean.
The chicken exit
At some point in here I learned that Dwarf Fortress can run under curses
emacs data/init/init.txt
...
Linux/OS X users may also use PRINT_MODE:TEXT for primitive ncurses output.
[PRINT_MODE:TEXT]
... but the output goes to a very small fixed area, and that's not
what we're after here.
Let's try running the game:
su erik
export DISPLAY=:0
./df
...it fails because there a couple of things left to debug. One
error was about being unable to load a png file, and a search engine
turned
up a solution which involved symlinking *.png to *.bmp. It seems
that the game needs an older libpng, but it still didn't work when I
tried that solution so I just went with the symlink hack.
And it worked!
Extra credit: re-using portage
So, all this took a few attempts, especially since tweaking the
user map requires re-creating the container. I ran out of space on my
notebook, and realized that it would be nice if the "download"
template could re-use the host's portage and distfiles just like the
privileged "gentoo" template does. This saves about 850M per
container.
I started by deleting the contents of /usr/portage in the
container. Then I restarted the container with a new bind-mount:
lxc.mount.entry = /usr/portage usr/portage none bind,optional,create=dir
A little extra user-map magic was required, since the container
boosted privileges for it's 'erik' user but now I wanted the guest's
'root' user to also have privileges to write into the new bind-mount.
There are probably multiple ways to achieve this, but my little brain
only came up with this:
- root on the guest is mapped to 100000.
- This uid should have rw access to /usr/portage on the host.
- The existing 'portage' group (gid 250) has rw access /to /usr/portage on the host
- The guest 'portage' group should have the same gid as the host 'portage' group.
So, I created a new user on the host named 'dwarfroot' with the
desired uid and added it to the 'portage' group. Also, a hole was
punched in the subgid map for the guest 'portage' group, just like we
did earlier with the 'erik' user in the subuid map.
/etc/subuid and /etc/subgid:
erik:1000:65536
erik:100000:65536
dwarfroot:165536:65536
And in the container config file, the map has two special-case one-id ranges for the 'portage' and 'erik' uid/gid's:
lxc.id_map = u 0 100000 250
lxc.id_map = g 0 100000 250
lxc.id_map = u 250 250 1
lxc.id_map = g 250 250 1
lxc.id_map = u 251 100251 749
lxc.id_map = g 251 100251 749
lxc.id_map = u 1000 1000 1
lxc.id_map = g 1000 1000 1
lxc.id_map = u 1001 101001 64535
lxc.id_map = g 1001 101001 64535
After restarting the container, the guest 'root' user can write
files into the bind-mounted /usr/portage/distfiles as needed.
Audio
Audio isn't working, and I believe that's again a permissions
issue. The files in /dev/snd/* are writable by the host's group
'audio' (gid 18) which isn't mapped. By now, this seemed like it
should be simple. I punched a new hole in the gid map so that host
gid 18 remained guest gid 18:
lxc.id_map = u 0 100000 250
lxc.id_map = g 0 100000 18
lxc.id_map = g 18 18 1
lxc.id_map = g 19 100019 231
lxc.id_map = u 250 250 1
lxc.id_map = g 250 250 1
lxc.id_map = u 251 100251 749
lxc.id_map = g 251 100251 749
lxc.id_map = u 1000 1000 1
lxc.id_map = g 1000 1000 1
lxc.id_map = u 1001 101001 64535
lxc.id_map = g 1001 101001 64535
I repeatedly got "newgidmap: write to gid_map failed: Invalid
argument" when starting the container. Debugging, I noticed that
commenting out the final lines of the map allowed the container to
start. Finally I found this gem in "man user_namespaces":
There is an (arbitrary) limit on the number of lines in the
file. As at Linux 3.18, the limit is five lines.
Gah! Too many 'g' entries! Let me collapse the system entries, at
the risk of giving the guest slightly too many permissions on the
files in /dev. I must also adjust /etc/subgid accordingly.
lxc.id_map = u 0 100000 250
lxc.id_map = g 0 100000 18
lxc.id_map = g 18 18 232
lxc.id_map = u 250 250 1
lxc.id_map = u 251 100251 749
lxc.id_map = g 251 100251 749
lxc.id_map = u 1000 1000 1
lxc.id_map = g 1000 1000 1
lxc.id_map = u 1001 101001 64535
lxc.id_map = g 1001 101001 64535
While this succeeded in rendering the files in /dev/snd/* as owned
by 'audio' (and presumably writeable), the game still threw the same
audio errors:
Picking OpenAL Soft. If your desired device was missing, make sure you have the appropred a different device, configure ~/.openalrc appropriately.
ALSA lib /var/tmp/portage/media-libs/alsa-lib-1.0.28/work/alsa-lib-1.0.28/src/confmisc.
ALSA lib /var/tmp/portage/media-libs/alsa-lib-1.0.28/work/alsa-lib-1.0.28/src/conf.c:42card_driver returned error: No such file or directory
ALSA lib /var/tmp/portage/media-libs/alsa-lib-1.0.28/work/alsa-lib-1.0.28/src/confmisc.ings
ALSA lib /var/tmp/portage/media-libs/alsa-lib-1.0.28/work/alsa-lib-1.0.28/src/conf.c:42concat returned error: No such file or directory
ALSA lib /var/tmp/portage/media-libs/alsa-lib-1.0.28/work/alsa-lib-1.0.28/src/confmisc.e
ALSA lib /var/tmp/portage/media-libs/alsa-lib-1.0.28/work/alsa-lib-1.0.28/src/conf.c:42refer returned error: No such file or directory
ALSA lib /var/tmp/portage/media-libs/alsa-lib-1.0.28/work/alsa-lib-1.0.28/src/conf.c:47ch file or directory
ALSA lib /var/tmp/portage/media-libs/alsa-lib-1.0.28/work/alsa-lib-1.0.28/src/pcm/pcm.cfault
AL lib: (EE) alsa_open_playback: Could not open playback device 'default': No such file
Initializing OpenAL failed, no sound will be played
Graber's example used PulseAudio not ALSA, so there's a good chance
that something's missing in my mounted audio devices. I'm no ALSA
ace, but let me try bind-mounting all of the 'audio'-owned devices.
# All audio-owned devices
lxc.mount.entry = /dev/snd dev/snd none bind,optional,create=dir
lxc.mount.entry = /dev/adsp dev/adsp none bind,optional,create=file
lxc.mount.entry = /dev/dsp dev/dsp none bind,optional,create=file
lxc.mount.entry = /dev/audio dev/audio none bind,optional,create=file
lxc.mount.entry = /dev/mixer dev/mixer none bind,optional,create=file
lxc.mount.entry = /dev/sequencer dev/sequencer none bind,optional,create=file
lxc.mount.entry = /dev/sequencer2 dev/sequencer2 none bind,optional,create=file
It didn't help. To isolate the issue, like with xclock, I found a
.wav file I could test with aplay. Indeed, the error is reproducible
with aplay. The venerable strace utility showed a failure to write to
/dev/snd/controlC0. I hadn't added 'erik' to 'audio' in the guest
also. Problem solved, and audio works.
Now, down to the mines...