FreeBSD IPv4 and IPv6 Setup on Multi-homed Laptop and Firewall

Now that ACPI suspend/resume works on my Asus X53E laptop with FreeBSD 10.0-CURRENT, one thing began to annoy me — if I changed networks, I would have to su(1), and manually start dhclient(8) to obtain a DHCP lease on the "new" network.

This "non-blog" entry covers how I achieved seamless network changes, as well as setting up a multi-homed firewall with IPv4 and IPv6 dhcpd services.

Laptop Configuration

Configuring my laptop was fairly straight forward. In fact, there already exists an entry in the FreeBSD Handbook on configuring failover wireless and wired networks.

First, I had to create the failover device, lagg(4). This rc.conf(5) entry got things working for IPv4. In my setup, alc(4) is the wired interface, and ath(4) is the wireless (which eventually is used as device wlan(4)).

  ## /etc/rc.conf:
  # wired interface
  ifconfig_alc0="up"
  # wireless interface, cloning MAC address from wired
  ifconfig_ath0="ether 00:de:ad:be:ef:00"
  # wlan(4) device set to ath0
  wlans_ath0="wlan0"
  # wlan0 interface set to use wpa_supplicant(5)
  ifconfig_wlan0="WPA"
  # set up the lagg(4) failover device
  cloned_interfaces="lagg0"
  # set up lagg0 to use alc0 as primary interface, wlan0 as failover with
  # DHCP-assigned address
  ifconfig_lagg0="inet laggproto failover laggport alc0 laggport wlan0 DHCP"

This worked quite well for IPv4, however IPv6 DHCP leases would not be obtained. In order for dhcpd6 leases to be obtained on the laptop, the net/isc-dhcp42-client port is needed, since the base FreeBSD dhclient(8) code does not support IPv6 leases. After installing the port, these lines were added to rc.conf(5):

  ## /etc/rc.conf, continued:
  # add ipv6 entry for lagg0
  ipv6_network_interfaces="lagg0"
  # set up lagg0 to accept rtadv(8) advertisements
  ifconfig_lagg0_ipv6="inet6 accept_rtadv"
  # hard-code an IPv6 link-local address, which is used when dhcpd6 requests are
  # sent to the dhcpd6 server
  ifconfig_lagg0_ipv6_alias0="inet6 fe80::5604:a6ff:fe3a:96ea%lagg0"
  # optional: configure to prever ipv4, ipv6, or AUTO
  ip6addrctl_enable="YES"
  ip6addrctl_policy="ipv6_prefer"

Note: At this time, I am not entirely certain setting the link-local address for lagg(4) is either neccessary or the right thing to do. In my setup, it is the same link-local address as alc0.

Finally, for obtaining a dhcpdv6 lease from the firewall, I created a startup script for lagg(4), which is run every time the interface is configured:

  #!/bin/sh
  # /etc/start_if.lagg0
  if [ -e /var/run/dhclient6.lagg0.pid ]; then
      kill `cat /var/run/dhclient6.lagg0.pid`
  fi
  /usr/local/sbin/dhclient -6 -lf /var/db/dhclient6.leases -pf \
    /var/run/dhclient6.lagg0.pid lagg0 2>&1 >/dev/null

Firewall Configuration

Configuring the firewall was far trickier.

My firewall runs the following services for my network:

  • hostapd
  • dhcpd
  • dhcpd6
  • rtadvd
  • pf

First, rc.conf(5) entries are created for the firewall interfaces. In my case, em0 and wlan0 are configured with if_bridge(4) to allow serving the same DHCP services across both wired and wireless segments.

  ## /etc/rc.conf:
  # set up if_bridge(4)
  cloned_interfaces="bridge0"
  wlans_ath0="wlan0"
  # set the hostapd(8) wlan(4) mode
  create_args_wlan0="wlanmode hostapd channel 11"
  # bring up em0 and wlan0
  ifconfig_em0="up"
  ifconfig_wlan0="up"
  # add em0 and wlan0 to bridge0
  ifconfig_bridge0="addm em0 addm wlan0 up"
  # give bridge0 its IPv6 address
  ifconfig_bridge0_alias0="inet 10.10.0.1 netmask 255.255.255.0"
  ifconfig_bridge0_ipv6="inet6 fe80::2b:5bff:fe54:4400%bridge0"
  ifconfig_bridge0_alias1="inet6 2001:0:0:5:1:2:3:1 prefixlen 96"
  hostapd_enable="YES"
  dhcpd_enable="YES"
  dhcpd_ifaces="bridge0"
  dhcpd6_enable="YES"
  dhcpd6_ifaces="bridge0"
  gateway_enable="YES"
  ipv6_gateway_enable="YES"
  sshd_enable="YES"
  rtadvd_enable="YES"
  rtadvd_flags="-s"
  rtadvd_interfaces="bridge0"
  update_motd="NO"
  forward_sourceroute="YES"
  pf_enable="YES"

