Chapter 7. Internet Gateway

Table of Contents

7.1. Sysctl and packet forwarding
7.2. Firewall
7.2.1. pf.conf
7.2.2. ftp-proxy
7.3. DHCP daemon
7.3.1. dhcpd.conf
7.3.2. Starting the daemon
7.4. BIND
7.4.1. named.conf
7.4.2. Zone master files
7.4.3. Starting BIND

7.1. Sysctl and packet forwarding

Before you can set up your OpenBSD box as any kind of router, you will need to enable IPv4 packet forwarding in the kernel through the sysctl(3) mechanism. For added security, and because you generally will not need this feature anyway, you should also disable ICMP redirects. Issue these commands as root:

# sysctl -w net.inet.ip.forwarding=1
# sysctl -w net.inet.ip.redirect=0

Also, append the lines

net.inet.ip.forwarding=1
net.inet.ip.redirect=0

to the file /etc/sysctl.conf in order to make this configuration persistent across reboots.

7.2. Firewall

Next comes the lengthy task of configuring pf(4), the OpenBSD Packet Filter. The firewall is configured using the file /etc/pf.conf, which is divided into several sections pertaining to macro definitions, address translation, traffic normalization and queueing, packet filtering, and so forth.

7.2.1. pf.conf

Configure PF by editing /etc/pf.conf as follows:

#####################
# Macros and Tables #
#####################

### Interface devices ###
if_lo = "lo0"
if_wan = "pppoe0"
if_lan = "vr1"

### Subnets and address ranges ###
# IPv4 loopback
net_lo      = "127.0.0.0/8"
# IPv4 multicast
multicast   = "224.0.0.0/4"
# All the private addresses that fall under our purview
net_private = "10.19.0.0/16"
# LAN subnet (as opposed to other subnets under our control, such as VPN
# subnets)
net_lan     = "10.19.0.0/17"

### Port-based access control ###
# Allowed TCP ports from Internet (ssh treated separately, below)
ports_in_tcp = "{ auth 1194 }"
# Allowed UDP ports from Internet
ports_in_udp = "{ ntp 1194 1195 }"

### Address tables ###
# Address groups which shouldn't be in circulation on the Internet
table <martians> const { 127.0.0.0/8 192.168.0.0/16 172.16.0.0/12 \
    10.0.0.0/8 169.254.0.0/16 192.0.2.0/24 0.0.0.0/8 }

# Table of ddresses that have attempted to brute-force attack our SSH
# service from the Internet, for blocking purposes.
table <wan_bruteforce> persist


###########
# Options #
###########

# Strictly associate connection states with network interfaces
set state-policy if-bound

# Reject disallowed TCP and UDP connections with an RST or ICMP UNREACHABLE
# packet, respectively
set block-policy return

# Log packet- and byte-count statistics for our Internet connection
set loginterface $if_wan

# You gotta have loopback...
set skip on $if_lo


#########################
# Traffic Normalization #
#########################

# Replace TCP timestamps and IP identification fields with random values to
# compensate for less secure values that may be generated by other hosts.
# Also, lower the MSS for traversing TCP connections to 1440 because our
# PPPoE interface's MTU is lower than that of clients' LAN interfaces.
match on $if_wan scrub (reassemble tcp random-id max-mss 1440)


###############
# Translation #
###############

# NAT
match out on $if_wan from $net_private to !(if_wan) nat-to ($if_wan)


####################
# Packet Filtering #
####################

# Block everything unless we say otherwise
block all

# Prevent loopback address spoofing
antispoof quick for $if_lo

# Prevent spoofing where an antispoof rule wouldn't be a robust choice.
# However, this is incompatible with the bridged tap device configuration
# used for OpenVPN unless we set skip on one of the bridged interfaces.
block drop in quick from urpf-failed

# FTP proxy anchor
anchor "ftp-proxy/*"

### WAN interface ###

## Ingress
# Allow SSH access, but block any communications from hosts found trying to
# brute-force the SSH server...
block drop in quick on $if_wan inet from <wan_bruteforce>
pass in quick on $if_wan inet proto tcp from any to ($if_wan) \
    port 22 flags S/SFRA modulate state \
    (max-src-conn-rate 3/30, overload <wan_bruteforce> flush global)
