[Tip/Example] PF ruleset for IPv4 & IPv6 on a (publicly available) VPS

Aaron LI aly at aaronly.me
Thu Aug 31 05:52:10 PDT 2017


Hello DFlyers,

Several months ago, I purchased a small VPS (512MB RAM + 50GB SDD) from
LiteServer.nl, on which I'm running DragonFly BSD (v4.8.0) to serve as
my personal mail, website, caldav/carddav, and git server.

Recently, I set up IPv6 (static) for DFly on this VPS, but the PF causes
significant IPv6 packet loss (e.g., up to ~70% for both incoming &
outgoing ping), while IPv4 remains fine.

After many tedious trials, it is the "urpf-failed" block rule causes the
IPv6 traffic issue. By disabling this block rule on IPv6, the IPv6 works
the same well as IPv4 on my VPS box, no (significant) packet loss
anymore :-)

Most PF examples/tutorials suggest such a (combined) ruleset:
---------------------------------------------------------------------
block in quick from { $broken urpf-failed no-route } to any
---------------------------------------------------------------------

However, I have to disable the "urpf-failed" for IPv6 to make it works
well, i.e.,
---------------------------------------------------------------------
block in log quick from no-route to any
block in log quick inet from urpf-failed to any
---------------------------------------------------------------------

Did anyone come across such similar problem?  Does anyone know the
specific reason for this IPv6 issue w.r.t. PF's uRPF (Unicast Reverse
Path Forwarding)?? (I did some search but without answer yet.)


In addition, the following is my full PF ruleset (/etc/pf.conf) for my
DFly VPS box.  I have added many comments and references/docs.  Hope it
may help others.

Any suggestions (especially NAT rules on VPN, e.g., OpenVPN/OCserv) are
welcome.  Thank you :-)

---------------------------------------------------------------------
#
# /etc/pf.conf
# ------------
# PF rules for DragonFly BSD @ VPS
#
#
# Introduction
# ------------
# PF selectively passes or blocks data packets on a network interface
# based on the Layer 3 (IPv4 and IPv6) and Layer 4 (TCP, UDP, ICMP, and
# ICMPv6) headers.  The most often used criteria are source and
# destination address, source and destination port, and protocol.
# A series of rules specify matching criteria and the action block or
# pass.  PF is a *last-matching-rule-wins* firewall.
# An implicit `pass all` at the beginning of the ruleset means that if a
# packet does not match any filter rule the packet passes.  A best practice
# is to add an explicit `block all` as the first rule of a ruleset.
#
#
# References
# ----------
# [1] OpenBSD PF - User's Guide
#     https://www.openbsd.org/faq/pf/index.html
# [2] Firewalling with OpenBSD's PF packet filter
#     https://home.nuug.no/~peter/pf/en/index.html
#     https://home.nuug.no/~peter/pf/newest/
# [3] OpenBSD PF (brief introduction)
#     https://paulgorman.org/technical/openbsd-pf.txt
# [4] FreeBSD Handbook - 29.3 PF
#     https://www.freebsd.org/doc/en_US.ISO8859-1/books/handbook/firewalls-pf.html
# [5] PF - A baseline configuration for a web server with IPv6 and TLS/SSL
#     https://forums.freebsd.org/threads/56470/
# [6] How to secure FreeBSD with PF firewall
#     https://www.vultr.com/docs/how-to-secure-freebsd-with-pf-firewall
# [7] OpenBSD packet filter (PF): Real life example
#     http://daemon-notes.com/articles/network/pf
# [8] A simple VPN tunnel with FreeBSD
#     https://cyprio.net/wtf/2014-03-21-a-simple-vpn-tunnel-with-freebsd.html
#
#
# Configurations
# --------------
# * Load the kernel module: add following line to "/etc/rc.conf":
#       pf_load="YES"
# * Enable PF service, see pfctl(8) for additional options:
#       pf_enable="YES"
#       pf_flags=""    # additional flags for "pfctl" startup
# * Specify the ruleset configuration file for PF:
#       pf_rules="/etc/pf.conf"   # default
# * Enable logging support provided by pflog(4):
#       pflog_enable="YES"
# * Further configure the pflog:
#       pflog_flags=""   # additional flags for "pflogd" startup
#       pflog_logfile="/var/log/pflog"   # default
#
#
# Usage Examples
# --------------
# * pfctl -vnf /etc/pf.conf
#   Check "/etc/pf.conf" for errors, but do not load ruleset.
# * pfctl -F all -f /etc/pf.conf
#   Flush all NAT, filter, state, and table rules, and reload ruleset.
# * pfctl -e
#   Enable PF.
# * pfctl -d
#   Disable PF.
# * pfctl -s [ rules | nat | states ]
#   Report on the filter rules, NAT rules, or state table.
# * pfctl -k host
#   Kill all state entries originating from "host".
# * pfctl -s states -vv
#   Show state ID's, ages, and rule numbers.
# * pfctl -s rules -vv
#   Show rules with stats and rule numbers.
# * pfctl -s Tables
#   List tables.
# * pfctl -s info
#   Show filter stats and counters.
# * pfctl -s all
#   Show everything.
# * pfctl -t foo -T show
#   Show the contents of table "foo".
# * pfctl -t foo -T add xx.xx.xx.xx
#   Add address "xx.xx.xx.xx" to table "foo".
# * pfctl -t foo -T delete xx.xx.xx.xx
#   Delete address "xx.xx.xx.xx" from table "foo".
#
# * tcpdump -n -e -ttt -i pflog0
#   Get PF logging messages from the "pflog0" interface.
# * tcpdump -n -e -ttt -r /var/log/pflog
#   Read PF logging from the "pflog" file.
#
#
# Aaron LI
# 2017-08-31
#


