Hetzner routed IPv4 network on FreeBSD

I have my personal Internet-side infrastructure hosted at Hetzner where I’ve been a happy customer for years now. Their bare metal server and network options hit the right price level for me, their support is good, and above all they’re reliable.

I use FreeBSD on both of the bare metal servers I have with them, and one thing that’s tripped me up for a long time was how to correctly configure a server for one of the additional routed IPv4 blocks you can provision. In previous iterations of my server configuration, I bridged the network directly to the cloned interface that I attached my VMs to. That worked for the most part, but every time a VM rebooted, packets with MACs from both the bridge interface and the VM’s virtual NIC interface would show up on the physical interface.

Hetzner rightly monitor that physical port, so those stray packets would trip their monitoring system and I’d get an email demanding an explanation and an associated remedy. For a long time I thought it was a bug in FreeBSD 11 rather than a misconfiguration on my part so I just put up with it. I’d get the email saying I’d tripped the monitor, respond to it and say it was a bug after capturing traffic on the physical port to make sure the stray packets had stopped, and Hetzner would validate that and close the ticket, accepting my explanation.

The thing I was getting wrong is right there in the name: the net block you can provision is a routed network. So bridging it to the server is wrong; you need to route it instead. Here’s how to set that up on a FreeBSD server.

You need three key pieces of information, all of them IPv4 related: the gateway IP address on the Hetzner network that you route through, the primary IPv4 address that Hetzner assign to your server, and the additional net block that you want to add to your server.

This configuration goes in your /etc/rc.conf.

# setup the key information
gateway_ip="<IPv4>"
primary_ip="<IPv4>"
primary_if="<physical interface>" # for example: igb0
routed_net="<network CIDR>" # for example: 8.9.10.11/29

# bring up the primary interface with the primary IPv4
ifconfig_igb0="inet $primary_ip/32 up"

# create up a bridge interface
cloned_interfaces="bridge0"
# add the routed block's network address to it
ifconfig_bridge0="inet $routed_net up"

# setup the routes we need
static_routes="gateway default vms"

# build those routes
route_gateway="-host $gateway_ip -interface $primary_if"
route_default="default $gateway_ip"
route_vms="-net $routed_net $primary_ip"

In the example above, you bring up the physical interface with its IP, setup a bridge interface for your VMs to attach to, then create the default route out to Hetzner and on to the Internet, and the route for the extra net block you want to use.

If you want to use the IPv6 block that Hetzner delegate to each server, that’s simple too. I use the first IP in the delegated block on the physical interface and then give the rest of the network to the bridge, like so:

# describe the IPv6 net
ipv6_net="<IPv6 net>" # for example: 2a01:4f8:1111:2222::

# assign the first address to the physical interface
ifconfig_igb0_ipv6="inet6 $ipv6_net::1/128"

# assign the rest to the bridge
ifconfig_bridge0_ipv6="inet6 $ipv6_net::2/64"

# remember to configure the default IPv6 router
ipv6_defaultrouter="fe80::1%$gateway_if"

Hetzner’s network is peculiar in how they assign and route IPv4 and IPv6 to their bare metal servers, so a trip to their own documentation is highly recommended to get the context for the above. Single IP addresses are treated differently, for example.