sympl-firewall-blacklist 8.31 KB
Newer Older
1
#! /usr/bin/ruby
2
3
#
# NAME
4
#   sympl-firewall-blacklist - Automatically blacklist IP addresses.
5
6
#
# SYNOPSIS
7
#  sympl-firewall-blacklist [ -h | --help ] [-m | --manual]
8
#       [ -v | --verbose ] [ -x | --no-exec] [ -d | --no-delete ]
9
#       [ -a | --block-after <n> ] [ -e | --expire-after <n> ]
10
#       [ -p | --prefix <dir> ] 
11
12
13
14
15
16
17
18
19
20
21
#
# 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
22
#
23
#  -a, --block-after <n>   Number of attempts before an IP address is
24
#                          blacklisted. Defaults to 25.
25
26
27
#
#  -b, --block-all-after <n>  Number of attempts before an IP address is
#                             blocked from all ports, not just the ports
28
#                             mentioned in the pattern. Defaults to 100.
29
#
30
31
#  -e, --expire-after <n>  Number of days after which blacklisted IPs should be
#                          expired. Defaults to 2.
32
#
33
#  -p, --prefix <dir>      Directory where incoming.d, outgoing.d etc are
34
#                          located. Defaults to /etc/sympl/firewall.
35
#
36
# USAGE
37
38
#
# This script is designed to automatically blacklist IP addresses which
39
40
41
42
43
44
45
46
47
# 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
48
#   2001:123:456:789::|64.auto  The IPv6 range 2001:123:456:789::/64
49
50
51
52
53
# 
# 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.
54
#
55
# Once that directory has been written, sympl-firewall(1) is called with
56
57
# the reload-blacklist action.
#
58
# Most of the flags above are passed straight on to sympl-firewall(1).
59
60
61
#
# SEE ALSO
#
62
# sympl-firewall(1), sympl-firewall-whitelist(1)
63
64
65
66
67
68
#
# AUTHOR
#
#   Steve Kemp <steve@bytemark.co.uk>
#

69
# TODO: fix manpage (above)
70
71
72

require 'getoptlong'
require 'tempfile'
73
require 'fileutils'
74
require 'syslog'
75
76
77
78

#
#  The options set by the command line.
#
79
80
help         = false
manual       = false
81
$VERBOSELOCAL     = false
82
base_dir     = "/etc/sympl/firewall/"
83
84
delete       = true
execute      = true
85
force        = false
86
87
block_after     = 25
block_all_after = 100
88
89
90
91
92
93
94
95
96
97
expire_after = 2