##
## NOTE:
##
#
# * Avoid negated lists (e.g. "{ 10.0.0.0/8, !10.1.2.3 }"), because each
#   list item expands to add another rule, which causes undesirable results.
#
# * persist : (table)
#   Force the kernel to keep the table even when no rules refer to it.
#   If this flag is not set, the kernel will automatically remove the
#   table when the last rule referring to it is flushed.
# * quick : (rule)
#   If the rule is matched, no further rules will be evaluated!
# * self : (rule)
#   Expands to all addresses assigned to all interfaces.
#
# * egress : (interface group)
#   The kernel automatically creates an `egress` group for the interface(s)
#   that hold the default route(s).
# * Interface (group) names, and `self` can have *modifiers* appended:
#   + `:0` : Do not include interface aliases.
#   + `:broadcast` : Translates to the interface's broadcast address(es).
#   + `:network` : Translates to the network(s) attached to the interface.
#   + `:peer` : Translates to the point-to-point interface's peer address(es).
# * Host names may also have the `:0` modifier appended to restrict the
#   name resolution to the first of each v4 and v6 address found.
# * Host name resolution and interface to address translation are done at
#   ruleset *load-time*.  By surrounding the interface name (and optional
#   modifiers) in *parentheses* makes PF update the rules whenever the
#   interface changes its address, avoiding manual reloading, which is
#   especially useful with NAT. 
#


##
## Macros & Lists
##

loopback = "lo"
# External interface
ext_if = "vtnet0"
# Interface used by VPN (OpenVPN/OCserv)
vpn_if = "tun"
# Network used by VPN on $vpn_if
vpn_net = "10.8.0.0/24"

# Services (incoming & outgoing)
#   * ssh : SSH connections (default port: 22)
#   * 8864 : custom SSH port
#   * domain: DNS resolution
#   * ntp: NTP daemon
#   * smtp : mail server (receiving mail from other mail servers)
#            (NOTE: also allow outgoing for remote mail delivery!)
#   * submission : mail server (for receiving mail from MUA/user)
#   * imaps : IMAP server
#   * http & https : web service
#   * git : Git service (e.g., clone)
#   * 8484 & 8989 : ShadowSocks services
#   * 8080 : OpenVPN / OpenConnect VPN service (tcp+udp)
#
# For restrictive incoming rules
in_tcp_services_restricted = "{ 8864 }"
# For non-restrictive incoming rules
in_tcp_services = "{ smtp, submission, imaps, http, https, 8484, 8989, 8080 }"
# For incoming UDP rules
in_udp_services = "{ 8080 }"
# For outgoing rules
# NOTE: allow outgoing SMTP connections for remote mail delivery!
out_tcp_services = "{ domain, smtp, http, https, git, ssh }"
out_udp_services = "{ domain, ntp }"

# ICMP message types:
#   * echoreq : Echo service request; used by "ping(8)" and "traceroute(8)"
#               NOTE: also open the UDP ports 33433-33626 for "traceroute(8)"
#   * unreach : Destination unreachable; allow to probe for MTU discovery
icmp_types = "{ echoreq, unreach }"
#
# IPv6 ICMP: see also icmp6(4)
#   * timex : Time exceeded
#   * paramprob : Invalid IPv6 header
#   * routeradv & routersol :
#     For getting address using IPv6 autoconfiguration from router.
#   * neighbradv & neighbrsol :
#     For getting neighbor addresses.
icmp6_types = "{ echoreq, unreach, timex, paramprob, \
                 routeradv, routersol, neighbradv, neighbrsol }"


##
## Tables
##

# Bruteforce protection (e.g., SSH)
table <bruteforce> persist

# Fail2ban detected bad hosts
table <fail2ban> persist

# Martians: non-routables addresses as defined by stantards
# https://www.iana.org/assignments/iana-ipv4-special-registry/
# https://www.iana.org/assignments/iana-ipv6-special-registry/
# http://en.wikipedia.org/wiki/Reserved_IP_addresses
table <martians> const { \
      0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8, \
      169.254.0.0/16, 172.16.0.0/12, 192.0.0.0/24, 192.0.2.0/24, \
      192.168.0.0/16, 198.18.0.0/15, 198.51.100.0/24, \
      203.0.113.0/24, 240.0.0.0/4, 255.255.255.255/32, \
      ::1/128, ::/128, ::/96, ::ffff:0:0/96, 100::/64, \
      2001::/32, 2001:2::/48, 2001:db8::/32, fc00::/7, fe80::/10 \
}


