Multiple virtual DHCP interfaces on a port [Gentoo]


My ISP, EPB of Chattanooga, will kindly provide more than one publicly routable IP address via DHCP. They do this, I think, in order to allow customers to use only a Layer-2 switch instead of a router to network all of their Windows boxen and game consoles, at their own Wild-West peril (observed at my boss’s house, which is how I got this idea). I’m not sure how many addresses EPB would hand out per fiber gateway, but I suspect it would be way more than I would ever use. Even with the imminent IPv4 crunch, we dine like kings…

I wanted to use this nice feature to multi-home my server. Never mind that IPv6 will obsolete the benefits. Name-based virtual hosting has its limitations (e.g. SSL) and I’m not waiting around!

One easy way to do this is to run virtual machines and bridge them to the port. But that has complications of its own and I didn’t feel like adding a new VM for every new low-traffic website I want to host. Of course, the VMs could be pretty small and optimized for routing, in which case I could run a bunch of them and send the traffic to a master Apache installation in Ring #0. But that’s not what I did.

So here’s the method:

1. Run multimac to give us virtual TAP ports with real-looking MACs.

/etc/init.d/multimac:

#!/sbin/runscript

opts="start stop restart"

depend() {
   need localmount
}

makemac() {
   killall -q multimac
   /usr/local/bin/multimac 1
   #one greater than eth1's MAC. The Gentoo scripts can do this as well but this seemed safer.
   ifconfig tap1 hw ether YOUR FAKE MAC HERE
}

start() {
   ebegin "Starting multimac"
   makemac
   eend $?
}

stop() {
   ebegin "Stopping multimac"
   killall -q multimac
   eend $?
}

restart() {
   ebegin "Restarting multimac"
   makemac
   eend $?
}
rc-update add multimac default

2. Policy routing for multiple uplinks

/etc/conf.d/net (excerpts):

modules=("dhcpcd")
dhcp="nontp nonis"

bridge_br1="eth1 tap0"
config_eth1=( "null" )
config_tap0=( "null" )
config_br1=( "dhcp" )
dhcpcd_br1="-L -t 0 -G"

#This is a static address "aliased" via "ip rule" to tap1
config_lo=(
   "172.16.0.60/32"
)

config_tap1=( "dhcp" )
#Supply a custom hostname so as not to confuse the DHCP server.
dhcpcd_tap1="-L -t 0 -G -h penguin2"
dhcp_tap1="nontp nonis nodns"

RC_NEED_tap0="multimac"
RC_NEED_br1="multimac net.eth1 net.tap0"
RC_NEED_tap1="multimac net.br1"

/etc/conf.d/local.start :

#The first three rules are for LAN and VPN traffic
ip rule add order 100 to 10.0.1/24 table main
ip rule add order 101 to 172.16.10/24 table main
ip rule add order 102 to 172.16.11/24 table main
ip rule add order 103 from 172.16.0.60 table tap1

Some iptables stuff (excerpts):

IPT=/sbin/iptables
#Traffic coming from the real world should have real addresses. This does basically the same thing as
#rp_filter but rp_filter is broken for multiple routing tables.
$IPT -N rp_filter_t
$IPT -A rp_filter_t -s 192.168.0.0/16 -j DROP
$IPT -A rp_filter_t -s 172.16.0.0/12 -j DROP
$IPT -A rp_filter_t -s 10.0.0.0/8 -j DROP
#else RETURN

for i in br1 tap1; do
   for j in FORWARD INPUT; do
      $IPT -A $j -i $i -j rp_filter_t
   done 
done

echo 0 > /proc/sys/net/ipv4/conf/all/rp_filter
echo 0 > /proc/sys/net/ipv4/conf/default/rp_filter

for j in br1 tap1; do
   $IPT -t nat -A POSTROUTING -o $j -j MASQUERADE
done;

#For Apache:
$IPT -t nat -A PREROUTING -i br1 -p tcp --dport 443 -j DNAT --to 10.0.1.1
$IPT -t nat -A PREROUTING -i br1 -p tcp --dport 80 -j DNAT --to 10.0.1.1
$IPT -t nat -A PREROUTING -i tap1 -p tcp --dport 80 -j DNAT --to 172.16.0.60

/etc/iproute2/rt_tables :

255	local
254	main
253	default
0	unspec
100	br1
101	tap1

