# Copyright (c) 2016 Philip Hane
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from .exceptions import (IPTablesError, IPTablesExists, IPTablesNotExists,
SubprocessError)
from .utils import popen_wrapper
import logging
import os
log = logging.getLogger(__name__)
uid = os.geteuid()
[docs]class IPTablesSinkhole:
"""
The class for managing sinkhole configuration within iptables.
Args:
interface: The secondary network interface dedicated to sinkhole
traffic.
Warning: Do not accidentally set this to your primary interface.
It will drop all traffic, and kill your remote access.
interface_addr: The IP address assigned to interface.
log_prefix: Prefix for syslog messages.
protocol: The protocol(s) to log (all traffic will still be dropped).
Accepts a comma separated string of protocols
(tcp,udp,udplite,icmp,esp,ah,sctp) or all.
dport: The destination port(s) to log (for applicable protocols).
Range should be in the format startport:endport or 0,1,2,3,n..
hashlimit: Set the hashlimit rate. Hashlimit is used to tune the
amount of events logged. See the iptables-extensions docs:
http://ipset.netfilter.org/iptables-extensions.man.html
hashlimitmode: Set the hashlimit mode, a comma separated string of
options (srcip,srcport,dstip,dstport). More options here results
in more logs generated.
hashlimitburst: Maximum initial number of packets to match.
hashlimitexpire: Number of milliseconds to keep entries in the hash
table.
srcexclude: Exclude a comma separated string of source IPs/CIDRs from
logging.
"""
def __init__(self, interface=None, interface_addr=None,
log_prefix='"[nfsinkhole] "',
protocol='all', dport='0:65535',
hashlimit='1/h', hashlimitmode='srcip,dstip,dstport',
hashlimitburst='1', hashlimitexpire='3600000',
srcexclude='127.0.0.1'
):
# TODO: add arg checks across all classes
self.interface = interface
self.interface_addr = interface_addr
self.log_prefix = log_prefix
self.protocol = protocol
self.dport = dport
self.hashlimit = hashlimit
self.hashlimitmode = hashlimitmode
self.hashlimitburst = hashlimitburst
self.hashlimitexpire = hashlimitexpire
self.srcexclude = srcexclude
[docs] def list_existing_rules(self, filter_io_drop=False):
"""
The function for retrieving current iptables rules related to
nfsinkhole.
Args:
filter_io_drop: Boolean for only showing the DROP rules for INPUT
and OUTPUT. These are not shown by default. This exists to
avoid allowing packets on the interface if the service is down.
If installed, the interface always drops all traffic regardless
of the service state.
Returns:
List: Matching sinkhole lines returned by iptables -S.
Raises:
IPTablesError: A Unix process had an error (stderr).
"""
# Get list summary of iptables rules
cmd = ['iptables', '-S']
# run sudo if not root
if uid != 0:
cmd = ['/usr/bin/sudo'] + cmd
existing = []
# Get all of the iptables rules
try:
out, err = popen_wrapper(cmd)
except OSError as e:
raise IPTablesError('Error encountered when running process "{0}":'
'\n{1}'.format(' '.join(cmd), e))
# If any errors, iterate them and write to log, then raise
# IPTablesError.
if err:
arr = err.splitlines()
raise IPTablesError('Error encountered when running process "{0}":'
'\n{1}'.format(' '.join(cmd), '\n'.join(arr)))
# Iterate the iptables rules, only grabbing nfsinkhole related rules.
arr = out.splitlines()
for line in arr:
if ('SINKHOLE' in line) or filter_io_drop and line.strip() in [
'-A INPUT -i {0} -j DROP'.format(self.interface),
'-A OUTPUT -o {0} -j DROP'.format(self.interface)
]:
existing.append(line.strip())
return existing
[docs] def create_rules(self):
"""
The function for writing iptables rules related to nfsinkhole.
"""
log.info('Checking for existing iptables rules.')
existing = self.list_existing_rules()
# Existing sinkhole related iptables lines found, can't create.
if len(existing) > 0:
raise IPTablesExists('Existing iptables rules found for '
'nfsinkhole:\n{0}'
''.format('\n'.join(existing)))
log.info('Writing iptables config')
# Create a new iptables chain for logging
tmp_arr = ['iptables', '-N', 'SINKHOLE']
# run sudo if not root
if uid != 0:
tmp_arr = ['/usr/bin/sudo'] + tmp_arr
log.info('Writing: {0}'.format(' '.join(tmp_arr)))
popen_wrapper(cmd_arr=tmp_arr, raise_err=True)
# Exclude IPs/CIDRs from logging (scanners, monitoring, pen-testers,
# etc):
for addr in self.srcexclude.split(','):
tmp_arr = [
'iptables',
'-A', 'SINKHOLE',
'-s', addr,
'-j', 'RETURN'
]
# run sudo if not root
if uid != 0:
tmp_arr = ['/usr/bin/sudo'] + tmp_arr
log.info('Writing: {0}'.format(' '.join(tmp_arr)))
popen_wrapper(cmd_arr=tmp_arr, raise_err=True)
# Tell the chain to log and use the prefix self.log_prefix:
tmp_arr = [
'iptables',
'-A', 'SINKHOLE',
'-j', 'LOG',
'--log-prefix', self.log_prefix
]
# run sudo if not root
if uid != 0:
tmp_arr = ['/usr/bin/sudo'] + tmp_arr
log.info('Writing: {0}'.format(' '.join(tmp_arr)))
popen_wrapper(cmd_arr=tmp_arr, raise_err=True)
# Tell the chain to also log to netfilter (for packet capture):
tmp_arr = ['iptables', '-A', 'SINKHOLE',
'-j', 'NFLOG']
# run sudo if not root
if uid != 0:
tmp_arr = ['/usr/bin/sudo'] + tmp_arr
log.info('Writing: {0}'.format(' '.join(tmp_arr)))
popen_wrapper(cmd_arr=tmp_arr, raise_err=True)
# Tell the chain to trigger on hashlimit and protocol/port settings
tmp_arr = [
'iptables',
'-i', self.interface,
'-d', self.interface_addr,
'-j', 'SINKHOLE',
'-m', 'hashlimit',
'--hashlimit', self.hashlimit,
'--hashlimit-burst', self.hashlimitburst,
'--hashlimit-mode', self.hashlimitmode,
'--hashlimit-name', 'sinkhole',
'--hashlimit-htable-expire', self.hashlimitexpire,
'-I', 'INPUT', '1'
]
# run sudo if not root
if uid != 0:
tmp_arr = ['/usr/bin/sudo'] + tmp_arr
# if --protocol filtered, set mode to multiport with protocol, and
# set destination port if provided and applicable to the protocol(s)
if self.protocol != 'all':
tmp_arr += ['-m', 'multiport', '--protocol', self.protocol]
if self.dport != '0:65535':
tmp_arr += ['--dport', self.dport]
log.info('Writing: {0}'.format(' '.join(tmp_arr)))
popen_wrapper(cmd_arr=tmp_arr, raise_err=True)
[docs] def create_drop_rule(self):
"""
The function for writing the iptables DROP rule for the interface.
"""
log.info('Checking for existing iptables DROP rules.')
existing = self.list_existing_rules(filter_io_drop=True)
# Existing sinkhole related iptables lines found, can't create.
if len(existing) > 0:
for line in existing:
if line in (
'-A INPUT -i {0} -j DROP'.format(self.interface),
'-A OUTPUT -o {0} -j DROP'.format(self.interface)
):
raise IPTablesExists('Existing iptables DROP rules found '
'for nfsinkhole:\n{0}'
''.format(line))
log.info('Writing iptables DROP config')
# Create rules to drop all I/O traffic:
tmp_arr = [
'iptables',
'-i', self.interface,
'-j', 'DROP',
'-I', 'INPUT', '1'
]
# run sudo if not root
if uid != 0:
tmp_arr = ['/usr/bin/sudo'] + tmp_arr
log.info('Writing: {0}'.format(' '.join(tmp_arr)))
popen_wrapper(cmd_arr=tmp_arr, raise_err=True)
tmp_arr = [
'iptables',
'-o', self.interface,
'-j', 'DROP',
'-I', 'OUTPUT', '1'
]
# run sudo if not root
if uid != 0:
tmp_arr = ['/usr/bin/sudo'] + tmp_arr
log.info('Writing: {0}'.format(' '.join(tmp_arr)))
# TODO: replicate exception handling to other subprocess calls
try:
popen_wrapper(cmd_arr=tmp_arr, raise_err=True)
except SubprocessError as e:
raise IPTablesError(e)
[docs] def delete_rules(self):
"""
The function for deleting iptables rules related to nfsinkhole.
"""
log.info('Checking for existing iptables rules.')
existing = self.list_existing_rules()
# No sinkhole related iptables lines found.
if len(existing) == 0:
raise IPTablesNotExists('No existing rules found.')
log.info('Deleting iptables config (only what was created)')
# Iterate all of the active sinkhole related iptables lines
flush = False
for line in existing:
if '-A SINKHOLE' in line or line == '-N SINKHOLE':
# Don't try to delete the SINKHOLE chain yet, it needs to be
# empty. Set flush to clear it after this loop.
flush = True
elif line not in (
'-A INPUT -i {0} -j DROP'.format(self.interface),
'-A OUTPUT -o {0} -j DROP'.format(self.interface)
):
# Delete a single line (not the SINKHOLE chain itself).
stmt = line.replace('-A', '-D').strip().split(' ')
tmp_arr = ['iptables'] + stmt
# run sudo if not root
if uid != 0:
tmp_arr = ['/usr/bin/sudo'] + tmp_arr
log.info('Deleting: {0}'.format(' '.join(tmp_arr)))
popen_wrapper(cmd_arr=tmp_arr, raise_err=True)
# The SINKHOLE chain was detected. Remove it.
if flush:
# All lines in the SINKHOLE chain should have been flushed already,
# but run a flush to be sure.
tmp_arr = ['iptables', '-F', 'SINKHOLE']
# run sudo if not root
if uid != 0:
tmp_arr = ['/usr/bin/sudo'] + tmp_arr
log.info('Flushing: {0}'.format(' '.join(tmp_arr)))
popen_wrapper(cmd_arr=tmp_arr, raise_err=True)
# Now that the SINKHOLE chain has been flushed, we can delete it.
tmp_arr = ['iptables', '-X', 'SINKHOLE']
# run sudo if not root
if uid != 0:
tmp_arr = ['/usr/bin/sudo'] + tmp_arr
log.info('Deleting: {0}'.format(' '.join(tmp_arr)))
popen_wrapper(cmd_arr=tmp_arr, raise_err=True)
# Return a list of matching lines.
return len(existing)
[docs] def delete_drop_rule(self):
"""
The function for deleting the iptables DROP rule for the interface.
"""
log.info('Checking for existing iptables DROP rules.')
existing = self.list_existing_rules(filter_io_drop=True)
# No sinkhole related iptables lines found.
if len(existing) == 0:
raise IPTablesNotExists('No existing rules found.')
log.info('Deleting iptables DROP config.')
count = 0
for line in existing:
if line in (
'-A INPUT -i {0} -j DROP'.format(self.interface),
'-A OUTPUT -o {0} -j DROP'.format(self.interface)
):
count += 1
# Delete a single line (not the SINKHOLE chain itself).
stmt = line.replace('-A', '-D').split(' ')
tmp_arr = ['iptables'] + stmt
# run sudo if not root
if uid != 0:
tmp_arr = ['/usr/bin/sudo'] + tmp_arr
log.info('Deleting: {0}'.format(' '.join(tmp_arr)))
popen_wrapper(cmd_arr=tmp_arr, raise_err=True)
# Return the number of matching lines.
return count