Commit c737c1e0 authored by Patrick J Cherry's avatar Patrick J Cherry
Browse files

* Names now resolved to IPs (A and AAAA records)

 * Command options tidied
 * Manpages written and tidied
parent b0f20d33
......@@ -22,20 +22,16 @@ clean:
manpages/%.man: ./sbin/%
[ -d ./manpages ] || mkdir ./manpages
RUBYLIB=./lib $< --help | sed -e 's/^=\+$$//' | txt2man -s 1 -t $(notdir $<) | sed -e 's/\\\\fB/\\fB/' > $@
RUBYLIB=./lib:./ext $< --manual | sed -e 's/^=\+$$//' | txt2man -s 1 -t $(notdir $<) | sed -e 's/\\\\fB/\\fB/' > $@
manpages/%.1: ./bin/%
[ -d ./manpages ] || mkdir ./manpages
pod2man -s 1 $< > $@
manpages: ./manpages/symbiosis-firewall-whitelist.man ./manpages/symbiosis-firewall.man ./manpages/firewall.1 ./manpages/firewall-logtail.1 ./manpages/firewall-blacklist.1
manpages: ./manpages/symbiosis-firewall-whitelist.man ./manpages/symbiosis-firewall.man ./manpages/symbiosis-firewall-blacklist.man
all: manpages ext/symbiosis_utmp.so
distclean: clean
test: ext/symbiosis_utmp.so
#@cd test && ruby ./ts_firewall.rb
@cd test && ruby ./ts_firewall.rb
@if [ ! -d ./i ]; then mkdir ./i ; fi
@if [ ! -d ./i/incoming.d/ ]; then mkdir ./i/incoming.d/; fi
@if [ ! -d ./i/outgoing.d/ ]; then mkdir ./i/outgoing.d/; fi
......@@ -69,7 +65,7 @@ test: ext/symbiosis_utmp.so
@echo "212.110.161.177" > i/outgoing.d/20-accept
@echo "2001:41c8:20:862:ac1:1::" >> i/outgoing.d/20-accept
@touch i/outgoing.d/99-reject
@ruby -I lib ./sbin/symbiosis-firewall -b ./i -T rule.d -x -v -d
@ruby -I lib ./sbin/symbiosis-firewall -p ./i -t rule.d -x -v -d
ext/symbiosis_utmp.so: ext/Makefile
make -C ext $(notdir $@)
......
......@@ -3,12 +3,12 @@ Section: net
Priority: extra
Maintainer: Steve Kemp <steve@bytemark.co.uk>
Uploaders: Patrick J Cherry <patch@bytemark.co.uk>
Build-Depends: debhelper (>= 7.0.0), cdbs, txt2man, ruby-pkg-tools, ruby-dev
Build-Depends: debhelper (>= 7.0.0), cdbs, txt2man, ruby-pkg-tools, ruby-dev, ruby1.8-dev
Standards-Version: 3.8.0
Package: symbiosis-firewall
Architecture: any
Depends: iptables, ruby, ruby1.8, symbiosis-common, ${shlibs:Depends}, ${misc:Depends}
Depends: iptables, ruby, ruby1.8, symbiosis-common, libruby1.8, libsqlite3-ruby1.8, ${shlibs:Depends}, ${misc:Depends}
Replaces: bytemark-vhost-ssh-protection, bytemark-vhost-firewall
Conflicts: bytemark-vhost-firewall, bytemark-vhost-ssh-protection (<< 20081110153344)
Description: A simple firewall generator for the Bytemark vhost system
......
require 'symbiosis/firewall/template'
require 'resolv-replace'
#
# A directory, like incoming.d, blacklist.d, local.d.
#
......@@ -50,8 +52,8 @@ module Symbiosis
#
# Read the rules, and generate.
#
do_read.each do |template, addresses|
rules += do_generate_rules( template, addresses )
do_read.each do |template, hostnames|
rules += do_generate_rules( template, hostnames )
end
return rules.join("\n")
......@@ -80,9 +82,24 @@ module Symbiosis
#
# This applies the template, and catches any error in its generation
#
def do_generate_rules(template, addresses)
def do_generate_rules(template, hostnames)
rules = []
addresses = []
#
# resolve addresses
#
hostnames.each do |hostname|
if hostname.is_a?(String)
addresses += do_resolve_name(hostname)
else
addresses << hostname
end
end
#
# Now, for each address create a template and add it to our rules.
#
addresses.each do |address|
begin
#
......@@ -94,7 +111,7 @@ module Symbiosis
#
# Catch any error and display neatly.
#
msg = "Ignoring #{self.direction} rule #{template} #{address.nil? ? "" : "to #{address.inspect} "}because #{err.to_s}"
msg = "Ignoring #{self.direction} rule #{template.name} #{address.nil? ? "" : "to #{address.inspect} "}because #{err.to_s}"
warn msg
rules << "# #{msg}"
end
......@@ -103,6 +120,46 @@ module Symbiosis
return rules
end
#
# Resolve hostnames to A and AAAA records.
#
# The name is a string, and can be a hostname or an IP address. A
# hostname will get resolved to a set of IP addresses, based on the A or
# AAAA records available.
#
def do_resolve_name(name)
ips = []
begin
ips << IPAddr.new(name)
rescue ArgumentError
%w(A AAAA).each do |type|
begin
Resolv::DNS.open do |dns|
#
# This works with CNAME records too, depending on what the
# resolver gives us.
#
dns.getresources(name, Resolv::DNS::Resource::IN.const_get(type)).each do |a|
#
# Convert to IPAddr straight away, ignoring errors.
#
begin
ips << IPAddr.new(a.address.to_s)
rescue ArgumentError
warn "#{type} record for #{name} returned duff IP #{a.address.to_s.inspect}." if $VERBOSE
end
end
end
rescue Resolv::ResolvError, Resolv::ResolvTimeout => e
warn "#{name} could not be resolved because #{e.message}." if $VERBOSE
end
end
end
ips.uniq
end
end
......@@ -143,17 +200,17 @@ module Symbiosis
#
# File.readlines always returns an array, one element per line, even for dos-style files.
#
addresses = File.readlines( File.join( self.path, entry ) ).collect{|l| l.chomp}
hostnames = File.readlines( File.join( self.path, entry ) ).collect{|l| l.chomp.strip}
#
# Add a dummy address of nil if there are no addresses in the list
# Add a dummy address of nil if there are no hostnames in the list
#
addresses << nil if addresses.empty?
hostnames << nil if hostnames.empty?
#
# Append our result
#
results << [template, addresses]
results << [template, hostnames]
end
#
......@@ -177,7 +234,17 @@ module Symbiosis
# |--- 1.2.3.4
# \-- 1.4.4.4
#
# 0 directories, 3 files
# If the name looks like an IP address and is of the form
#
# 1.2.3.4-24
#
# or
#
# 2001:dead:beef:cafe::1-64
#
# then these would be mangled to become 1.2.3.4/24 or
# 2001:dead:beef:cafe::1/64 respectively, before being transformed into
# an IP address.
#
# Each file can contain a list of ports/services/templates, or the word
# "all", or nothing at all.
......@@ -201,7 +268,7 @@ module Symbiosis
#
# A hash of arrays
#
port_addresses = Hash.new{|i,j| i[j] = []}
port_hostnames = Hash.new{|i,j| i[j] = []}
#
# Read the contents of the directory
......@@ -215,7 +282,14 @@ module Symbiosis
#
# Here we need to strip the optional ".auto" suffix.
#
ip = File.basename(file,".auto")
hostname = File.basename(file,".auto").downcase
#
# Cope with ranges by unmangling the CIDR notation.
#
if hostname =~ /^([0-9a-f\.:]+)-([0-9]+)$/
hostname = [$1, $2].join("/")
end
#
# Now see if the file contains any lines for ports
......@@ -248,21 +322,21 @@ module Symbiosis
# Save each port/address combo.
#
ports.each do |port|
port_addresses[port] << ip
port_hostnames[port] << hostname
end
end
#
# Now translate our ports into templates.
#
port_addresses.each do |port, addresses|
port_hostnames.each do |port, hostnames|
template_path = do_find_template( self.default )
template = Template.new( template_path )
template.name = self.default
template.direction = self.direction
template.port = port unless port.nil?
template.chain = self.chain unless self.chain.nil?
templates << [template, addresses]
templates << [template, hostnames]
end
return templates
......
......@@ -94,13 +94,17 @@ module Symbiosis
#
def address=( new_address )
#
# Cope with ranges.
# If we're given an IPAddr, suck it up, otherwise if it is a string,
# convert it.
#
if new_address.downcase =~ /^([0-9a-f\.:]+)-([0-9]+)$/
new_address = [$1, $2].join("/")
case new_address
when IPAddr
@address = new_address
when String
@address = IPAddr.new(new_address)
else
raise ArgumentError, "Cannot do much with #{new_address.inspect}"
end
@address = IPAddr.new(new_address)
end
#
......
......@@ -17,4 +17,3 @@
<%= cmd %> -A <%= chain%> <%= src_or_dst %> -j ACCEPT
% end
% end
#!/usr/bin/ruby
#
# NAME
# firewall -- The symbiosis firewall package.
# symbiosis-firewall -- Symbioisis firewall management
#
# SYNOPSIS
# General Options:
# symbiosis-firewall [ -h | --help ] [-m | --manual] [ -v | --verbose ]
# [ -p | --prefix <dir> ] [ -t | --template-d <dir> ]
# [ -x | --no-exec] [ -d | --no-delete ] <action>
#
# Help Options:
# OPTIONS
# -h, --help Show a help message, and exit.
#
# --help Show the help information for this script.
# --verbose Show debugging information.
# -m, --manual Show this manual, and exit.
#
# -v, --verbose Show verbose errors
#
# DETAILS
# This script is designed to both generate and load a simple firewall
# based upon the contents of a hierarchy of flat files and directories.
# -p, --prefix <dir> Directory where action.d, incoming.d, outgoing.d etc
# are located. Defaults to /etc/symbiosis/firewall.
#
# This means that creating a firewall will be as simple as touching a file
# or removing and existing one.
# -t, --template-d <dir> Additional directory to search for templates.
#
# -x, --no-exec Do not execute the generated firewall rules
#
# AUTHOR
# ------
# -d, --no-delete Do not delete the generated script
#
# <action> The action to run. This defaults to "load".
#
# USAGE
#
# This firewall script is designed to be simple to use, while still allowing a
# reasonable level of control over your system. This command is used to update
# the iptables(8) and ip6tables(8) firewalls. It uses a set of directories in
# the prefix directory to define which rules should be applied.
#
# The script will be executed once it has been generated, and then removed.
# (You may use --no-delete and --no-execute to prevent either action from being
# carried out.)
#
# Usage of the Symbiosis firewall is comprehensively documented in the
# symbiosis-documentation package, as well as on the documentation website.
#
# CONFIGURATION
#
# To configure the firewall which is generated and applied to your server you
# simply need to create files in the directories the script reads:
#
# $PREFIX/incoming.d/ This directory is examined to determine which rules
# should be applied to incoming connections.
#
# $PREFIX/outgoing.d/ This directory is examined to determine which rules
# should be applied to outgoing connections.
#
# $PREFIX/blacklist.d/ Any file present in this directory is assumed to be the
# IP address of a machine you wish to globally prevent
# connections from.
#
# $PREFIX/whitelist.d/ Any file present in this directory is assumed to be the
# IP address of a machine you wish to globally allow connections from.
#
# $PREFIX/local.d/ Executable shell-scripts in this directory are executed
# after the firewall is installed.
#
# Steve Kemp <steve@bytemark.co.uk>
# For the incoming and outgoing directories you should create files with names
# such as "10-ssh". (The prefix you choose merely determines sorting order.)
#
# The presence of a file named "NN-ssh" will mean that the firewall will
# include rules it knows about for the service "ssh". These rule types may be
# arbitrarily complex, as they are processed via bash(1).
#
# The presence of a rule file will allow access to the named service. For
# example the file "10-ssh" placed in the incoming directory will allow all
# access to port 22. If you wish to restrict access place the hostnames, or IP
# addresses, in the file instead of leaving it empty. This will restrict
# access to/from the named addresses.
#
# ACTIONS AND TEMPLATES
#
# The "actions" are bash(1) scripts, that have been templated using eRuby.
# Each action is found in the action.d directory located inside the prefix
# directory.
#
# The actions that this program comes with are
#
# load Load the firewall.
# flush Flush the firewall.
# reload-blacklist Update the blacklist chain.
# reload-whitelist Update the whitelist chain.
#
# It is possible to add your own actions to the template as needed.
#
# ADDING ADDITIONAL RULETYPES
#
# For each "rule" type you should simply create two files:
#
# /usr/local/share/symbiosis/firewall/rule.d/$name.incoming
# /usr/local/share/symbiosis/firewall/rule.d/$name.outgoing
#
# The contents of these file(s) will be inserted appropriately into the
# generated firewall script.
#
# The magic strings '$SRC' and '$DEST' will be replaced by any IP addresses the
# user has specified in their file - or removed if none are present.
#
# SEE ALSO
# symbiosis-firewall-whitelist(1), symbiosis-firewall-blacklist(1),
# iptables(8), ip6tables(8)
#
# AUTHOR
# Steve Kemp <steve@bytemark.co.uk>
# Patrick J Cherry <patrick@bytemark.co.uk>
#
......@@ -44,85 +128,82 @@ end
#######
##############
#
# Parse the arguments
#
help = false
manual = false
$VERBOSE = false
test = false
flush = false
execute = true
delete = true
base_dir = '/etc/symbiosis/firewall'
base_dir = '/etc/symbiosis/firewall'
template_dir = '/usr/share/symbiosis/firewall/rule.d'
services = '/etc/services'
#
# This allows us just to symlink straight to /etc/network/if-up.d and if-down.d
#
if ENV.has_key?('IFACE')
flush = true if "stop" == ENV['MODE']
$VERBOSE = true if ENV.has_key?('VERBOSE')
end
opts = GetoptLong.new(
[ '--help', '-h', GetoptLong::NO_ARGUMENT ],
[ '--manual', '-m', GetoptLong::NO_ARGUMENT ],
[ '--verbose', '-v', GetoptLong::NO_ARGUMENT ],
[ '--test', '-t', GetoptLong::NO_ARGUMENT ],
[ '--flush', '-f', GetoptLong::NO_ARGUMENT ],
[ '--no-execute', '-x', GetoptLong::NO_ARGUMENT ],
[ '--no-delete', '-d', GetoptLong::NO_ARGUMENT ],
[ '--no-root', '-R', GetoptLong::NO_ARGUMENT ],
[ '--base-d', '-b', GetoptLong::REQUIRED_ARGUMENT ],
[ '--template-d', '-T', GetoptLong::REQUIRED_ARGUMENT ],
[ '--services', '-s', GetoptLong::REQUIRED_ARGUMENT ]
)
[ '--help', '-h', GetoptLong::NO_ARGUMENT ],
[ '--manual', '-m', GetoptLong::NO_ARGUMENT ],
[ '--verbose', '-v', GetoptLong::NO_ARGUMENT ],
[ '--no-execute', '-x', GetoptLong::NO_ARGUMENT ],
[ '--no-delete', '-d', GetoptLong::NO_ARGUMENT ],
[ '--prefix', '-p', GetoptLong::REQUIRED_ARGUMENT ],
[ '--template-d', '-t', GetoptLong::REQUIRED_ARGUMENT ]
)
opts.each do |opt,arg|
case opt
when '--help'
help = true
when '--manual'
manual = true
when '--verbose'
$VERBOSE = true
when '--flush'
flush = true
when '--test'
when '--test'
test = true
when '--no-execute'
execute = false
when '--no-delete'
delete = false
when '--base-d'
base_dir = File.expand_path(arg)
when '--prefix'
base_dir = File.expand_path(arg)
when '--template-d'
template_dir = File.expand_path(arg)
when '--services'
services = arg
end
end
#
# Set the action
# CAUTION! Here be quality kode.
#
action = ARGV.first || "load"
if ( help )
if manual or help
# Open the file, stripping the shebang line
lines = File.open(__FILE__){|fh| fh.readlines}[2..-1]
lines = File.open(__FILE__){|fh| fh.readlines}[1..-1]
found_synopsis = false
lines.each do |line|
line.chomp!
break if line.empty?
if help and !found_synopsis
found_synopsis = (line =~ /^#\s+SYNOPSIS\s*$/)
next
end
puts line[2..-1].to_s
break if help and found_synopsis and line =~ /^#\s*$/
end
exit 0
end
#
# Set the action
#
action = ARGV.first || "load"
# DONE parseCommandLineArguments
# TODO sanityCheck
# DONE flushAllRules
......@@ -144,7 +225,7 @@ end
begin
include Symbiosis::Firewall
Ports.load(services)
Ports.load
#
# Write the firewall to a local file
......
#! /usr/bin/ruby1.8
#
# NAME
#
# symbiosis-firewall-blacklist -- Automatically blacklist IP addresses.
#
# SYNOPSIS
# symbiosis-firewall-blacklist [ -h | --help ] [-m | --manual]
# [ -v | --verbose ] [ -x | --no-exec] [ -d | --no-delete ]
# [ -a | --attempts <n> ] [ -e | --expire-after <n> ]
# [ -p | --prefix <dir> ] [ -t | --template-d <dir> ]
#
# OPTIONS
# -h, --help Show a help message, and exit.
#
# -m, --manual Show this manual, and exit.
#
# -v, --verbose Show verbose errors
#
# -x, --no-exec Do not execute the generated firewall rules
#
# -d, --no-delete Do not delete the generated script
#
# Options:
# -a, --attempts <n> Number of attempts before an IP address is
# blacklisted. Defaults to 20.
#
# --prefix The directory to operate upon.
# -e, --expire-after <n> Number of days after which blacklisted IPs should be
# expired. Defaults to 2.
#
# Help Options:
# -p, --prefix <dir> Directory where action.d, incoming.d, outgoing.d etc
# are located. Defaults to /etc/symbiosis/firewall.
#
# --help Show the help information for this script.
# --verbose Show debugging information.
# -t, --template-d <dir> Additional directory to search for templates.
#
# USAGE
#
# This script is designed to automatically blacklist IP addresses which
# have been used to successfully login via SSH.
# have been used to brute force various services running on the machine.
#
# It uses a set of definitions found in $PREFIX/pattern.d/ to match IP
# addresses in log files, and then adds the offending IPs to the blacklist by
# adding files to the directory $PREFIX/blacklist.d.
#
# Each addition is one of the two forms:
#
# 1.2.3.4.auto The IPv4 address 1.2.3.4
# 2001:123:456:789::-64.auto The IPv6 range 2001:123:456:789::/64
#
# It should be noted that IPv6 addresses will be added as entire /64s.
#
# Each file will contain a list of ports, one per line, or simply "all" to
# blacklist all ports.
#
# It does this by parsing the output of the "last" command, and creating
# entries in /etc/symbiosis/firewall/blacklist.d/
# Once that directory has been written, symbiosis-firewall(1) is called with
# the reload-blacklist action.
#
# Most of the flags above are passed straigh on to symbiosis-firewall(1).
#
# SEE ALSO
#
# symbiosis-firewall(1), symbiosis-firewall-whitelist(1)
#
# AUTHOR
#
# Steve Kemp <steve@bytemark.co.uk>
#
#
# Modules we require
#
# TODO: fix manpage (above)
require 'getoptlong'
require 'tempfile'
require 'symbiosis/utmp'
require 'symbiosis/firewall/blacklist'
require 'symbiosis/firewall/directory'
require 'symbiosis/firewall/template'
require 'symbiosis/firewall/ipaddr'
require 'symbiosis/firewall/logtail'
require 'symbiosis/firewall/pattern'
opts = GetoptLong.new(
[ '--help', '-h', GetoptLong::NO_ARGUMENT ],
[ '--verbose', '-v', GetoptLong::NO_ARGUMENT ],
[ '--prefix', '-p', GetoptLong::REQUIRED_ARGUMENT ],
[ '--template-d', '-t', GetoptLong::REQUIRED_ARGUMENT ]
)
#
# The options set by the command line.
#
$HELP = false