Published: 13th April 2024
Recently, I set up a Samba server
on Raspberry
Pi 4B at home. Its use case being a place for me and my wife to store
(from several devices and OSes) stuff as well as to share it among
ourselves without sending it over the globe via every BigTech server
imaginable.
I chose FreeBSD
as the
base operating system, since I wanted to explore its built-in support
for OS-level containerization - jails
- and its file system - ZFS
.
This article documents my journey, where I
Let’s get to it!
Disclaimer: I am just a guy on the Internet. What works for me may not work for thee. I encourage you do your own research and testing.
Jails
are FreeBSD’s built-in tool for running services in a contained manner.
In short, you take chroot(8)
,
add isolation of system processes, resources and users and you get
jails
.
VNET jails
allow you to separate the jail’s whole network stack from the host on
top of that, meaning it has its own network interfaces, IP address,
routing table, firewall rules, …
I find VNET jail
to be a good fit for running
Samba
, since I do not want to pollute the host with
additional packages, expose too many ports on it (which
Samba
does) and limit the blast radius were the service
compromised.
From this particular setup - home server for file storage - I want 2 things:
If you want to have a file server to which you can connect from
Linux, FreeBSD, macOS, iOS, Android, Windows, … without any major
compatibility and installation hassles, Samba
is pretty
much the only option. Easy.
Second point means having 2 disks connected to the RPi in a mirrored
setup. I decided to spend 1 innovation
token here and go for ZFS
as the
file system and volume manager. It supports mirrored setup and dataset
snapshotting out of the box and offers flexible file system
managment.
Here is the final setup:
As you can see, I have 2 SSDs connected to the RPi via USB-SATA
adapters. I cannot use whole disks for the pool, because they are of
slightly different sizes (both advertised as 1 “TB” by the manufacturers
though). Here is an excerpt from the information provided by geom(8)
:
root@on-host# geom disk list
...
Geom name: da0
Providers:
1. Name: da0
Mediasize: 1000204886016 (932G)
Sectorsize: 512
Mode: r0w0e0
...
Geom name: da1
Providers:
1. Name: da1
Mediasize: 1024209543168 (954G)
Sectorsize: 512
Mode: r0w0e0
...
gpart(8)
to the rescue! I create a GPT
partition table on both disks
(named da0
and da1
) and a 896GB partition,
aligned to 1MB and labeled by the manufacturer and the
Mediasize
listed above:
root@on-host# gpart destroy -F da0
da0 destroyed
root@on-host# gpart create -s GPT da0
da0 created
root@on-host# gpart add -a 1M -s 896G -l crucial-932G -t freebsd da0
da0s1 added
root@on-host# gpart create -s GPT da1
da1 created
root@on-host# gpart add -a 1M -s 896G -l apacer-954G -t freebsd da1
da1s1 added
Now is the time to create a ZFS
pool. I use the labels
created in the preceding step and set the ashift
property, which governs the smallest IO operation on the pool and should
be equal to the sector size of the disks.
It is a logarithm with base 2, but setting it to 12 results in
power(2, 12) = 4KB
, so it is not equal to
power(2, 9) = 512
reported by geom disk list
command. As discussed in the linked article, operating systems often
don’t report the sector size correctly. Since the disks are relatively
new, I set the ashift
property to 12.
atime=off
disables the system changing the
access time
of a file each time it’s accessed and
compression=lz4
enables compression of all files, which is
very much worth it - according to all the folks on the Internet.
root@on-host# zpool create -o ashift=12 storage-pool mirror gpt/crucial-932G gpt/apacer-954G
root@on-host# zfs set compression=lz4 storage-pool
root@on-host# zfs set atime=off storage-pool
root@on-host# echo 'zfs_enable="YES"' >> /etc/rc.conf
For more info about these parameters and why they might be a good
idea to set this way, read the whole openzfs
tuning guide.
Caution: When testing
the ZFS
pool, it would, after reboots, sometimes not get
loaded automatically (even though the zfs_enable="YES"
should take care of it) and would need a manual
# zfs import ...
intervention. I suspect a race condition
between the kernel recognizing disks, since they go through USB, and
init scripts.
Therefore I devised a following solution - modifying
/etc/rc.d/zpool
to wait for the da0
and
da1
disks:
#!/bin/sh
#
#
# PROVIDE: zpool
# REQUIRE: hostid disks
# BEFORE: mountcritlocal
# KEYWORD: nojail
. /etc/rc.subr
name="zpool"
desc="Import ZPOOLs"
rcvar="zfs_enable"
start_cmd="zpool_start"
required_modules="zfs"
+ __wait_for_disk()
+ {
+ local disk i
+ disk="$1"
+ i=0
+ while ! [ -c "$disk" ]; do
+ echo "Waiting for disk $disk..."
+ sleep 1
+ if [ "$i" -eq 5 ]; then
+ break
+ fi
+ i=$(expr "$i" + 1)
+ done
+ }
+
zpool_start()
{
local cachefile
+
+ __wait_for_disk da0
+ __wait_for_disk da1
for cachefile in /etc/zfs/zpool.cache /boot/zfs/zpool.cache; do
if [ -r $cachefile ]; then
# Rest of the file...
Mostly following the guide in the jails
chapter in the
Handbook
,
notable differences are:
/jails
and not
/usr/local/jails
base.txz
from FreeBSD
mirror, I already have it downloaded in
/usr/freebsd-dist
Create the datasets:
root@on-host# zfs create -o mountpoint=/jails storage-pool/jails
root@on-host# zfs create storage-pool/jails/media
root@on-host# zfs create storage-pool/jails/containers
root@on-host# zfs create storage-pool/jails/templates
Prepare archives to extract as well as files to copy into the
templates
sub-directory:
root@on-host# cp /usr/freebsd-dist/base.txz /jails/media/
root@on-host# cp /etc/resolv.conf /jails/media/
root@on-host# cp /etc/localtime /jails/media/
Create the 14.0-RELEASE
dataset and “dump” stuff into
it:
root@on-host# zfs create storage-pool/jails/templates/14.0-RELEASE
root@on-host# tar -xf /jails/media/base.txz -C /jails/templates/14.0-RELEASE --unlink
root@on-host# cp /jails/media/resolv.conf /jails/templates/14.0-RELEASE/etc
root@on-host# cp /jails/media/localtime /jails/templates/14.0-RELEASE/etc
Update it (-b <base-dir>
means
chroot
ing into the <base-dir>
, that’s
why resolv.conf
is necessary in the
<base-dir>/etc
directory):
root@on-host# freebsd-update -b /jails/templates/14.0-RELEASE fetch install
src component not installed, skipped
Looking up update.FreeBSD.org mirrors... 3 mirrors found.
Fetching metadata signature for 14.0-RELEASE from update1.freebsd.org... done.
Fetching metadata index... done.
Inspecting system... done.
Preparing to download files... done.
The following files will be updated as part of updating to
14.0-RELEASE-p5:
... # cut for brevity
Installing updates...Scanning /jails/templates/14.0-RELEASE/usr/share/certs/untrusted for certificates...
Scanning /jails/templates/14.0-RELEASE/usr/share/certs/trusted for certificates...
done.
… and finally, snapshot it:
root@on-host# zfs snapshot storage-pool/jails/templates/14.0-RELEASE@base
I clone the snapshot created earlier to
/jails/containers/samba
dataset:
root@on-host# zfs clone storage-pool/jails/templates/14.0-RELEASE@base storage-pool/jails/containers/samba
and install Samba
into it:
root@on-host# pkg -c /jails/containers/samba/ install samba416-4.16.11_2
I use the -c <chroot-dir>
in the pkg
command to chroot
into the
/jails/containers/samba
, therefore Samba
is
actually installed in that directory and invoking smbclient
on the host results in an error:
root@on-host# smbclient --version
su: smbclient: not found
in contrast with invoking the same command while
chroot
ed:
root@on-host# chroot /jails/containers/samba/ smbclient --version
Version 4.16.11
That is something I really like about FreeBSD
- both
freebsd-update
and pkg
integrate with
chroot/jails
seamlessly.
Here are the contents of the smb4.conf
that need to be
put into /usr/local/etc
:
[global]
workgroup = WORKGROUP
server string = SambaHome
server role = standalone server
hosts allow = 192.168.120.
netbios name = SambaHome
wins support = no
security = user
passdb backend = tdbsam
encrypt passwords = yes
[libor]
path = /opt/libor
valid users = @owners
write list = libor
writable = yes
browsable = yes
guest ok = no
create mask = 0666
directory mask = 0755
[shared]
path = /opt/shared
valid users = guest @owners
write list = guest @owners
writable = yes
browsable = yes
guest ok = yes
guest user = guest
create mask = 0666
directory mask = 0775
This also governs our next steps:
ZFS
dataset for the /opt
directory in the jailowners
grouplibor
and guest
usersSamba
aware of the users via the
smbpasswd
command/opt/...
directory structure for each
userNote that all steps may be carried out from the host
system. When dealing with users, we just need to issue commands
prepended by chroot
.
I decided (arbitrarily) to place the Samba
root in the
/opt
directory, so I create a dataset:
root@on-host# zfs create storage-pool/jails/containers/samba/opt
and, since it will contain mostly larger files like photos, I set the
recordsize
dataset attribute to 1MB instead of the default
128KB. You can check the following article
for more details about setting the right recordsize
.
root@on-host# zfs set recordsize=1M storage-pool/jails/containers/samba/opt
Adding the owners
group:
root@on-host# chroot /jails/containers/samba pw group add owners
Adding libor
user (guest
is a copy-pasta,
it just does not belong to the owners
group):
root@on-host# chroot /jails/containers/samba adduser
Username: libor
Full name: Libor
Uid (Leave empty for default):
Login group [libor]:
Login group is libor. Invite libor into other groups? []: owners
Login class [default]:
Shell (sh csh tcsh nologin) [sh]: nologin
Home directory [/home/libor]: /nonexistent
Home directory permissions (Leave empty for default):
Use password-based authentication? [yes]: no
Lock out the account after creation? [no]: no
Username : libor
Password : <disabled>
Full Name : Libor
Uid : 1001
Class :
Groups : libor owners
Home : /nonexistent
Home Mode :
Shell : /usr/sbin/nologin
Locked : no
OK? (yes/no) [yes]: yes
adduser: INFO: Successfully added (libor) to the user database.
Add another user? (yes/no) [no]: yes
... # dialog starts over again...
creating the directories and recursively chown
ing
them:
root@on-host# chroot /jails/containers/samba mkdir /opt/libor /opt/shared
root@on-host# chroot /jails/containers/samba chown -R libor:owners /opt/libor
root@on-host# chroot /jails/containers/samba chown -R guest:owners /opt/shared
and registering the users to Samba
via
smbpasswd
:
root@on-host# chroot /jails/containers/samba smbpasswd -a libor
New SMB password:
Retype new SMB password:
Added user libor.
I copy the /jails/media/samba/rc.conf
to
/jails/containers/samba/etc
. It’s a minimal init script,
with auxiliary services disabled (sshd
,
syslogd
, sendmail
, cron
).
Samba
is enabled on the last line:
sshd_enable="NO"
sendmail_enable="NONE"
sendmail_submit_enable="NO"
sendmail_outbound_enable="NO"
sendmail_msp_queue_enable="NO"
growfs_enable="NO"
syslogd_enable="NO"
cron_enable="NO"
samba_server_enable="YES"
Lastly, I create a snapshot the
storage-pool/jails/containers/samba
dataset to save myself
some trouble in case I botch something in the future:
root@on-host# zfs snapshot storage-pool/jails/containers/samba@base
Almost everything is now (finally!) in place to start the jail. This
follows closely VNET
section in the Handbook
,
so if details are unclear, refer to it.
We first need to create a network bridge
device and
attach it to the network interface of the RPi. Since I want it to be a
permanent change, I put the commands into /etc/rc.conf
directly:
root@on-host# cat <<EOF >> /etc/rc.conf
defaultrouter="192.168.120.1"
cloned_interfaces="bridge0"
ifconfig_bridge0="inet 192.168.120.120/24 addm genet0 up"
EOF
as well as enable jails in /etc/rc.conf
:
root@on-host# cat <<EOF >> /etc/rc.conf
jail_enable="YES"
jail_parallel_start="YES"
EOF
Next, I need make sure all jail configurations are taken into account:
root@on-host# echo '.include "/etc/jail.conf.d/*.conf";' >> /etc/jail.conf
And create the following /etc/jail.conf.d/samba.conf
file, conforming to the jail.conf(5)
format:
samba {
exec.start = "/bin/sh /etc/rc";
exec.stop = "/bin/sh /etc/rc.shutdown";
exec.consolelog = "/var/log/jail_console_${name}.log";
enforce_statfs = "1";
allow.raw_sockets;
exec.clean;
allow.mount;
allow.mount.devfs = 1;
allow.mount.fdescfs;
mount.devfs;
devfs_ruleset = 5;
path = "/jails/containers/${name}";
host.hostname = "${name}";
vnet;
vnet.interface = "${epair}b";
$id = "123";
$ip = "192.168.120.${id}/24";
$gateway = "192.168.120.1";
$bridge = "bridge0";
$epair = "epair${id}";
exec.prestart += "ifconfig ${epair} create up";
exec.prestart += "ifconfig ${epair}a up descr jail:${name}";
exec.prestart += "ifconfig ${bridge} addm ${epair}a up";
exec.start += "ifconfig ${epair}b ${ip} up";
exec.start += "route add default ${gateway}";
exec.poststop = "ifconfig ${bridge} deletem ${epair}a";
exec.poststop += "ifconfig ${epair}a destroy";
}
With this file, I declare I want a VNET
jail with the
name samba
, whose base directory (chroot
) will
be /jails/containers/samba
, it will have a static IP
address 192.168.120.123
and all its logs will appear in
/var/log/jail_console_samba.log
on the host (lines: 18, 1, 15 and 22, 4).
Said jail communicates with the host via an epair(4)
,
which is a virtual Ethernet connection, with one end -
epair123a
- residing in the host network stack and the
other - epair123b
- in the jail network stack, connecting
them this way.
Setup before jail start (on the host) is specified on lines 27-29, after the jail start (within the jail!)
on lines 30-31 and after the jail stop (on
the host) on lines 32-33. They are concerned
with creation/destruction of the epair
, connecting
to/disconnecting from the bridge and setting the default route in the
jail.
Lines 6-13 are what’s needed to run
Samba
successfully in a jailed environment, otherwise it
complains about missing privileges for mounting and other stuff.
Frankly, it’s bit unclear to me what all the options mean and whether
they are really necessary, so I prescribe myself a homework to figure it
out. My assumption is something about Samba
running the rpcbind(8)
under the hood, therefore maybe needing raw sockets (?) and mounting
file systems, therefore needing access to the host’s /dev
filesystem, but I dunno.
jail(8)
seems like a good place to start.
OK, let’s stand up the whole show!
root@on-host# service jail start samba
Starting jails: samba.
Seems to work. Let’s check the logs:
root@on-host# cat /var/log/jail_console_samba.log
ELF ldconfig path: /lib /usr/lib /usr/lib/compat /usr/local/lib /usr/local/lib/samba4
32-bit compatibility ldconfig path: /usr/lib32
Starting Network: lo0 epair123b.
lo0: flags=1008049<UP,LOOPBACK,RUNNING,MULTICAST,LOWER_UP> metric 0 mtu 16384
options=680003<RXCSUM,TXCSUM,LINKSTATE,RXCSUM_IPV6,TXCSUM_IPV6>
inet 127.0.0.1 netmask 0xff000000
inet6 ::1 prefixlen 128
inet6 fe80::1%lo0 prefixlen 64 scopeid 0x6
groups: lo
nd6 options=21<PERFORMNUD,AUTO_LINKLOCAL>
epair123b: flags=1008842<BROADCAST,RUNNING,SIMPLEX,MULTICAST,LOWER_UP> metric 0 mtu 1500
options=8<VLAN_MTU>
ether 02:fb:c0:41:5a:0b
groups: epair
media: Ethernet 10Gbase-T (10Gbase-T <full-duplex>)
status: active
nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>
add host 127.0.0.1: gateway lo0 fib 0: route already in table
add host ::1: gateway lo0 fib 0: route already in table
add net fe80::: gateway ::1
add net ff02::: gateway ::1
add net ::ffff:0.0.0.0: gateway ::1
add net ::0.0.0.0: gateway ::1
Clearing /tmp (X related).
Updating motd:.
Updating /var/run/os-release done.
Creating and/or trimming log files.
Performing sanity check on Samba configuration: OK
Starting nmbd.
Starting smbd.
Tue Apr 9 22:41:26 CEST 2024
add net default: gateway 192.168.120.1
You can see the epair123b
end of the epair
as the primary interface, smbd/nmbd
starting,
samba
config is actually OK, etc…
However, we cannot see the IP address (yet), so I issue ifconfig(8)
with -j <jail_name>
option, meaning it will be
executed in the supplied jail:
root@on-host# ifconfig -j samba
lo0: flags=1008049<UP,LOOPBACK,RUNNING,MULTICAST,LOWER_UP> metric 0 mtu 16384
options=680003<RXCSUM,TXCSUM,LINKSTATE,RXCSUM_IPV6,TXCSUM_IPV6>
inet 127.0.0.1 netmask 0xff000000
inet6 ::1 prefixlen 128
inet6 fe80::1%lo0 prefixlen 64 scopeid 0x7
groups: lo
nd6 options=21<PERFORMNUD,AUTO_LINKLOCAL>
epair123b: flags=1008843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST,LOWER_UP> metric 0 mtu 1500
options=8<VLAN_MTU>
ether 02:9f:23:30:dc:0b
inet 192.168.120.123 netmask 0xffffff00 broadcast 192.168.120.255
groups: epair
media: Ethernet 10Gbase-T (10Gbase-T <full-duplex>)
status: active
nd6 options=29<PERFORMNUD,IFDISABLED,AUTO_LINKLOCAL>
Now the jail has an IP address we assigned it -
192.168.120.123
, as the epair123b
went from
DOWN
to UP
.
Let’s also check what ports is the jail listening on with sockstat(1)
.
root@on-host# sockstat -j samba -l -w # again, `-j <jail_name>` executes the command in the jail
USER COMMAND PID FD PROTO LOCAL ADDRESS FOREIGN ADDRESS
root smbd 2579 5 dgram /var/db/samba4/private/msg.sock/2579
root smbd 2578 5 dgram /var/db/samba4/private/msg.sock/2578
root smbd 2555 5 dgram /var/db/samba4/private/msg.sock/2555
root smbd 2555 28 tcp6 *:445 *:*
root smbd 2555 29 tcp6 *:139 *:*
root smbd 2555 30 tcp4 *:445 *:*
root smbd 2555 31 tcp4 *:139 *:*
root nmbd 2551 5 dgram /var/db/samba4/private/msg.sock/2551
root nmbd 2551 13 udp4 *:137 *:*
root nmbd 2551 14 udp4 *:138 *:*
root nmbd 2551 15 udp4 192.168.120.123:137 *:*
root nmbd 2551 16 udp4 192.168.120.255:137 *:*
root nmbd 2551 17 udp4 192.168.120.123:138 *:*
root nmbd 2551 18 udp4 192.168.120.255:138 *:*
root nmbd 2551 19 stream /var/run/samba4/nmbd/unexpected
I don’t see anything unexpected. Ports 137, 138, 139 and 445 are all
Samba
-related.
When I navigate in my laptop’s file explorer to
smb://192.168.120.123
, I see 2 directories
(Samba
shares) - libor
and
shared
.
Trying to enter the libor
directory results in a
password prompt. guest
user, even with correct password, is
rejected, even though shared
directory is accesible by
them.
Authenticating as libor
renders all shares accesible and
writable.
So … LGTM?
If I now test the upload download speed from my laptop, I get the following:
40 MB/s? Not too shabby. Let’s see upload:
55 MB/s. Cool!
Of course, it’s just me uploading 1 (!) big file, so performance may be wildly different on multiple smaller files, but I think for home use, it is more than sufficient.
I enjoyed this exercise really, really much. Sure, there were some
hiccups (patching the /etc/rc.d/zpool
…), but overall,
FreeBSD
is such a pleasant, coherent operating system that
I will rely on more in the future and build more services atop of.
Also, ZFS
innovation token seems to pay off - it sure is
daunting to get started with, but its flexibility and the fact that so
many features are baked in makes me really curious about it and I want
to dig deeper into its functionality.
The goal post of potential improvements is always moving, that’s a universal law. So, I came up with these enhancements:
To be even more sure about the durability of the data stored on the disks, I would like to create backups that are geographically separated from my closet.
To achieve that, I would like to set up a WireGuard
virtual
private network, connect the file server and other machines to it and
send snapshots to a dedicated machine via zfs-send.8
.
I hope you find this article helpful. Stay tuned for next!
Big thanks go to my wife for providing stylistic guidance as well as picture edits.