Written by Steven N. Fettig
Rev. 1.1 - 8 May 2004
(See Creative Commons Copyright in right pane for copy restrictions)
Materials:
IBM Small Form Factor Workstation (Overkill)
Integrated Intel 10/100 NIC
Sangoma Dual CSU/DSU T1 PCI Card
Preface: As part of the Darien-WiFi project, I spent the past two weeks trying to install a router for our two T1's from Global Crossing. I had originally intended to use FreeBSD as the base system. What I found was that there was no (documentation showing a) way to do native load balancing across both T1's. The Sangoma Wanpipe drivers for Linux were able to do load balancing using the TEQL module (used in firewall QoS). Although an open supporter of what works - which can often be Linux - there was no way that I was going to use Linux on my router considering that I have had much, much more experience with FreeBSD and am familiar with locking it down for security uses. With quite a bit of Google digging, I found that OpenBSD'spf was not only capable of doing load balancing, but a whole array of items that I had spent much time learning how to do with ipfw on FreeBSD. After working until 10:30 pm on a Wednesday, I decided not to try OpenBSD, but instead opted for the pf port for FreeBSD. At about 1 am the next morning, I found out that not only would pf not load on a pre-5.0 machine (I know, I know - it says so in the docs, but I'm not enough of a kernel hacker to understand that it really wouldn't work), but I also couldn't get the Wanpipe drivers to load on the FreeBSD 5.2.1 system I just installed. By this time, I had been in contact with the Wanpipe BSD developer, Alex Feldman, and found that he hadn't thoroughly tested the driver install packages on 5.1 + and he asked me to wait for him to do his work. (He was extremely helpful - if you continue reading, you will find that I found more than one helpful soul through this whole process.) Instead of waiting, though - I had already spent enough wasted time on this project (which might have been completely avoided by using a Cisco Router with dual CSU/DSU's)* - I decided to dive into OpenBSD for the first time. I thought, with all of the structural similarities, it shouldn't be that difficult to acclimate myself to OpenBSD's peculiarities. So, here are basic instructions as to how I was able to get load balancing to work using the Sangoma card and pf on OpenBSD.
Instructions:
It happens that the University of Wisconsin - Madison runs an OpenBSD mirror. I downloaded the 3.4 boot iso from http://mirror.cs.wisc.edu/pub/mirrors/OpenBSD/3.4/i386/ on my OS X workstation:
wget http://mirror.cs.wisc.edu/pub/mirrors/OpenBSD/3.4/i386/cd34.iso
Using DiskTools, I burned the bootable iso image to CD. There are a slew of tools out there to burn ISO's to CD. I assume that if you are reading how to configure a custom workstation/server to be your router, that you know how to burn a CD. I put the CD in my future router and booted the machine.
I was careful to follow the install instructions at:
Some people have complained that FreeBSD's install procedure was difficult... well, they obviously have not tried to install OpenBSD. This is not to say that I found it all too difficult - I'm neither afraid of a command line, nor am I worried if I hose the system - I can rebuild it, right? I learn best through trial-and-error anyway. This was one time, however, that I was able to get the base system installed without a hitch. Well, that is not entirely true - I tried to do a base install on a system with a Broadcom Gigabit NIC and could never get the NIC to activate during the install. Perhaps had I read the supported hardware information on the main site, I would have found that support for the NIC needs to be added via a custom kernel. I had another machine waiting idly by to get used - with an onboard Intel 10/100 NIC (supported by the generic kernel) - and went through the same procedure again.
Because I had not yet received the CD's I purchased (c'mon - support the open source projects you use!), I installed from the UW-Madison http server.
After choosing the install source, I selected the following filesets (in FreeBSD lingo, I suppose this is base packages):
The following sets are available. Enter a filename, 'all' to select all the sets, or 'done'. You may de-select a set by prepending a '-' to its name.
[X] bsd
[ ] bsd.rd
[X] base34.tgz
[X] etc34.tgz
[ ] misc34.tgz
[X] comp34.tgz
[X] man34.tgz
[ ] game34.tgz
[ ] xbase34.tgz
[ ] xshare34.tgz
[ ] xfont34.tgz
[ ] xserv34.tgz
There are a few more items that need to be configured after the appropriate distributions have been downloaded. Again, follow the install FAQ for assistance. I would take more time to outline how to install your OpenBSD system, but the FAQ provides an amazingly detailed explanation of the installation procedure. It would be wrong of me to provide another explanation when it is laid out so well in the OpenBSD documentation.
Since I had already physically installed the Sangoma card, I rebooted the system and started to install the drivers and configure the T1's (according to the settings that Global Crossing is using). I downloaded the Wanpipe drivers and software:
# wget ftp://ftp.sangoma.com/OpenBSD/current_wanpipe/wanpipe-obsd-1.5.2-b2.tgz
I took care to follow the installation instructions given to us by Sangoma and simply ran:
# pkg_add ./wanpipe-obsd-1.5.2-b2.tgz
I had the install script do all the defaults - install drivers, add support to kernel, install startup scripts, etc. Everything went off without a hitch. When I had originally tried the installation on the pre 5.x FreeBSD system, the compilation of the drivers failed. I have yet to test the Wanpipe installation on OpenBSD 3.5, so I can't say whether it works or not. Due to the stability of OpenBSD, I imagine the packages will install without any problems as they did on my 3.4 system.
Again, following instructions that come with the Sangoma card, I configured the T1's.
# /usr/sbin/wancfg
This part requires that you have pertinent protocol and IP information from your provider.
Sidenote: I must say that Global Crossing is amazing. Their Circuit Turn-up staff was amazingly helpful. Through this whole ordeal, a gentleman named John Brox helped me out and was patient with my insistence on doing a custom install - I say patient because I initially was not able to even bring up the circuits with a box I had intended to use (a Eden 500 VIA mini-itx machine), as the ethernet interface caused the circuits to cycle up and down at weird intervals. I don't know if this was a problem with the motherboard itself or with the VIA ethernet interface. I have yet to do the same install on another VIA mini-itx based system, so I don't know. He was more than willing to help me make sure the circuit was up for good before turning it over to the monitoring group.
I haven't worked on routers in years and considering that, not only does Sangoma make the circuit configuration easy, but Global Crossing made the circuit turn-up a breeze.
On to the actual configuration of the firewall and load balancing. The Wanpipe drivers give you two interfaces to work with (once the T1's have been enabled). The last line in wanrouter.rc (in /etc/wanpipe) should read:
WAN_DEVICES="wanpipe1 wanpipe2"
so that both lines are enabled when the machine is rebooted. If you have not rebooted the machine, you can start the wanpipes by (assuming you configured wanpipe1 and wanpipe2):
# wanrouter start wanpipe1
# wanrouter start wanpipe2
After starting the circuits, running
# ifconfig -A
gives you:
wpachdlc0: flags=51
inet 22.109.101.234 --> 22.109.101.233 netmask 0xfffffffc
wpbchdlc0: flags=51
inet 22.222.9.162 --> 22.222.9.161 netmask 0xfffffffc
at the end.
You also need to make sure that your machine is set up to act as a gateway. Straight from the FAQ: http://www.openbsd.org/faq/faq6.html
The GENERIC kernel already has the ability to allow IP Forwarding, but needs to be turned on. You should do this using the sysctl(8) utility. To change this permanently you should edit the file /etc/sysctl.conf to allow for IP Forwarding. To do so add this line in that configuration file.
net.inet.ip.forwarding=1
My internal ethernet interface (fxp0) was set as 24.192.154.1 - the beginning of my address block available for use. At first - just to try the load balancing - I modified the rules at http://openbsd.org/faq/pf/pools.html#outgoing for my interface:
lan_net = "24.192.154.1/24"
int_if = "fxp0"
ext_if1 = "wpachdlc0"
ext_if2 = "wpbchdlc0"
ext_gw1 = "22.109.101.233"
ext_gw2 = "22.222.9.161"
so that the whole pf.conf looked like:
# define the interfaces
lan_net = "24.192.154.1/24"
int_if = "fxp0"
ext_if1 = "wpachdlc0"
ext_if2 = "wpbchdlc0"
ext_gw1 = "22.109.101.233"
ext_gw2 = "22.222.9.161"
# nat outgoing connections on each internet interface
nat on $ext_if1 from $lan_net to any -> ($ext_if1)
nat on $ext_if2 from $lan_net to any -> ($ext_if2)
# default deny
# I disabled these rules because we are in the testing phase - i.e. I am not yet using this as a real firewall - do not forget to re-enable these rules!
#block in from any to any
#block out from any to any
# pass all outgoing packets on internal interface
pass out on $int_if from any to $lan_net
# pass in quick any packets destined for the gateway itself
pass in quick on $int_if from $lan_net to $int_if
# load balance outgoing tcp traffic from internal network.
pass in on $int_if route-to \
{ ($ext_if1 $ext_gw1), ($ext_if2 $ext_gw2) } round-robin \
proto tcp from $lan_net to any flags S/SA modulate state
# load balance outgoing udp and icmp traffic from internal network
pass in on $int_if route-to \
{ ($ext_if1 $ext_gw1), ($ext_if2 $ext_gw2) } round-robin \
proto { udp, icmp } from $lan_net to any keep state
# general "pass out" rules for external interfaces
pass out on $ext_if1 proto tcp from any to any flags S/SA modulate state
pass out on $ext_if1 proto { udp, icmp } from any to any keep state
pass out on $ext_if2 proto tcp from any to any flags S/SA modulate state
pass out on $ext_if2 proto { udp, icmp } from any to any keep state
# route packets from any IPs on $ext_if1 to $ext_gw1 and the same for
# $ext_if2 and $ext_gw2
pass out on $ext_if1 route-to ($ext_if2 $ext_gw2) from $ext_if2 to any
pass out on $ext_if2 route-to ($ext_if1 $ext_gw1) from $ext_if1 to any
At this point, I was able to ping out of both interfaces, ping beyond the gateway, ping internally, etc. So, I set up a client behind the firewall and ran speed tests. Lo and behold, I was getting load balancing on the inbound (vis a vis Global Crossing's router configuration - it equally passes packets down both pipes), but it did not look like it was working on the outbound. It was about 12 am by this time and I was simply happy to see the OpenBSD machine working with the T1's and everything being pingable. Plus, John was gone by this time. I decided to call it quits and start again in the morning.
I woke up still puzzled as to why I couldn't get load balancing on the outbound route to work - theoretically, I had everything configured correctly! I called John and asked him if he could take a look at packet counts while I was running the bandwidth tests. After running a few tests, we found that the outbound were indeed being passed across both interfaces, but for some reason the bandwidth tests were not showing that. So, I decided to try to run more than one bandwidth test at once to see if I could fill up the pipes with more data. After getting my timing right (I kept starting one test well before the other), we were able to show that both pipes were indeed being load balanced, but for whatever reason, the speed tests would "sense" a limit at the 1 T1 barrier. Running multiple speed tests showed that just as much bandwidth was available down as it was up - i.e. just under 3 mbit/s. John was able to confirm that data was being sent down both pipes - one being favored by about 2% over the other. We chalked this up to "imperfect" round-robin'ing and called it a day. To this day, I still have not been able to figure out why the speed tests show outbound limited by one T1, but I don't really care as long as my available outbound is really the two T1's. I'm sure a ttcp test would be a better measure of throughput, but I haven't had the time to run such tests and am content that things are working as advertised.
After closing the firewall by enabling the default deny rules, I started working on allowing inbound connections. I struggled quite a bit with ssh. So much so that I finally emailed the OpenBSD misc list. I never did get a response to my question - I never really expected to seeing as I doubted many people were trying to do what I was and I further figured that they would pass my query off as being that of someone who failed to read the manual. They were wrong... I read the manual over and over but was never able to wrap my head around setting up the rules such that I could pass ssh through after default denying everything (I did confirm that ssh worked without the firewall...) So, I set out to test this through trial-and-error. Finally, after 5 hrs of testing, I came up with:
pass log quick on { $int_if, $ext_if1, $ext_if2 } inet proto tcp from any to \
any port 22 flags S/SA keep state
This was the culmination of so many combinations of pass etc. I feign to write them all down out of sheer embarrassment. How simple and elegant it ended up being...
There are a number of other ports I ended up opening. There are also a few clients behind the T1 who want their IP's completely open. I was able to accomplish all of this with the following ruleset:
# define the interfaces
lan_net = "24.192.154.1/24"
int_if = "fxp0"
ext_if1 = "wpachdlc0"
ext_if2 = "wpbchdlc0"
ext_gw1 = "22.109.101.233"
ext_gw2 = "22.222.9.161"
# a few macros
icmp_types = "echoreq"
priv_nets = "{ 127.0.0.0/8, 192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8 }"
## define all of the open hosts
open_hosts = "{ 24.192.154.15, 24.192.154.9 }"
## define some servers
dns_servers = "{ 24.192.154.3, 24.192.154.6 }"
email_servers = "{ 24.192.154.21 }"
http_servers = "{ 24.192.154.11 }"
# scrub all
scrub in all
# default deny
block log all
# pass all on internal loop
pass quick on lo0 all
# block spoofing from external interfaces
block drop in log quick on { $ext_if1, $ext_if2 } from $priv_nets to any
block drop out log quick on { $ext_if1, $ext_if2 } from any to $priv_nets
# allow ssh
pass log quick on { $int_if, $ext_if1, $ext_if2 } inet proto tcp from any to \
any port 22 flags S/SA keep state
# allow dns
pass quick on { $int_if, $ext_if1, $ext_if2 } inet proto udp from any to \
$dns_servers port 53
# allow mail
pass quick on { $int_if, $ext_if1, $ext_if2 } inet proto tcp from any to \
$email_servers port 25 flags S/SA keep state
# allow traffic to httpd servers
pass quick on { $int_if, $ext_if1, $ext_if2 } inet proto tcp from any to \
$http_servers port 80 flags S/SA keep state
# define the non-firewalled hosts
pass quick on { $int_if, $ext_if1, $ext_if2 } inet proto tcp from any to \
$open_hosts flags S/SA keep state
pass quick on { $int_if, $ext_if1, $ext_if2 } inet proto udp from any to \
$open_hosts
# allow icmp
pass inet proto icmp all icmp-type $icmp_types keep state
# pass all outgoing packets on internal interface
pass out on $int_if from any to $lan_net
# pass in quick any packets destined for the gateway itself
pass in quick on $int_if from $lan_net to $int_if
# load balance tcp
pass in on $int_if route-to \
{ ($ext_if1 $ext_gw1), ($ext_if2 $ext_gw2) } round-robin \
proto tcp from $lan_net to any flags S/SA modulate state
# load balance udp and icmp
pass in on $int_if route-to \
{ ($ext_if1 $ext_gw1), ($ext_if2 $ext_gw2) } round-robin \
proto { udp, icmp } from $lan_net to any keep state
# general "pass out" rules for external interfaces
pass out on $ext_if1 proto tcp from any to any flags S/SA modulate state
pass out on $ext_if1 proto { udp, icmp } from any to any keep state
pass out on $ext_if2 proto tcp from any to any flags S/SA modulate state
pass out on $ext_if2 proto { udp, icmp } from any to any keep state
# route packets on external interfaces through the appropriate
# gateway
pass out on $ext_if1 route-to ($ext_if2 $ext_gw2) from $ext_if2 \
to any
pass out on $ext_if2 route-to ($ext_if1 $ext_gw1) from $ext_if1 \
to any
I realize that these rules can be redundant and must be rewritten in a much more efficient manner. That is what I am in the middle of working on. I hope, though, that these will help guide you to set the firewall and routing rules for your needs.
Remaining Problems:
One other item that needs to be cleaned up is having the rules loaded after the T1 interfaces are activated at boot time. This process usually is finished 45-60 seconds after the machine is fully booted. This means that if there is a power failure or problem with the machine and it reboots, one has to physically re-set the default gateway and load the rules. I don't understand enough of OpenBSD to make this happen, but when I do, I'll tell you how it is done.
HTH,
Steve
* In between all of this, I had given up on using pf for load balancing because of what I had seen as problems with pf - I was mistaken at the time, but I was convinced the problem was with pf and not with what I was seeing. I unwrapped a brand new Cisco 1721 and two T1 interfaces from a box - my just in case box - and spent eight hours trying to get it working. Fortunately, I was never able to bring up the circuits with the Cisco. I say fortunately because I ended up back with the BSD box and got it working - what I wanted in the beginning.
Comments (7)
thanks a lot for this post, I intend to pool a cable line and an adsl line and I was looking for a complete example, which you provide. I'll test this tomorrow.
If you have any additional comments please let me know
Jean-Francois
Posted by Jean-Francois Boisvieux | May 15, 2004 4:11 PM
Posted on May 15, 2004 16:11
That was freakin' excellent. Now I have an excuse to try openBSD.
I was going to do this with FreeBSD. I figured, 'to problem, just google up some instructions - lot's of people have done it' but that turned out to be a bad assumption.
I going to try to combine 6 (maybe up to 10) satellite connections to serve a remote camp. I'll see if I can do it with DHCP assigned addresses on my cable and DSL connection first.
I'll try to let you know here.
Shannon
Posted by Shannon Wheeler | November 6, 2004 10:25 AM
Posted on November 6, 2004 10:25
I merged your and my firewall script and run it on a Soekris NET4501 and it works fine except for the VPN. I have a T1 and a dhcp enabled Road Runner cable. I have to delete the default route of Road Runner and add the gateway of the T1 to get the VPN to negotiate. But the firewall rules prevent data going through the VPN. Any suggestions?
### Some vars
LoIf="lo0"
IntIf="sis1"
ExtIf1="sis0"
ExtIf2="sis2"
ExtIp1="{ 69.38.82.161, 69.38.82.162, 69.38.82.163, 69.38.82.164, 69.38.82.189 }"
ExtIp2="24.26.13.97"
ExtGw1="69.38.82.190"
ExtGw2="24.26.8.1"
LocalNet="192.168.1.0"
VPN="{ 64.132.158.130 }"
### Declare ports
OutTCP="{ 23,25,43,80,110,119,143,443,554,871,993,1412,1723 }"
OutUDP="{ > 1024 }"
SshPorts="{ 22,2022 }"
ImPorts="{ 1863,5190,5222 }"
InUDP="{ 68 }"
InICMP="{ 3,11 }"
OutTracerouteUDP="{ 33434 >
### Set options
#set loginterface sis0
set limit { states 10000, frags 10000 }
scrub on $ExtIf1 all fragment reassemble random-id
scrub on $ExtIf2 all fragment reassemble random-id
# nat outgoing connections on each internet interface
nat on $ExtIf1 from $LocalNet/24 to any -> ($ExtIf1)
nat on $ExtIf2 from $LocalNet/24 to any -> ($ExtIf2)
# redirect local FTP traffic
rdr on $IntIf proto tcp from any to any port 21 -> 127.0.0.1 port 8081
# forward DNS and SMTP
rdr on $ExtIf1 proto { tcp, udp } from any to 69.38.82.163 port 53 -> 192.168.1.3
#rdr on $ExtIf proto tcp from any to 69.38.82.162 port 25 -> 192.168.1.3
# forward HTTP
rdr on $ExtIf1 proto tcp from any to 69.38.82.161 port 80 -> 192.168.1.3
# default deny
block all
# send connection refused on blocked ports
block return-rst in on $ExtIf1 inet proto tcp from any to $ExtIp1
block return-icmp in on $ExtIf1 inet proto udp from any to $ExtIp1
block return-rst in on $ExtIf2 inet proto tcp from any to $ExtIp2
block return-icmp in on $ExtIf2 inet proto udp from any to $ExtIp2
# pass all on internal loop
pass quick on lo0 all
# spoofing protection on IntIf
antispoof quick for $IntIf inet
# VPN
pass in quick on enc0 from any to any flags S/SAFR keep state
pass out quick on enc0 from any to any flags S/SAFR keep state
pass in quick on $ExtIf1 inet proto esp from $VPN to 69.38.82.189 keep state
pass out quick on $ExtIf1 inet proto esp from 69.38.82.189 to $VPN keep state
pass in quick on $ExtIf1 inet proto udp from $VPN to 69.38.82.189 port 500 keep state
# allow all incoming traffic on IntIf
pass in quick on $IntIf from any to $LocalNet/24
# allow all outgoing traffic on IntIf
pass out quick on $IntIf from any to $LocalNet/24
# allow tcp traffic out on ExtIf1 and ExtIf2
pass out quick on $ExtIf1 inet proto tcp from $ExtIf1 to any port $OutTCP \
flags S/SA modulate state
pass out quick on $ExtIf2 inet proto tcp from $ExtIf2 to any port $OutTCP \
flags S/SA modulate state
# traceroute to outside world
pass out quick on { $ExtIf1, $ExtIf2 } inet proto udp from any to any port \
$OutTracerouteUDP keep state
# ping to outside world
pass out quick on { $ExtIf1, $ExtIf2 } inet proto icmp all icmp-type 8 \
code 0 keep state
# others require opening high udp ports
pass out quick on { $ExtIf1, $ExtIf2 } inet proto udp from any to any keep state
# ping from the outside
pass in quick on { $ExtIf1, $ExtIf2 } inet proto icmp all \
icmp-type 8 code 0 keep state
# allow external access to SSH on both interfaces
pass in quick on { $ExtIf1, $ExtIf2 } inet proto tcp from any to \
any port 22 flags S/SA modulate state
# allow DNS in
pass in quick on $ExtIf1 proto tcp from any to any port 53 modulate state
pass in quick on $ExtIf1 proto udp from any to any port 53 keep state
# allow HTTP in
pass in quick on $ExtIf1 proto tcp from any to any port 80 modulate state
# traceroute to internal host
pass in quick on { $ExtIf1, $ExtIf2 } inet proto icmp from any to any \
icmp-type $InICMP keep state
# active FTP
pass in quick on $ExtIf1 inet proto tcp from any to $ExtIf1 port > 49151 \
flags S/SA modulate state
pass in quick on $ExtIf2 inet proto tcp from any to $ExtIf2 port > 49151 \
flags S/SA modulate state
# load balance tcp
pass in on $IntIf route-to { ($ExtIf1 $ExtGw1), ($ExtIf2 $ExtGw2) } round-robin \
proto tcp from $LocalNet/24 to any flags S/SA modulate state
# load balance udp and icmp
pass in on $IntIf route-to { ($ExtIf1 $ExtGw1), ($ExtIf2 $ExtGw2) } round-robin \
proto { udp, icmp } from $LocalNet/24 to any keep state
# route packets trough the appropiate gateways
pass out on $ExtIf1 route-to ($ExtIf2 $ExtGw2) from $ExtIf2 to any
pass out on $ExtIf2 route-to ($ExtIf1 $ExtGw1) from $ExtIf1 to any
Posted by Martijn Stam | December 7, 2004 2:25 PM
Posted on December 7, 2004 14:25
On the Cisco router, you can load balance by just using two default static routes. Of course, you need to get the T-1s up first. Let me know if you need help...
Posted by Paul Howell | January 23, 2005 9:33 AM
Posted on January 23, 2005 09:33
The apparent speed limit on outbound traffic is probably a side-effect of NAT: when a connection is created, NAT turns it into a connection using one of the firewall's two external IPs, each of which has a single T1. This way, load balancing can work without any help from the ISP, or even using two different ISPs for the two connections: each line is just handling some of your TCP connections, independently of the other. In your particular case, because they both go to the same system at the far end, you could probably get around this by telling OpenBSD to send outbound packets over connection 1 or 2 regardless of addressing (i.e. let packets for a connection with 1's IP address go over link 2) - alternatively, you could try getting true channel bonding (a single logical link, with a single IP address at each end).
At some point I might end up setting up a load-balancing configuration like this, but with a pair of ADSL lines, probably from different ISPs for a little more reliability (no extra cost, and ADSL will have rather lower priority than a T1 to repair faults).
Posted by James | June 5, 2005 10:37 AM
Posted on June 5, 2005 10:37
I like this solution, but am busy implementing a similar solution using FreeBSD 5.4 and ipfw - using the dummynet probability function to split the outgoing traffic between my different incoming connections.
Would love to implement a routing protocol like bird to do this - which would help if i have different ISPs for the different incoming lines, and help with fail-over.
Mine is more fun though with a transparent web proxy, and local bandwidth management and accounting built into the same box. Time to go play :)
Posted by Graham | August 29, 2005 10:45 AM
Posted on August 29, 2005 10:45
Hi Steven,
If I were to configure an OpenBSD box to combine (load balance) 2 DSL connections using two individual NICs, can I achieve it with your configuration? How would the BSD box figure out which of the two connections to send packet to? and How do I configure the extra default route?
Kind regards,
Yance
Posted by Yance | December 11, 2005 9:28 PM
Posted on December 11, 2005 21:28