From d9f1c04e912d44c218a90b9b0debcb0a11b87631 Mon Sep 17 00:00:00 2001 From: Giuseppe Bilotta Date: Fri, 27 Oct 2006 14:18:23 +0000 Subject: [PATCH] Penalty-based flood protection --- ChangeLog | 6 ++++ lib/rbot/ircbot.rb | 10 ++---- lib/rbot/ircsocket.rb | 74 ++++--------------------------------------- lib/rbot/rfc2812.rb | 64 +++++++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 75 deletions(-) diff --git a/ChangeLog b/ChangeLog index 203bd3c7..fae97f54 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,9 @@ +2006-10-27 Giuseppe Bilotta + + * Flood protection: first attempt at penalty-based flood protection. + This should make rbot much less prone to Excess Floods *and* still + serve normally without excessive delays. + 2006-10-25 Giuseppe Bilotta * HttpUtil: Strings returned by get_cached now have a cached? method diff --git a/lib/rbot/ircbot.rb b/lib/rbot/ircbot.rb index 8249895b..4bf3e8d3 100644 --- a/lib/rbot/ircbot.rb +++ b/lib/rbot/ircbot.rb @@ -173,10 +173,6 @@ class IrcBot :default => 4, :validate => Proc.new{|v| v >= 0}, :desc => "(flood prevention) max lines to burst to the server before throttling. Most ircd's allow bursts of up 5 lines", :on_change => Proc.new {|bot, v| bot.socket.sendq_burst = v }) - BotConfig.register BotConfigStringValue.new('server.byterate', - :default => "400/2", :validate => Proc.new{|v| v.match(/\d+\/\d/)}, - :desc => "(flood prevention) max bytes/seconds rate to send the server. Most ircd's have limits of 512 bytes/2 seconds", - :on_change => Proc.new {|bot, v| bot.socket.byterate = v }) BotConfig.register BotConfigIntegerValue.new('server.ping_timeout', :default => 30, :validate => Proc.new{|v| v >= 0}, :on_change => Proc.new {|bot, v| bot.start_server_pings}, @@ -654,12 +650,10 @@ class IrcBot # relevant say() or notice() methods. This one should be used for IRCd # extensions you want to use in modules. def sendmsg(type, where, message, chan=nil, ring=0) - # limit it according to the byterate, splitting the message - # taking into consideration the actual message length - # and all the extra stuff + # Split the message so that each line sent is not longher than 510 bytes # TODO allow something to do for commands that produce too many messages # TODO example: math 10**10000 - left = @socket.bytes_per - type.length - where.to_s.length - 4 + left = 510 - type.length - where.to_s.length - 3 begin if(left >= message.length) sendq "#{type} #{where} :#{message}", chan, ring diff --git a/lib/rbot/ircsocket.rb b/lib/rbot/ircsocket.rb index ccc751f7..408287a8 100644 --- a/lib/rbot/ircsocket.rb +++ b/lib/rbot/ircsocket.rb @@ -168,6 +168,9 @@ module Irc # wrapped TCPSocket for communication with the server. # emulates a subset of TCPSocket functionality class IrcSocket + + MAX_IRC_SEND_PENALTY = 10 + # total number of lines sent to the irc server attr_reader :lines_sent @@ -183,10 +186,6 @@ module Irc # accumulator for the throttle attr_reader :throttle_bytes - # byterate components - attr_reader :bytes_per - attr_reader :seconds_per - # delay between lines sent attr_reader :sendq_delay @@ -197,7 +196,7 @@ module Irc # port:: IRCd port # host:: optional local host to bind to (ruby 1.7+ required) # create a new IrcSocket - def initialize(server, port, host, sendq_delay=2, sendq_burst=4, brt="400/2") + def initialize(server, port, host, sendq_delay=2, sendq_burst=4) @timer = Timer::Timer.new @timer.add(0.2) do spool @@ -222,23 +221,6 @@ module Irc else @sendq_burst = 4 end - @bytes_per = 400 - @seconds_per = 2 - @throttle_bytes = 0 - @hit_limit = 0 # how many times did we reach the limit? - setbyterate(brt) - end - - def setbyterate(brt) - if brt.match(/(\d+)\/(\d)/) - @bytes_per = $1.to_i - @seconds_per = $2.to_i - debug "Byterate now #{byterate}" - return true - else - debug "Couldn't set byterate #{brt}" - return false - end end def connected? @@ -288,38 +270,6 @@ module Irc end end - def byterate - return "#{@bytes_per}/#{@seconds_per} (limit hit #{@hit_limit} times)" - end - - def byterate=(newrate) - @qmutex.synchronize do - setbyterate(newrate) - end - end - - def run_throttle(more=0) - now = Time.new - # Each time we reach the limit, we reduce the bitrate. We reset the bitrate only if the throttle - # manages to reset twice. This way we have better flood control, although the really perfect way - # would be to calculate our penalty the way it's done serverside. - if @throttle_bytes > 0 - if @throttle_bytes >= @bytes_per - @hit_limit += 1 - @hit_limit = 3 if @hit_limit > 3 - end - delta = ((now - @last_throttle)*(0.5**@hit_limit.ceil)*@bytes_per/@seconds_per).floor - if delta > 0 - @throttle_bytes -= delta - @throttle_bytes = 0 if @throttle_bytes < 0 - @last_throttle = now - end - else - @hit_limit -= 0.5 if @hit_limit > 0 - end - @throttle_bytes += more - end - # used to send lines to the remote IRCd by skipping the queue # message: IRC message to send # it should only be used for stuff that *must not* be queued, @@ -385,19 +335,10 @@ module Irc return end debug "can send #{@sendq_burst - @burst} lines, there are #{@sendq.length} to send" - (@sendq_burst - @burst).times do - break if @sendq.empty? + while !@sendq.empty? and @burst < @sendq_burst and now > @last_send - MAX_IRC_SEND_PENALTY mess = @sendq.next - if @throttle_bytes == 0 or mess.length+@throttle_bytes < @bytes_per - debug "flood protection: sending message of length #{mess.length}" - debug "(byterate: #{byterate}, throttle bytes: #{@throttle_bytes})" - puts_critical(@sendq.shift) - else - debug "flood protection: throttling message of length #{mess.length}" - debug "(byterate: #{byterate}, throttle bytes: #{@throttle_bytes})" - run_throttle - break - end + puts_critical(@sendq.shift) + @last_send += mess.irc_send_penalty end if @sendq.empty? @timer.stop @@ -452,7 +393,6 @@ module Irc @last_send = Time.new @lines_sent += 1 @burst += 1 - run_throttle(message.length + 1) end rescue => e error "SEND failed: #{e.inspect}" diff --git a/lib/rbot/rfc2812.rb b/lib/rbot/rfc2812.rb index 26549539..9fa81cb5 100644 --- a/lib/rbot/rfc2812.rb +++ b/lib/rbot/rfc2812.rb @@ -1,3 +1,67 @@ +class ::String + # Calculate the penalty which will be assigned to this message + # by the IRCd + def irc_send_penalty + # According to eggrdop, the initial penalty is + penalty = 1 + self.length/100 + # on everything but UnderNET where it's + # penalty = 2 + self.length/120 + + cmd, pars = self.split($;,2) + debug "cmd: #{cmd}, pars: #{pars.inspect}" + case cmd.to_sym + when :KICK + chan, nick, msg = pars.split + chan = chan.split(',') + nick = nick.split(',') + penalty += nick.length + penalty *= chan.length + when :MODE + chan, modes, argument = pars.split + extra = 0 + if modes + extra = 1 + if argument + extra += modes.split(/\+|-/).length + else + extra += 3 * modes.split(/\+|-/).length + end + end + if argument + extra += 2 * argument.split.length + end + penalty += extra * chan.split.length + when :TOPIC + penalty += 1 + penalty += 2 unless pars.split.length < 2 + when :PRIVMSG, :NOTICE + dests = pars.split($;,2).first + penalty += dests.split(',').length + when :WHO + # I'm too lazy to implement this one correctly + penalty += 5 + when :AWAY, :JOIN, :VERSION, :TIME, :TRACE, :WHOIS, :DNS + penalty += 2 + when :INVITE, :NICK + penalty += 3 + when :ISON + penalty += 1 + else # Unknown messages + penalty += 1 + end + if penalty > 99 + debug "Wow, more than 99 secs of penalty!" + penalty = 99 + end + if penalty < 2 + debug "Wow, less than 2 secs of penalty!" + penalty = 2 + end + debug "penalty: #{penalty}" + return penalty + end +end + module Irc # RFC 2812 Internet Relay Chat: Client Protocol # -- 2.39.2