Lab Notes

Various personal weekend projects

Sep 02, 2024

Using FreeBSD on Azure as an IPv4 to IPv6 SSH tunnel

When away from your home network, there are multiple options available for remote access. My choice is to use SSH to connect via IPv6.

My home firewall (OpenBSD) passes in an IPv6 port to a FreeBSD jail that runs a hardened sshd daemon on my IPv6-only home lab VLAN. When I'm away, my laptop usually has IPv6 via my tethered mobile phone and I can connect directly via IPv6. I can also use an SSH tunnel for things like connecting remotely to Home Assistant or RDP'ing into my home Windows client. When I can't get IPv6 when away, I need a way to bridge from IPv4 to IPv6.

To gain access to my IPv6-only network remotely when I only have IPv4, I use a dual-stack VM as a bridge. Here are some notes how I use an Azure VM running FreeBSD for this purpose. Also, FreeBSD 14 supports .NET, so I run a simple website for my home weather station data. And finally, with ZFS as the root partition (zroot), I have all the goodness of ZFS snapshots, etc.

Creating an Azure FreeBSD VM with a ZFS root

When creating a VM in Azure, you can choose several operating systems in Marketplace including FreeBSD. In my case I chose FreeBSD 14.1-RELEASE (ZFS) on x64. There is also an ARM64 option, but I want to run .NET 8 workloads and this is not yet available on ARM64. I chose a B-series size with 1gb. This is more than enough RAM for this purpose.

Choose to only use an SSH key to access the VM. I also enabled a System Assigned Identity since my VM needs read access to a keyvault secret and a storage account. Otherwise, it's fine to accept the defaults for remaining properties.

Once your shiny new FreeBSD VM is up and running and you access via SSH, the first thing I recommend is changing the default SSH server port. Edit /etc/ssh/sshd_config and change Port from 22 to some other higher port number. This minimizes random scans from showing up in /var/log/auth.log. Now reboot your VM and add a custom network security group inbound port rule that matches the new port you chose. You can also change the existing SSH rule from Allow to Deny (or simply delete it).

Adding a public IPv6 address

By default, Azure will create an IPv4-only VM. What you need to do is add a new IP configuration to your network interface that has an IPv6 address. The first step is to add an IPv6 address space to your virtual network (VNET) address spaces. I added 2404:f800:0:1::/64.

Next, edit your VNET subnet (likely named default) and enable IPv6 addresses. Once set, you can then edit the subnet settings again and add a public IPv6 address. I chose a dynamic IPv6 address and it has been stable.

Now that the VM configuration has IPv6 set, we next need to configure our FreeBSD VM to receive an IPv6 address from Azure. An Azure VM first needs an internal IPv6 address provided via the address space we added to our VNET address spaces and IPv6 IP configuration. We first need to run sudo pkg install dual-dhclient.

Once you have dual-dhclient installed, add the following to your /etc/rc.conf:

ipv6_activate_all_interfaces="YES"
ifconfig_hn0_ipv6="SYNCDHCP accept_rtadv"
dhclient_program="/usr/local/sbin/dual-dhclient"

Now reboot and when you log back in do an ifconfig hn0 and you'll see the internal IPv6 we configured, e.g. inet6 2404:f800:0:1::4 prefixlen 128. Your VM now has a local IP that gets you to your VNET's public facing IPv6 that you previously configured in your VNET subnet. Do a simple test:

ping6 -c 3 freebsd.org
PING(56=40+8+8 bytes) 2404:f800:0:1::4 --> 2610:1c1:1:606c::50:15
16 bytes from 2610:1c1:1:606c::50:15, icmp_seq=0 hlim=43 time=69.095 ms
16 bytes from 2610:1c1:1:606c::50:15, icmp_seq=1 hlim=43 time=68.517 ms
16 bytes from 2610:1c1:1:606c::50:15, icmp_seq=2 hlim=43 time=68.514 ms

--- freebsd.org ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss

Cool, we have a dual-stack VM. Now we need to connect your home lab SSH server to your Azure FreeBSD VM.

Adding an SSH tunnel to connect to your FreeBSD VM

What we'll do is add an SSH reverse tunnel from your SSH server to your FreeBSD VM. I created an A record for my domain (e.g. idatum.net) that has the public IPv4 for the FreeBSD VM. I don't need to add an AAAA record, since the point of this is an IPv4 to IPv6 bridge. With NAT64/DNS64 on my IPv6-only VLAN, my SSH server gets the IPv6 with the DNS64 prefix I have in my OpenBSD router unbound.conf:

# DNS64
module-config: "dns64 iterator"
dns64-prefix: 64:ff9b::/96

And on the same OpenBSD router, /etc/pf.conf:

# NAT64
pass in inet6 from any to 64:ff9b::/96 af-to inet from ($int_if)

Now add your home SSH server's client public SSH key to your FreeBSD's ~/.ssh/authorized_keys. This let's the SSH client on your server connect to the FreeBSD VM.