3. dhcpcd’s run-hooks feature to reconfigure routes when addresses change

/etc/dhcpcd.exit-hook (where the action happens):

#!/bin/bash

#useful commands:
#   dhcpcd -V to see available variables.
#   dhcpcd -T to spit out all variables received by the DHCP server

echo `date` "$reason   $interface" >> /var/log/multi_epb_log

if [ -z "$new_ip_address" ] || [ "$reason" == "RENEW" ]; then
   #various events that don't result in new IPs
   exit
fi

save_tables() {
   ip route > $1
   echo >> $1
   echo "br1:" >> $1
   ip route show table br1 >> $1
   echo >> $1
   echo "tap1:" >> $1
   ip route show table tap1 >> $1
   echo >> $1
   echo "rules:" >> $1
   ip rule >> $1
   echo >> $1
   date >> $1
}

save_tables /pre_tables

#try to clear out some cruft--------
#one or more of these could cough up an error, but it does not indicate failure.

ip route flush table $interface

if [ "$interface" == "br1" ]; then
   #the main table default gw
   ip route del default
fi

if [ -n "$old_network_number" ] && [ -n "$old_subnet_cidr" ]; then
   OLD_NET_STR="$old_network_number/$old_subnet_cidr"
   #In the case that the old and new were the same subnet (maybe different IPs, though), there will be two almost-matching entries (save for the
   #"src" field present on the older one)
   #This deletes one of them and the other will be deleted below.
   ip route del "$OLD_NET_STR" dev $interface
fi

NET_STR="$new_network_number/$new_subnet_cidr"

#for instance, "ip route del 123.123.123.123/24 dev br1"--delete the route added by dhcpcd. Supply the dev in case
#another interface is on the same subnet.
ip route del "$NET_STR" dev $interface

#------------------
#now add some new stuff

if [ "$interface" == "br1" ]; then
   ip route add "$NET_STR" dev br1 src "$new_ip_address"
   ip route add default via "$new_routers" dev br1

   #maybe the only way to do this "right" would be to establish a mapping between interfaces and integers,
   #since this approach only works if there is only one simulated interface (tap1). Order/preference
   #numbers have to be unique.
   ip rule del order 200
   ip rule add from $new_ip_address table $interface order 200
else
   #the next route shouldn't be needed, but is referenced (hardlinked?) by the tap1 table gateway.
   #The only time it would ever be referenced would be for locally originated connections addressed
   #to tap1's subnet, if it is different from br1's. That is why tap1 needs a masquerade rule in iptables.
   ip route add "$NET_STR" dev $interface metric 2000 src "$new_ip_address"

   #see note above
   ip rule del order 201
   ip rule add from $new_ip_address table $interface order 201
fi

ip route add "$NET_STR" table $interface dev $interface
ip route add default via "$new_routers" table $interface dev $interface

#Critical! It seems like arp_filter should work, but it doesn't for some reason.
#I was prepared to implement this using arptables before I found out the kernel does it for me.
echo 2 > /proc/sys/net/ipv4/conf/"$interface"/arp_announce
echo 1 > /proc/sys/net/ipv4/conf/"$interface"/arp_ignore

#This should be off anyway, but make SURE! It's broken with multiple tables and iptables rules do the same thing.
echo 0 > /proc/sys/net/ipv4/conf/"$interface"/rp_filter

ip route flush cache

save_tables /post_tables

4. Bind Apache to an internal static IP and use DNAT

5. Finally, some gotcha’s

One interesting thing is that the netfilter PREROUTING hook apparently occurs *after* the RPDB lookup, since unless ip rule 103 from step 2 is included, the DNAT iptables rule is not sufficient to cause the tap1 table to be selected. So much for “pre” routing! See this. “Stateless” NAT seems like it would be a better method, but it’s been deprecated.

See the note about arp_announce and arp_ignore. I think that arp_filter may fail for the same reason as rp_filter in this scenario, since my arp_announce and arp_ignore options seem to describe what arp_filter should have done. Read about it in /usr/src/linux/Documentation/networking/ip-sysctl.txt .


One response to “Multiple virtual DHCP interfaces on a port [Gentoo]”

  1. For anyone curious as to the actual upper bound on DHCP addresses with EPB…it appears to be two :/ Still better than one, I guess.

Leave a Reply

Your email address will not be published. Required fields are marked *