Geo blocking with tcpwrappers

i recently had an issue with frequent login attempts against on of my services. These were almost all from countries that should not be accessing my service. To resolve the issue I implemented geo blocking with TCP Wrappers. This is how I went about geo blocking connections.

System setup

My system uses TCP Wrappers with the aclexec option. The request is accepted if it is not geo blocked. It is possible to use the spawn option to do the check, but this requires as second attempt and a cache file of accepted connections. The geo_check script I use creates a file of accepted IP addresses. This allows it to work with the spawn option. The cache file must be writable by all services that are geo blocked. I created a /usr/lib/tcpwrap directory to contain the hosts_allow cache file. The services being geo blocked use TCP Wrappers. Many programs have TCP Wrappers enabled, and will work without extra effort. Change a configuration setting to enable TCP Wrappers, if necessary. xinetd should be linked to librwap and will therefore wrap the services it provides. If you are still using inetd, you will need to use tcpd to wrap programs that do not have TCP Wrappers enabled. You will need to add rules to /etc/hosts.allow and /etc/hosts.deny files. The hosts_access man page explains the file fomat. I am working with a newer versions with more options as documented in the host_options man page. (Instructions for working with the original format are provided below in the notes for using the spawn option.) The newer versions of TCP Wrappers allow using one file instead of two by adding DENY and ALLOW options.

/etc/hosts.allow

I use two rules to handle the geo blocking. The first rule allows access from IP addresses in the cache file. This works both the aclexec and spawn approach. Replace service with the required service (or list of services). If this rule accepts the connection, then no geo_check command is run and will not log those connections. There are usually othe log messages for these connections.

service : /var/lib/tcpwrap/hosts_allowed

The second calls the geo_check utility via the aclexec option. If your TCP Wrappers does not support aclexec, omit this rule and use the option specified for /etc/hosts.deny instead;

service : ALL : aclexec /usr/local/bin/geo_check %a %d

/etc/hosts.deny

If your TCP Wrappers does not support aclexec, you can use the spawn option instead. Initial attempts fail, but later attempts are by the file based rule above. If the spawn option is not available, just remove the spawn keyword from the line.

service: ALL : spawn (/usr/local/bin/geo_check %a %d)

Make sure you have a deny rule that blocks all IP addresses. I block all services that are not otherwise allowed. If there is no matching deny rule, TCP Wrappers will accept the connection.

ALL : ALL

/usr/local/bin/geo_check

This is a simple script that invokes geoiplookup (which must be available) and parses the output. There are two parameters: the IP address to check; and an optional service name for the log message. For the hosts_allowed file to work properly it must be writable by all geo blocked applications. The string variable ALLOWED_COUUNTRIES lists the allowed countries. All others countries are rejected. Change this string to meet your needs. The values are ISO two character country codes, with a few additions for IP addresses not assigned to countries. If you want the script to write its own log, set LOG_FILE to the path to a log file writable by all geo blocked applications. It you want to log to the syslog facility using the logger command, set the ACCEPT_PRIORITY and or DENY_PRIORITY to appropriate values. In the code below I’ve enabled both a log file and syslog. If run from a terminal, the message is logged to the terminal.

#!/bin/sh

# Space separated list of allowd country codes
ALLOWED_COUNTRIES="CA"

# File containing allowed host IP addresses - must be writable by all callers
ALLOWED_FILE=/var/lib/tcpwrap/hosts_allowed

# Log file - must be writable by all callers that should log
LOG_FILE=/var/log/geo_check.log

# Syslog priorities
ACCEPT_PRIORITY=auth.info
DENY_PRIORITY=auth.warning

# Valid path
PATH=/bin:/usr/bin

# Attempt to create the allowed files if missing; world writable
umask 111
[ -f ${ALLOWED_FILE:-''} ] || touch ${ALLOWED_FILE}
[ -f ${LOG_FILE:-''} ] || touch ${LOG_FILE}

# Lookup country and country code
case $1 in
    *:*)
        LOC=$(geoiplookup6 ${1} | head -1)
        ;;
    [0-9]*)
        LOC=$(geoiplookup ${1} | head -1)
        ;;
    *)
        echo usage $1 ip_addr
        exit -1
        ;;
esac

COUNTRY=${LOC##*, }
CCODE=${LOC%%,*}
CCODE=${CCODE##* }

# Set message and status - Add to approved IP address, if appropriate
if $( echo ${ALLOWED_COUNTRIES} | grep -q ${CCODE:-xx}); then
    if [ -w ${ALLOWED_FILE:-''} ]; then
        grep -q ${1} ${ALLOWED_FILE} || \
            echo ${1} >> ${ALLOWED_FILE}
        MSG="${1} allowed ${CCODE} - ${COUNTRY}"
    else
        MSG="${1} would allow ${CCODE} - ${COUNTRY}"
    fi
    LOG_PRIORITY=${ACCEPT_PRIORITY}
    STATUS=0
else
    MSG="${1} denied ${CCODE} - ${COUNTRY}"
    LOG_PRIORITY=${DENY_PRIORITY}
    STATUS=1
fi

# Log, report and exit with status
[ -w ${LOG_FILE:-''} ] && \
    echo $(date -Iseconds) - ${MSG} >> ${LOG_FILE}
[ -z ${LOG_PRIORITY:-''} ] || \
    logger -p ${LOG_PRIORITY} -t geo_check --id ${2}${2:+: }${MSG}
tty -s && echo ${MSG}
exit ${STATUS}

# EOF

/usr/lib/tcpwrap/hosts_allowed

This file is simply a list of IP addresses one per line. Over time it may grow larger than you like. There are a few options:

  • Truncate the file, possibly keeping lines from the end of the file. (logrotate can do this.)
  • Merge IP addresses using CIDR format. This is useful if you have users getting addresses by DHCP from their ISP or phone provider.
  • Remove addresses associated with invalid requests. (You could move them to a hosts_denied file and add EXCEPT /usr/lib/tcpwrap/hosts_allowed to the rules.)