##
## Options
##

# Play nicely to return with status codes
set block-policy return

# Allow all traffic on loopback and internal interfaces
set skip on $loopback
set skip on $vpn_if

# Enable collection of packet and byte count statistics for the external
# interface, which can be viewed using `pfctl -s info`.
# NOTE: cannot use "egress" here (on DragonFly at least)
set loginterface $ext_if


##
## Rules
##

# Network packet normalization
# Enabling scrub provides a measure of protection against certain kinds of
# attacks based on incorrect handling of packet fragments
scrub in all

# NAT for VPN network (OpenVPN/ocserv)
#
# WARNING: not tested yet!!!
#
#nat on ! $vpn_if inet from $vpn_net to any -> ($ext_if)
#nat on $ext_if inet from $vpn_net to any -> ($ext_if)
#nat on egress inet from !(egress:network) to any -> (egress:0)

# The antispoof mechanism protects against activity from spoofed or
# forged IP addresses, mainly by blocking packets appearing on
# interfaces and in directions which are logically not possible.
# Use "antispoof" only on interfaces with an IP address.
antispoof log quick for $ext_if

# Block all traffic by default (disable logging for IPv4)
block inet all
block log inet6 all
#
# Block all incoming traffic while allow all outgoing traffic
#block in log all
#pass out all keep state

# Block non-routables addresses.
# NOTE: Using "return" action to prevent annoying timeouts for users.
block drop   in  quick on egress from <martians> to any
block return out quick on egress from any to <martians>

# Block anything coming form source we have no back routes for.
block in log quick from no-route to any

# Block packets whose ingress interface does not match the
# one the route back to their source address.
#
# WARNING:
# Only apply this rule to IPv4; otherwise it causes significant
# packet loss on IPv6 traffic (e.g., incoming/outgoing ping),
# reason???
block in log quick inet from urpf-failed to any

# Block bruteforce on all connections (both in and out)
block log quick from <bruteforce>

# Get rid quick of Internet noises (e.g., M$ NetBIOS service),
# and don't log this also!
block drop in quick on $ext_if proto { tcp, udp } from any to any \
      port { netbios-ns, netbios-dgm, netbios-ssn, microsoft-ds, nfsd }

# Use overload tables to protect restrictive services (e.g., SSH)
#
#   * max-src-conn :
#     number of simultaneous connections allowed from one host
#   * max-src-conn-rate :
#     rate of new connections allowed from any single host
#     per number of seconds (here: 4 connections every 30 seconds).
#   * overload <bruteforce> :
#     any host which exceeds these limits gets its address added to
#     the "bruteforce" table.
#   * flush global :
#     when a host reaches the limit, that all (global) of that host's
#     connections will be terminated (flush).
#
# NOTE:
# Over time, tables will be filled by overload rules and their size
# will grow incrementally, taking up more memory.  Sometimes an IP
# address that is blocked is a dynamically assigned one, which has
# since been assigned to a host who has a legitimate reason to communicate
# with hosts.  Therefore, the expired entries should get flushed,
# e.g., this command will remove "bruteforce" table entries which
# have not been referenced for 86400 seconds (i.e., 1 day):
#    pfctl -t bruteforce -T expire 86400
# It is convenient to add such clean commands to a cron table.
#
pass in on $ext_if proto tcp to ($ext_if) port $in_tcp_services_restricted \
     flags S/SA keep state \
     (max-src-conn 8, max-src-conn-rate 4/30, \
      overload <bruteforce> flush global)

# Pass traffic for allowed non-restricted services
pass in on $ext_if proto tcp to ($ext_if) port $in_tcp_services keep state
pass in on $ext_if proto udp to ($ext_if) port $in_udp_services keep state

# Allow outgoing connection while retaining state information on those
# connections.  This state information allows return traffic for those
# connections to pass back and should only be used on machines that can
# be trusted.
pass out proto tcp to any port $out_tcp_services keep state
pass out proto udp to any port $out_udp_services keep state

# ICMP messages
#
# Allow only specified ICMP types (both in and out)
pass on $ext_if inet  proto icmp      all icmp-type  $icmp_types
pass on $ext_if inet6 proto ipv6-icmp all icmp6-type $icmp6_types
#
# Allow out the default UDP ports used by "traceroute(8)"
pass out on $ext_if proto udp to ($ext_if) port 33433 >< 33626 keep state
---------------------------------------------------------------------


Cheers,
-- 
Aly
-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 833 bytes
Desc: This is a digitally signed message part
URL: <http://lists.dragonflybsd.org/pipermail/users/attachments/20170831/4d4a5405/attachment.bin>


More information about the Users mailing list