Limit SSH to IPv6 on FreeBSD

When I first set up my server, I got numerous daily attempts to login via SSH almost immediately. None of them were successful, because I use public key authentication, of course, but the log spam was annoying nonetheless. The problem is with only 232 addresses, scanning all possible IPv4 addresses for open ports is quite easy.

But, yet again, IPv6 to the rescue. It has 2128 possible addresses, so scanning all of them for open ports is doable but takes a significant amount of time. My server has a /64 subnet, so even if someone knows the prefix, there are still 264 addresses to scan. Both my home internet and my cell phone provider support IPv6 (as they should), so limiting SSH to a spare IPv6 address was the obvious solution.

This is not an approach for security, but merely to get rid of the annoying login attempts. The server still has to be secured. No one should believe that trying to “hide” the IP address does anything for security. If anyone really wants to find it, he will. But it helped to reduce the random failed login attempts, from a couple of hundreds on bad days, to not a single one in more than a year. So without all this noise in the logs, I can more easily see if anyone is actually trying to get into my server now.

The first step is to add another IPv6 address to the network interface, which can be used for SSH. Let’s assume the IPv6 prefix for the /64 subnet is 2001:10:20:30. All regular services might use 2001:10:20:30::1, so 2001:10:20:30::2222 could be reserved for SSH only. The additional address can be added in the /etc/rc.conf, assuming the network interface is em0:

ifconfig_em0_ipv6="inet6 2001:10:20:30::1/64"
ifconfig_em0_alias0_ipv6="inet6 2001:10:20:30::2222/64"

The networking can be restartet with:

# service netif restart

Alternatively, the server can also be rebooted. Now ifconfig should list the additional address:

# ifconfig em0
em0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500
ether 96:00:00:aa:bb:cc
hwaddr 96:00:00:aa:bb:cc
inet6 2001:10:20:30::1 prefixlen 64 
inet6 fe80::9400:ff:feaa:bbcc%vtnet0 prefixlen 64 scopeid 0x1 
inet6 2001:10:20:30::2222 prefixlen 64 
inet netmask 0xffffffff broadcast 
media: Ethernet 10Gbase-T <full-duplex>
status: active

You can also try to ping the new address and it should work fine. If the services on your server are not bound to specific IP addresses, but to any, now is the time to fix that. The other services should only use the 2001:10:20:30::1 address, and 2001:10:20:30::2222 is for SSH exclusively. sockstat is your friend.

With the new IPv6 address up and running, SSH can be configured to only listen to it and nothing else. This is done by having only a single ListenAddress parameter in /etc/ssh/sshd_config:

ListenAddress 2001:10:20:30::2222

Save the config file and be sure you can access your server directly or via a serial console, in case you made a mistake and lock yourself out. Then restart SSH to apply the changes:

# service ssh restart

You can check with sockstat if sshd is only listening to the new IPv6 address:

# sockstat -l | grep sshd
root sshd 34490 3 tcp6 2001:10:20:30::2222:22 *:*

If it is listening to anything else, check your sshd_config file.

The only way to access the server via SSH is by using the new dedicated IPv6 address:

$ ssh 2001:10:20:30::2222

This is inconvenient, so I recommend adding a new subdomain, like, with a single AAAA record:

$ host has IPv6 address 2001:10:20:30::2222

With these little changes the amount of random login attempts can be significantly reduced. However, it only works when all of your devices from which you manage the server support and use IPv6. But that should be the standard nowadays.

IPv6 with Hurricane Electric 6in4 tunnel on DD-WRT

My Asus WL500g Deluxe router is running an Eko build of DD-WRT, which comes with IPv6 support. They haven’t yet included it in the official releases for some reason. However, there is still no ISP offering native IPv6 here. It’s a shame. So some sort of tunnel is needed to finally equip my LAN with an IPv6 subnet.

I didn’t really trust those tunnel brokers, so I tried a 6to4 tunnel. There is a good tutorial and only a few scripts are needed to get it running. The problem was that my IPv4 address is dynamic and changes with every reconnect, which is also enforced every 24h. Unfortunately with 6to4 this also changes the IPv6 prefix since it includes the v4 address. That’s no fun and it also messes up the routing and clients using automatic address assignment. Ok, I need a tunnel broker.

My first try was SIXXS, which took like forever to get everything approved. After finishing the ‘paperwork’, my very own /48 subnet was ready. Some hours later I figured out how to run their proprietary aiccu client on my router. It basically worked, but the connection was slow and the client crashed again and again. The port for MIPS-based routers was from 2007 and I couldn’t find a more recent version. It was very disappointing. So hopefully Hurricane Electric will do better.

The tunnel request is pretty easy and instant. You don’t have to wait a week or two until everything is approved. There is no special software, so you have to rely on the tunneling features of your operating system, that is good. DD-WRT has no GUI to configure tunnels, so some scripts are necessary. The IPv6 tutorial for HE tunnels on DD-WRT is not very good, but I finally figured out how it works. To my surprise this solution is very stable and also very fast. The latency with IPv6 is sometimes even lower than with native IPv4. They must have a really good routing.

So here’s what needs to be done:

  • Get an Eko build running for IPv6 support. For my Asus router it’s the VINT build in V24_TNG.
  • Go to Administration and Management and under IPv6 Support, enable IPv6, Radvd and use the following for Radvd config:
    interface br0 {
       MinRtrAdvInterval 3;
       MaxRtrAdvInterval 10;
       AdvSendAdvert on;
       prefix <Your subnet prefix>::/64 {
          AdvOnLink on;
          AdvAutonomous on;

    This enables Router advertisements, so all the clients can assign an address automatically. Note that this only works with /64 subnets. If you have an /48 subnet you can only use one /64 part of it. Make sure that br0 really is your LAN interface and replace it with the correct identifier otherwise.

  • Go to Commands under Administration and add the following Startup Script:
    /usr/bin/wget '
    ipv4b=AUTO&pass=<your password as MD5 hash>&
    user_id=<your user id>&tunnel_id=<your tunnel id>' -O -
    if [ -n `ifconfig | grep 'he-ipv6'` ]
       insmod ipv6
       sleep 3
       ip tunnel add he-ipv6 mode sit remote <HE IPv4 tunnel endpoint> ttl 64
       ip link set he-ipv6 up
       ip addr add <your IPv6 tunnel endpoint>/64 dev he-ipv6
       ip route add ::/0 dev he-ipv6 metric 1
       ip -6 addr add <your router's IPv6 address>/64 dev br0
    radvd -C /tmp/radvd.conf &

    I have an USB stick attached, and placed everything above in a script file. Makes it a little clearer. Make sure the wget call has no line breaks like displayed here (due to limited space).

  • Add the following Firewall rules on the same page:
    iptables -I INPUT 2 -p ipv6 -i br0 -j ACCEPT
    iptables -I INPUT 2 -p ipv6 -i ppp0 -j ACCEPT

That’s it already. Reboot the router and IPv6 should work fine on the router itself and on clients which automatically assigned themselves addresses of the subnet announced by radvd.

For increased reliability, I also added a script which is run as a cron job every couple of minutes:

IP=<HE IPv6 tunnel endpoint>

RESULT=`ping6 -c 3 $IP | grep '64 bytes'`

if [ -n $RESULT ]
  /usr/bin/wget '
  ipv4b=AUTO&pass=<your password as MD5 hash>&
  user_id=<your user id&tunnel_id=<your tunnel id>' -O -

This tries to ping the tunnel endpoint of HE three times. If it fails, the website to update the IP address is called, because probably the IPv4 address changed without the Startup Script being run.