# Necessary IPv4 ICMP types
pass in on $if_wan inet proto icmp from any to ($if_wan) \
    icmp-type { echoreq unreach redir timex paramprob } keep state
# Allow inbound connections on certain TCP and UDP ports
pass in on $if_wan inet proto tcp from any to ($if_wan) \
    port $ports_in_tcp modulate state flags S/SFRA
pass in on $if_wan inet proto udp from any to ($if_wan) \
    port $ports_in_udp keep state
block drop in on $if_wan inet from <martians> to any

## Egress
pass out on $if_wan inet from ($if_wan) to any modulate state \
    flags S/SA
block return out log on $if_wan inet from any to <martians>

### LAN interface ###

## Ingress
# Allow DHCP requests
pass in quick on $if_lan inet proto udp from { 0.0.0.0 $net_lan } \
    port 68 to { ($if_lan) 255.255.255.255 } port 67
# Just to make sure we don't accidentally lock ourselves out of SSH
pass in quick on $if_lan inet proto tcp from any to (self) port ssh \
    keep state flags S/SA
pass in on $if_lan
# Redirect outbound FTP connections through ftp-proxy
pass in on $if_lan inet proto tcp to port ftp rdr-to 127.0.0.1 port 8021

## Egress
pass out on $if_lan

Now start the firewall:

# pfctl -f /etc/pf.conf
# pfctl -e

Then, so that the packet filter will start automatically at boot time, add the following line to /etc/rc.conf.local.

pf=YES

7.2.2. ftp-proxy

Due to the goofy way in which the old FTP protocol establishes its data connections, special provisions must be made in firewalls like ours in order to support active-mode FTP connections from LAN clients to servers in the outside world. In OpenBSD, these provisions take the form of the ftp-proxy(8) daemon.

The proxy works by intercepting outbound FTP control connections, modifying key FTP commands as necessary, and adding special, temporary firewall rules to allow the inbound active-mode data connection. This is provided for by the anchor and FTP redirection rules we've already added to pf.conf, as shown above.

Now we need only run the proxy. Run the following command to start (and daemonize) it:

# ftp-proxy

We'll also want ftp-proxy(8) to run automatically when the system boots, so append the following to /etc/rc.conf.local:

ftpproxy_flags=""

7.3. DHCP daemon

OpenBSD 4.8 includes a modified version of the ISC DHCP server, which we will use to provide network configurations to IPv4 hosts on our local network.

7.3.1. dhcpd.conf

First, provide something analogous to the following configuration in the file /etc/dhcpd.conf, starting with the general network configuration:

authoritative;

shared-network LOCAL-NET {
    option domain-name "example.net";
    option domain-name-servers 10.19.0.1;
    option nntp-server 10.19.0.1;
    option time-servers 10.19.0.1;

    subnet 10.19.0.0 netmask 255.255.128.0 {
        option routers 10.19.0.1;
        option domain-name-servers 10.19.0.1;
        range 10.19.2.1 10.19.2.254;
    }
}

One of the goals of this project was to set up my own recursive domain name server to handle DNS requests on behalf of the LAN, so the DHCP option domain-name-servers has here been configured to return the gateway's own private IP address as the primary name server.

Append to dhcpd.conf any static DHCP allocations you would like to make, using the following template – substituting the actual desired hostnames, IP addresses, and MAC addresses:

group {
    host noatun {
        hardware ethernet XX:XX:XX:XX:XX:XX;
        fixed-address 10.19.1.1;
    }
    host loki {
        hardware ethernet XX:XX:XX:XX:XX:XX;
        fixed-address 10.19.1.2;
    }
}

Here we're using the 10.19.1/24 block for static DHCP allocations, and 10.19.2/24 for dynamic DHCP clients.

7.3.2. Starting the daemon

Add the following line to /etc/rc.conf.local so that the DHCP daemon will start at boot, listening on the LAN interface.