Note: At this point, you should be able to test the connection from the client machine. If you see the following message, it means the bridge0 interface does not have a properly configured link-local IPv6 address:

  # rtsol -D lagg0
  checking if lagg0 is ready...
  lagg0 is ready
  set timer for lagg0 to 0s
  New timer is 0s
  timer expiration on lagg0, state = 1
  send RS on lagg0, whose state is 2
  set timer for lagg0 to 4s
  New timer is 4s
  invalid RA with non link-local source from 2001:0:0:5:1:2:3:1 on lagg0

In such a case, manually add the link-local address.

  # ifconfig bridge0 inet6 fe80::2b:5bff:fe54:4400%bridge0 alias

Then the IPv6 router advertisements should work as expected.

  # rtsol -D lagg0
  checking if lagg0 is ready...
  lagg0 is ready
  set timer for lagg0 to 0s
  New timer is 0s
  timer expiration on lagg0, state = 1
  send RS on lagg0, whose state is 2
  set timer for lagg0 to 4s
  New timer is 4s
  received RA from fe80::2b:5bff:fe54:4400 on lagg0, state is 2

Next, I set up hostapd(8):

  ## /etc/hostapd.conf:
  interface=wlan0
  driver=bsd
  logger_syslog=-1
  logger_syslog_level=0
  logger_stdout=-1
  logger_stdout_level=0
  dump_file=/tmp/hostapd_wlan0.dump
  ctrl_interface=/var/run/hostapd
  ctrl_interface_group=wheel
  ssid=mysecretssid
  debug=
  auth_algs=1
  wpa=2
  wpa_key_mgmt=WPA-PSK
  wpa_pairwise=CCMP
  wpa_group_rekey=600
  wpa_gmk_rekey=3600
  wpa_passphrase=mysecretpassphrase
  ieee8021x=

Then, I set up a basic pf.conf:

  ## /etc/pf.conf:
  nat on em0 inet from !em0 to any -> em0:0
  nat on bridge0 inet from bridge0 to !bridge0 -> 10.0.0.1
  pass in quick inet proto icmp icmp-type echoreq
  pass in quick inet6 proto icmp6 icmp6-type {2,routeradv,echoreq,neighbrsol,neighbradv}

Note: icmp6-type "2" is "packet-too-large". If using a tunnel broker service that provides IPv6 over IPv4, this is necessary since IPv6 packets over such tunnels have additional header/trailer additions. Also note, for my setup, I needed to change the MTU size to 1280, which was done through my tunnel provider, HE.net.

Finally, dhcpd and dhcpd6 configurations:

  ## /usr/local/etc/dhcpd.conf:
  option domain-name "mydomain.local";
  option domain-search-list code 119 = text;

  default-lease-time 7200;
  max-lease-time 86400;
  log-facility local7;
  ddns-update-style none;
  one-lease-per-client true;
  deny duplicates;
  ping-check true;
  authoritative;
  subnet 10.10.0.0 netmask 255.255.255.0 {
    pool {
      range 10.10.0.31 10.10.0.100;
    }
    option routers 10.10.0.1;
    option domain-name-servers 8.8.8.8;
  }
  ## /usr/local/etc/dhcpd6.conf:
  option domain-name "mydomain.local";
  option domain-search-list code 119 = text;

  default-lease-time 7200;
  max-lease-time 86400;
  log-facility local7;
  ddns-update-style none;
  one-lease-per-client true;
  deny duplicates;
  ping-check true;
  authoritative;
  subnet6 2001:0:0:5:1:2::/96 {
    range6 2001:0:0:5::1 2001:0:0:5:ffff:ffff:ffff:ffff;
    option dhcp6.name-servers 2001:0:0:5:1:2:3:1;
  }

At this point, I gave both the firewall and laptop the old reboot test to be sure my setup was retained. I was quite pleased with the result — and now I can happily move from wired to wireless networks while retaining my IPv4 and IPv6 DHCP leases, without restarting any services.