Geo blocking with tcpwrappers

I recently had an issue with frequent login attempts against one 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 a 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 libwrap 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 format. I am working with newer versions with more options as documented on 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.


I use two rules to handle the geo-blocking. The first rule allows access from IP addresses in the cache file. This works with both the aclexec and spawn approaches. 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 other 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


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.



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_COUNTRIES lists the allowed countries. All other 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. If 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. When run from a terminal, the message is logged to the terminal.


# Space separated list of allowd country codes

# File containing allowed host IP addresses - must be writable by all callers

# Log file - must be writable by all callers that should log

# Syslog priorities

# Valid path

# 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)
        LOC=$(geoiplookup ${1} | head -1)
        echo usage $1 ip_addr
        exit -1


# 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}"
        MSG="${1} would allow ${CCODE} - ${COUNTRY}"
    MSG="${1} denied ${CCODE} - ${COUNTRY}"

# 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}



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.)

Leave a comment

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Cookie Consent with Real Cookie Banner