dhcpd_flags="vr1"

Then, start the daemon manually by running:

# /usr/sbin/dhcpd vr1

7.4. BIND

OpenBSD 4.8 also includes a version of ISC BIND 9, the Internet's de facto standard DNS server. Running a caching BIND server on your gateway can greatly reduce the latency experienced while browsing the Web, by minimizing DNS request round-trip time. This is especially noticeable if your ISP's DNS servers are underpowered and over-utilized.

Also, having your own server protects you from DNS abuse at the hands of your ISP, such as Cox Communications' "Enhanced Error Results Page," which breaks expected DNS behavior so that Cox can make a few extra bucks advertising to typo-prone customers.

7.4.1. named.conf

BIND's main configuration file is located at /var/named/etc/named.conf. Edit this file with a configuration like the following:

acl clients {
    ::1;
    127.0.0.1;
    10.19.0.0/16;
};

options {
    version "";
    listen-on { 127.0.0.1; 10.19.0.1; };
    allow-recursion { clients; };
};

logging {
    category lame-servers { null; };
};

// Standard zones
//
zone "." {
    type hint;
    file "etc/root.hint";
};

zone "localhost" {
    type master;
    file "standard/localhost";
    allow-transfer { localhost; };
};

zone "127.in-addr.arpa" {
    type master;
    file "standard/loopback";
    allow-transfer { localhost; };
};

zone "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa" {
    type master;
    file "standard/loopback6.arpa";
    allow-transfer { localhost; };
};


// Master zones
//
zone "example.net" {
    type master;
    file "master/db.net.example";
    allow-transfer { localhost; };
    allow-update { localhost; }; // Dynamic DNS for example.net.
};

zone "19.10.in-addr.arpa" {
    type master;
    file "master/db.10.19";
    allow-transfer { localhost; };
};

7.4.2. Zone master files

Next create the files in the /var/named/master directory which were referenced above. A detailed discussion of the zone file format is beyond the scope of this guide; refer to Cricket Liu & Paul Albitz's DNS and BIND if you really want to get into the details. However, the following examples should suffice as templates for most use cases, starting with db.net.example:

$ORIGIN example.net.
$TTL 3h ; 3 hours
@ IN SOA midgard.example.net. root.example.net. (
                        120     ; serial
                        3h      ; refresh (3 hours)
                        1h      ; retry (1 hour)
                        1w      ; expire (1 week)
                        1h      ; minimum (1 hour)
                        )

                        NS      midgard.example.net.

$TTL 5m ; 5 minutes

                        A       X.X.X.X ; Replace with your public IPv4 address

$TTL 3h ; 3 hours

ntp                     CNAME   example.net.

midgard                 A       10.19.0.1
ap                      A       10.19.0.2

noatun                  A       10.19.1.1
loki                    A       10.19.1.2

The $ORIGIN keyword allows you to specify a base name for all hostnames in this file, so you can say, e.g., noatun rather than noatun.example.net. when specifying that computer's A record.

Next up, reverse DNS for your network's address block, in the file db.10.19:

$ORIGIN 19.10.in-addr.arpa.
$TTL 3h

@ IN SOA midgard.example.net. root.example.net. (
        120     ; serial
        3h      ; refresh
        1h      ; retry
        1w      ; expire
        1h      ; negative caching TTL
        )

                NS      midgard.example.net.

1.0             PTR     midgard.example.net.
2.0             PTR     ap.example.net.

1.1             PTR     noatun.example.net.
2.1             PTR     loki.example.net.

The configuration file in Section 7.4.1, “named.conf” specified that dynamic updates should be allowed for the domain. In order for this to work, the name server itself must be allowed to write to the zone master files' directory. Give the name server's system account ownership of this directory:

# chown -R named /var/named/master

7.4.3. Starting BIND

Start the BIND domain name server by simply running the command named as root. Watch for any error messages which may indicate a syntax error in the zone files or in the BIND configuration file. If the daemon starts successfully, add the following line to /etc/rc.conf.local so that named(8) will run automatically on subsequent reboots:

named_flags=""