DEV Community

loading...
Cover image for Bind mounts with an OpenRC service

Bind mounts with an OpenRC service

Davyd McColl
Code monkey extraordinaire
・6 min read

Recently, Windows 10 is refusing to connect to my SMB service on my desktop Gentoo Linux machine. It basically refuses to use anything other than domain auth, and my Gentoo machine doesn't know anything about a domain, most especially my work domain (which is what the laptop wants to use -- .\username doesn't seem to help, and giving my Gentoo machine's name as the domain doesn't help either, most likely because I don't have a domain controller running -- and I really don't need one at home).

After about 1/2 an hour of trying to convince the Windows machine to connect to SMB shares it had had no problem with a few months ago (before the last Great Windows Update), I thought "stuff it, I'll just go old-school and install VSFTPD on my Gentoo machine so I can copy some stuff off to watch during the impending load-shedding".

For those of you lucky enough not to live in South Africa, "load-shedding" is a phenomenon where the state-owned electricity provider, due to rampant corruption and mismanagement for the last 20 years, is unable to provide power to the entire country, so, for fear of rolling blackouts caused by overloading the grid, certain neighborhoods are literally turned off for two hours at a time.

Yes, it's about as much fun as it sounds. Fortunately, I have a nice laptop and plenty of media stored offline. I just have to get the media onto the laptop. Hence the fun with connections.

Now, VSFTPD (the Very Secure File Transfer Protocol Daemon) lives up to its name: it's Very Secure. Part of that security is that users who log in to the server -- even the anonymous user -- are confined to a chroot jail so that they cannot possibly access files outside of their allowed domain. This works quite well for regular users, who are confined to their home folders by default, as well as the anonymous user, who is also confined to a special place on disk. The problem comes in when you'd like to allow access to some parts of the filesystem: VSFTPD does not follow symlinks (for security reasons), so I can't just symlink in my media drives. Another plan has to be made.

The generally accepted answer is to use bind mounts, which worked, but leaving these mounts in /etc/fstab as auto-mounting resulted in odd behavior: after booting, the mounts wouldn't be showing the same contents as the folders they were linked against. If I unmounted and remounted them, they worked just fine. I have a feeling they were being mounted before their targets were being mounted, perhaps because my original script, run via cron at @reboot wasn't waiting for local mounts to complete.

So first attempt was to:

  • set the mounts as noauto
  • alter my hacky script to do the mounting for me
    • that script could take arguments like -d 120 to delay the mounting
  • add this as a @reboot line in my crontab for root (with crontab -e)