opts = GetoptLong.new(
         [ '--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 ],
         [ '--force',      '-f', GetoptLong::NO_ARGUMENT ],
         [ '--prefix',     '-p', GetoptLong::REQUIRED_ARGUMENT ],
98
99
         [ '--block-after',    '-a', GetoptLong::REQUIRED_ARGUMENT ],
         [ '--block-all-after','-b', GetoptLong::REQUIRED_ARGUMENT ],
100
101
         [ '--expire-after', '-e', GetoptLong::REQUIRED_ARGUMENT ]
       )
102

103
104
105
106
107
108
109
110
begin
  opts.each do |opt,arg|
    case opt
    when '--help'
      help = true
    when '--manual'
      manual = true
    when '--verbose'
111
      $VERBOSELOCAL = true
112
113
114
115
116
117
118
119
120
121
122
123
    when '--test'
      test = true
    when '--no-execute'
      execute = false
    when '--no-delete'
      delete = false
    when '--force'
      force = true
    when '--prefix'
      base_dir     = File.expand_path(arg)
    when '--expire-after'
      expire_after = arg.to_i
124
    when '--block-after'
125
      block_after = arg.to_i
126
    end
127
  end
128
129
130
rescue
  # any errors, show the help
  help = true
131
132
end

133

134
135
136
#
# CAUTION! Here be quality kode.
#
137
if manual or help
138
  # Open the file, stripping the shebang line
139
140
141
  lines = File.open(__FILE__){|fh| fh.readlines}[1..-1]

  found_synopsis = false
142
143

  lines.each do |line|
144

145
146
    line.chomp!
    break if line.empty?
147
148
149
150
151
152

    if help and !found_synopsis
      found_synopsis = (line =~ /^#\s+SYNOPSIS\s*$/)
      next
    end

153
    puts line[2..-1].to_s
154
155
156

    break if help and found_synopsis and line =~ /^#\s*$/

157
158
159
160
161
  end

  exit 0
end

162
163
164
# Open syslog
syslog = Syslog.open( File.basename($0), Syslog::LOG_NDELAY, Syslog::LOG_USER)

165
166
167
168
#
# These requires are here to prevent dependency failures when generating manpages.
#
require 'symbiosis/ipaddr'
169
require 'symbiosis/utils'
170
171
172
173
174
175
require 'symbiosis/firewall/blacklist'
require 'symbiosis/firewall/directory'
require 'symbiosis/firewall/template'
require 'symbiosis/firewall/logtail'
require 'symbiosis/firewall/pattern'

176
177
178
#
# Exit if we've been disabled
#
179
if %w(disabled.blacklist blacklist.d/disabled).any?{|fn| File.exist?(File.join(base_dir, fn))}
180
  puts "Firewall blacklist disabled.  Exiting." if $VERBOSELOCAL
181
182
  exit 0
end
183

184
185
186
#
# Work out which user we're supposed to create the blacklist directory as.
#
187
188
189
190
191
192
193
begin
  srv = File.stat("/srv")
  admin_uid = srv.uid
  admin_gid = srv.gid
rescue Errno::ENOENT
  admin_gid = admin_uid = 0
end
194

195
expired = 0
196
blacklist_d = File.join(base_dir, "blacklist.d")
197

198
# 
199
# ensure the directory exists.
200
201
#
unless File.directory?( blacklist_d )
202
  Symbiosis::Utils.mkdir_p(blacklist_d, :user => admin_uid, :group => admin_gid)
203
204
205
206
207
end

#
# Fetch the IP addresses
#
208
blacklist = Symbiosis::Firewall::Blacklist.new
209
210
blacklist.block_after = block_after
blacklist.block_all_after = block_all_after
211
blacklist.base_dir = base_dir
212
213
214
215
216
217
218
219
220
221

#
#  Did we update?
#
updated=false

#
#  Iterate over each IP
#
blacklist.generate.each do |ip, ports|
222
223
224
225
  #
  # Make sure we can parse stuff
  #
  begin
226
    ip = Symbiosis::IPAddr.new(ip)
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
  rescue ArgumentError => err
    warn "Ignoring #{ip.inspect} because of #{err.to_s}"
    next
  end
  
  #
  # Mask IPv6 to /64s.
  #
  ip = ip.mask(64) if ip.ipv6?

  #
  # Mask IPv4 to /32s.
  #
  ip = ip.mask(32) if ip.ipv4?

  #
  # Only include globally routable IPs.
  #
  # FIXME: Need better IPv6 conditions.
  #
247
248
  next if ip.ipv4? and (Symbiosis::IPAddr.new("127.0.0.1/8").include?(ip) or Symbiosis::IPAddr.new("0.0.0.0") == ip )
  next if ip.ipv6? and !Symbiosis::IPAddr.new("2000::/3").include?(ip)
249

250
  puts "Found IP address: #{ip}" if ( $VERBOSELOCAL )
251

252
253
  setting = ip.to_s.gsub("/","|")

254
255
256
  #
  # Check filename without .auto first.
  #
257
  if !Symbiosis::Utils.get_param(setting, blacklist_d)
258
259
260
    #
    # Automatically blacklist.
    #
261
    setting += ".auto"
262

263
    old_ports = Symbiosis::Utils.get_param(setting, blacklist_d)
264

265
266
    if old_ports.is_a?(String) or true == old_ports
      
267
      #
268
      # Set old_ports to everything if it is just "true" (i.e. an empty file).
269
      #
270
271
      old_ports = "all" if true == old_ports
      old_ports = old_ports.split($/).collect{|pt| pt.strip }
272

273
      ports = (ports + old_ports).collect{|pt| pt.nil? ? "all" : pt.to_s }.uniq
274

275
      ports = %w(all) if ports.any?{|pt| "all" == pt}
276
      
277
      puts "\tUpdating blacklist entry for #{ports.join(",")} ports" if  $VERBOSELOCAL 
278
      syslog.info "updating blacklisted IP #{ip} for #{ports.join(",")} ports"
279
280
    else
      #
281
      # Add to the blacklist.
282
      #
283
      puts "\tAdding to blacklist for #{ports.join(",")} ports" if ( $VERBOSELOCAL )
284
      syslog.info "adding #{ip} to blacklist for #{ports.join(",")} ports"
285
    end
286

287
    updated=true
288

289
290
291
    Symbiosis::Utils.set_param(setting, ports.join("\n"), blacklist_d)

  else
292
    puts "\tAlready manually blacklisted" if ( $VERBOSELOCAL )
293
294
295
296
297
298
299
300
301
302
303
304
305

  end

end

#
# Expiry is defined in terms of days.
#
expire_before = Time.now - ( expire_after * ( 24 * 60 * 60 ) )

#
#  Now expire old entries.
#
306
puts "Expiring old blacklist entries" if ( $VERBOSELOCAL )
307

308
309
310
311
Dir.glob( File.join(blacklist_d,"*.auto" ) ).each do |entry|

  if  File.mtime(entry) < expire_before

312
    puts "Removing #{entry}" if ( $VERBOSELOCAL )
313
    syslog.info "removing blacklisted IP #{File.basename(entry,".auto")}"
314
315
    File.unlink(entry)
    expired += 1
316
  end
317

318
319
end

320
puts "Expiring done - removed #{expired} file(s)" if ( $VERBOSELOCAL )
321
322

#
323
324
325
326
327
328
329
330
331
332
333
334
# Re-generate the blacklist chain
#
if ( updated || expired > 0 || force )
  cmd = %w(/usr/sbin/sympl-firewall)
  cmd << "--verbose" if $VERBOSELOCAL
  cmd << "--no-execute" unless execute
  cmd << "--no-delete"  unless delete
  cmd += ["--prefix", base_dir]
  cmd << "reload-blacklist"
  puts "Running #{cmd.join(" ")}" if $VERBOSELOCAL
  exec(*cmd)
end