I wrote a script that sets up the reverse SSH tunnel, running as the same user that has the SSH client key configured on the VM:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/bin/sh
# Open local port on FreeBSD VM to connect to this home SSH server.
host=your-freebsd-vm-dns-name.net
home_ssh_port=22
vm_ssh_port=5001
vm_local_port=5002

while [ true ]
do
    ssh -NT -R $vm_local_port:localhost:$home_ssh_port \
        -o ExitOnForwardFailure=yes \
        -o ServerAliveInterval=2 \
        -o ServerAliveCountMax=2 \
        -6 -p $vm_ssh_port azureuser@$host
    sleep 1
done

host is your DNS record that has your VM's IPv4 address. home_ssh_port is the home server SSH port (default port 22). vm_ssh_port is the port we configured earlier in your VM's NSG rules and sshd_config. vm_local_port is the port available at localhost on your VM.

Once the SSH reverse tunnel is set up, you can log in from your FreeBSD VM to your home SSH server using port vm_local_port. From a remote client, use something like ssh -t -p 5001 azureuser@your-freebsd-vm-dns-name.net ssh -p 5002 your_home_username@localhost to log into your home SSH server directly. This connects to your FreeBSD VM and creates a terminal session via ssh with your home SSH server. You're in!

Additional tunnels

We now have a way to remotely connect to your home lab via a reverse SSH tunnel. What about remotely connecting to other home lab servers via your remote laptop? This needs another level if indirection with our SSH tunnels. Let's say you have Home Assistant running on an internal home lab address homeassistant.local. We need a way to tunnel from the home SSH server to that address and port. It looks like this:

remote client --> FreeBSD VM --> home SSH server --> homeassistant.local.

What we need is a local port on the home SSH server that connects to homeassistant.local -- an SSH local tunnel. What I do is run another script on the SSH server to set up the local SSH tunnel. Something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#!/bin/sh
# Open local port on home SSH server to connect to HA.
host=homeassistant.local
ha_port=8123

while [ true ]
do
    ssh -NT -L $ha_port:localhost:$ha_port ha_user@$host
    sleep 1
done

On the home SSH server, this sets up a local port ha_port that tunnels via ssh to the Home Assistant server.

On the FreeBSD VM, I set up an SSH tunnel to connect to the Home Assistant port on the home SSH server:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#!/bin/sh
# Open local port on FreeBSD VM to connect to HA port on home SSH server.
vm_local_port=5002
ha_port=8123

while [ true ]
do
    ssh -NT -L $ha_port:localhost:$ha_port \
        -o CheckHostIp=no \
        -o ExitOnForwardFailure=yes \
        -v \
        -p $vm_local_port home_user@localhost
    sleep 1
done

This uses the local SSH reverse proxy vm_local_port to your home SSH server to create a local ha_port. Now from your remote client, you can set up an SSH local port to tunnel all the way through so you can use your browser to connect to http://localhost:8123:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/bin/sh
# Client to open local HA port connected to HA port on FreeBSD VM connected to HA port on home SSH server.
host=your-freebsd-vm-dns-name.net
ha_port=8123
vm_ssh_port=5001

while [ true ]
do
    ssh -NT -L $ha_port:localhost:$ha_port \
        -o CheckHostIp=no \
        -o ExitOnForwardFailure=yes \
        -v \
        -4 -p $vm_ssh_port azureuser@$host
    sleep 1
done

Yep, this has a few layers! The key is to capture this in scripts with comments so you don't have to think about it everytime. On the servers, I run the scripts as daemons (@reboot in crontab works). Rest assured, I use this often to check in on my home automation status when remote with only IPv4 and it works great.

FreeBSD and .NET and ZFS

A quick note that FreeBSD has support for .NET. When I wrote this, I was running FreeBSD 14.1 and using .NET 8. Microsoft's .NET has been open source and supported on Linux for many years now. It's great to see the FreeBSD community and Microsoft work together to make .NET development available for FreeBSD.

I also really appreciate having ZFS on my VM. I can now create ZFS snapshots and do simple backups.

Conclusion

I'm happy with my Azure FreeBSD VM. When I'm remote and don't have IPv6, I can tunnel through my inexpensive VM and get access to my IPv6-only home lab. And I can also host my simple website I use for checking on my weather station.

There is only trust from my home SSH server to my FreeBSD VM. The FreeBSD VM is not trusted by my home server -- only by creating the SSH reverse tunnel can the VM access my home lab. My SSH server running in a FreeBSD jail has stict configuration including SSH key only authorization.

I'll find other uses for my FreeBSD VM running on Azure. Perhaps I'll start experimenting with sending ZFS snapshots from my home ZFS server for an additional backup.

Yes, I could have used a Linux VM on another cloud provider. I chose Azure and it delivers a solid FreeBSD VM with that familiar BSD experience.

Cheers to the BSDs!