here it is (don't judge me! I know it's hacky! I was trying to figure out wtf was going on, and I also was getting tired of debugging this issue with only a few minutes until the power is cut, and no media to watch on my laptop (: ):

#!/bin/sh
BASE=/var/ftp/pub
LAST=""
DELAY=0
while test ! -z "$1"; do
  if test -z "$LAST"; then
    LAST="$1"
  else
    if test "$LAST" = "-d"; then
      DELAY="$1"
    fi
    LAST=""
  fi
  shift
done

if test "$DELAY" != "0"; then
  echo "delaying by $DELAY seconds"
  sleep $DELAY
fi
for m in $BASE/*; do
  umount $m &> /dev/null
  if test -z "$(mount | grep $m)"; then
    mount $m
  else
    echo "Unable to (re-)mount $m: already mounted and umount denied"
  fi
done

If you're new to bash, the while loop at the top, with the shift command near the end, is a way of cycling through arguments provided to the script until we run out:

  • shift, like Javascript's Array.prototype.shift, removes the first element from the array of arguments that this script was invoked with
  • test ! -z "$1" tests the first argument in the args list to see if it's empty -- so once all arguments have been shifted off, $1 is empty (:

I figured someone else might like to have a look at the OpenRC init script:

  • it's an example of how simple OpenRC scripts can be, unlike the dumpster-fire that is systemd 🔥
    • all flaming aside: everything sucks in its own special way, so I'm sure OpenRC isn't perfect -- but systemd has so many "features" which make me dislike it intensely
  • it's an example of
    • line-by-line file reading with bash
    • some of the simplest, but very powerful GNU utils
      • awk
      • grep
#!/sbin/openrc-run

depend() {
  need localmount
}

start() {
  for MOUNT_POINT in $(find_bind_mounts); do
    do_mount $MOUNT_POINT
  done
}

stop() {
  for MOUNT_POINT in $(find_bind_mounts); do
    do_umount $MOUNT_POINT
  done
}

do_mount() {
    if test -z "$(mount | grep -E "\s$1\s")"; then
      echo "  mount $1"
      mount $1
    else
      echo "  already mounted: $1"
    fi
}

do_umount() {
    if test -z "$(mount | grep -E "\s$1\s")"; then
      echo "  already unmounted: $1"
    else
      echo "  umount $1"
      umount $1
    fi
}

find_bind_mounts() {
  while read LINE; do
    OPTS="$(echo $LINE | awk '{print $4}' | grep -E '(,)?bind(,)?')"
    if test -z "$OPTS"; then
      continue
    fi
    echo $LINE | awk '{print $2}'
  done < /etc/fstab
}

Some explanations:

  1. This script should be run with /sbin/openrc-run (see the hash-bang line) because that stub provides a lot of base functionality:
    • provides default start, stop and status functions
    • understands how to get dependencies from depend()
  2. we declare services that this one depends on in the depend function, one per-line. Simple!
  3. OpenRC scripts can be as short as just a few lines long because there are default implementations for start, stop and status (see here: https://github.com/OpenRC/openrc/blob/master/service-script-guide.md
    • in this case though, I'm not starting a daemon process: I want to do some work at startup and some more at shutdown, so I implement start and stop myself
  4. from start() and stop()
    • while read LINE; do echo $LINE; done < some_file is how you can work over a file line-by-line in bash. Now you know (:
  5. from find_bind_mounts()

    • awk '{print $1}' prints the first field found on a line -- this is a really simple way to get a field out of a line which is broken up by whitespace -- like fstab entries! Field 4 in /etc/fstab describes the mount options...
    • so bind mounts will have bind in there somewhere. Now we need to invoke grep with -E to use Extended syntax and the regex I'm using searches for the word "bind" preceded either by whitespace or a comma, and followed either by whitespace or a comma, so matches would be, eg: auto,bind, bind,noatime,noauto or noatime,bind,noauto, remembering that the mount options field is not the last field, so there would be whitespace before and after it.

      • we get lines like:
      /mnt/monolith /var/ftp/pub/monolith none defaults,bind,noauto 0 0
      
  • once we have a line describing a bind mount, we select out the second field with awk, since this is the directory into which we would be mounting, so the shorthand syntax of mount <mount-point> would work
    • from the above line, we would get /mnt/monolith
  1. from do_mount() and do_umount()
    • just to be safe, we check if the mount point retrieved from /etc/fstab is already mounted with mount | grep -E "\s$MOUNT_POINT\s", which would look for that mount-point by name, surrounded by whitespace, from the mount command
    • mount would print lines like: /dev/sdh1 on /mnt/monolith type fuseblk (rw,nosuid,nodev,noexec,noatime,user_id=0,group_id=0,default_permissions,allow_other,blksize=4096) and the second field in that line is the mount-point
    • if we're starting up and the mount-point is already mounted, we don't do it again (a bind mount can be done again onto the same directory!)
    • if we're shutting down and the mount-point is not mounted, we also don't attempt to unmount it

And whilst booting, we get the following output from OpenRC:

mount-binds        |  mount /var/ftp/pub/monolith
mount-binds        |  mount /var/ftp/pub/piggy
mount-binds        |  mount /var/ftp/pub/dump
 [ ok ]

And that's about all (:

Discussion (0)