=$Limit) { if(!$Emergency) { StopAttack("mass join", true); if($UsersCount>$Limit) PrintLog("EXCEPTION: more users in channel ($UsersCount) than limit allows ($Limit)"); } } // We want to react on netsplits and such network issues by setting a failsafe -l if(abs($UsersCount-$Limit)>$SplitThreshold) { if(!$Emergency) SendMsg($WatchedChan, "!netsplit"); if(!$StoppingAttack) ReleaseLimit(); } // If we don't get timely PING replied from the monitored nicknames, we set a failsafe -l if($LastPong>$LastPing+$MaxRoundtrip Or (time()>$LastPing+$MaxRoundtrip And $LastPing>$LastPong)) { if(!$Emergency) { if($ChanServAlive) PrintLog("WARN: Excessive lag detected on other bots, removing limit"); else SendMsg($RelayChan, "WARNING: ChanServ is not replying, removing limit", true); } ReleaseLimit(); } // Commands are in the form :!@ [] : if($FromServer!="" And eregi(':([^ .!]+)([^ ]+) +([^ ]+) +(:(.*)|([^ ]+) +:?(.*))$', $FromServer, $Parts)) { $Nick=$Parts[1]; $Mask=$Parts[2]; $Command=$Parts[3]; $Target=$Parts[6]; $Body=$Parts[7]; $ShortBody=$Parts[5]; $Host=substr($Mask, strpos($Mask, "@")+1); if($Command!="QUIT" And $Command!="JOIN" And ($Command=="NICK" Or Match($Target, $WatchedChan))) { if(time()-$LastEventTime<1) { $FloodCount+=1; if($FloodCount==20) SendCmd("PING", ":".$IRCServer); } else { $FloodCount-=2; if($FloodCount<0 Or $FloodCount+2>20) $FloodCount=0; } $LastEventTime=time(); } if($Command=="PRIVMSG" Or $Command=="NOTICE") { if(Match($Target, $WatchedChan)) { HandleExploit($Nick, $Body); if($Command=="NOTICE" Or substr($Body, 0, 1)=="\1") { if(ereg("^\1ACTION", $Body)) $CTCPCount+=0.5; else { $CTCPCount=floor($CTCPCount+1); if($CTCPCount==1) SendMsg($RelayChan, "WARNING: $Nick CTCP/NOTICE'd $WatchedChan ($Body)", true); if($CTCPCount>=4) { if($CTCPCount==4) StopAttack("CTCP/NOTICE"); AddBan("*!*@".$Host); } } } } if($Command=="PRIVMSG") { if(Match($Target, $BotNickname) And in_array($Nick, $MonitoredNicks) And ereg("\1PING", $Body)) { // Reply to CTCP PING from other bot SendCmd("NOTICE", $Nick, ":".$Body); } else if(Match($Target, $RelayChan)) { if($Body=="status") { SendMsg($RelayChan, "$UsersCount users - limit $Limit" .(!$EnforcingLimit?" not enforced":"") .($Emergency?" - emergency mode":"") .($GraceRound?" - grace round":"") .($Attack?" - attack ongoing":"") ); } else if($Body=="sync") { WhoChannel(); SendMsg($RelayChan, "Synced to $WatchedChan, $UsersCount users present"); } else if($Body=="hook") { $Limit=$UsersCount+$Offset; SendMsg($RelayChan, "Limit will be enforced at the next non-emergency round, see 'status'"); } else if($Body=="clones") { asort($Who); $Clones=array(); $List=array(); $PrevHost=false; foreach($Who as $Nick => $Host) { if($Host==$PrevHost) { array_push($Clones, $Nick); } else { sort($Clones); if(count($Clones)>1) array_push($List, $PrevHost.": ".implode(", ", $Clones)); $Clones=array($Nick); } $PrevHost=$Host; } shuffle($List); foreach($List as $Message) SendMsg($RelayChan, $Message, true); // $List=array(); foreach($Who as $Host) { // $Clones=array_keys($Who, $Host); // if(count($Clones)>=2) array_push($List, $Host.": ".implode(", ", $Clones)); // } // $List=array_unique($List); sort($List); // while(count($List)>0) SendMsg($RelayChan, implode(" || ", array_splice($List, 0, 6)), true); } } } else if($Command=="NOTICE") { // Check CTCP PING replies from ChanServ and other watched bots if(Match($Nick, 'ChanServ')) $ChanServAlive=true; if(in_array($Nick, $MonitoredNicks) And eregi("^\1PING ".$LastPing, $Body)) { PrintLog("RECV: $Nick replied after ".(time()-$LastPing)." s"); if(!Match($Nick, 'ChanServ')) $PingReplies+=1; // If enough people replied to PING, we are fine if($PingReplies>=2 And $ChanServAlive) { $LastPong=time(); PrintLog("STAT: Everybody replied after ".($LastPong-$LastPing)." s"); } } else { PrintLog("RECV: $Body from $Nick. but PING seed was $LastPing"); } } } else if($Command=="JOIN") { if(Match($Body, $WatchedChan) Or Match($ShortBody, $WatchedChan) Or Match($Target, $WatchedChan)) { $Who[$Nick]=$Host; $UsersCount+=1; $Clones=array_keys($Who, $Host); foreach($Clones as $Key => $Clone) $Clones[$Key]=trim($Clones[$Key], '-_`|^0123456789'); $Clones=array_unique($Clones); if(count($Clones)>=4) { SendCmd("PING", $IRCServer, "", true); SendMsg($RelayChan, "WARNING: ".implode(", ", $Clones)." are all connected from ".$Host, true); } PrintLog("JOIN: $Nick ($UsersCount)"); } else PrintLog("UNEXPECTED: $Command from $Nick to $Target - $Body - $ShortBody ($FromServer)"); } else if($Command=="PART") { HandleExploit($Nick, $Body); if(Match($ShortBody, $WatchedChan) Or Match($Target, $WatchedChan)) { unset($Who[$Nick]); $UsersCount-=1; PrintLog("PART: $Nick ($UsersCount)"); } else ("UNEXPECTED: $Command from $Nick to $Target - $Body - $ShortBody ($FromServer)"); } else if($Command=="KICK") { if(Match($Target, $WatchedChan)) { $Victim=substr($Body, 0, strpos($Body, " ")); unset($Who[$Victim]); $UsersCount-=1; PrintLog("KICK: $Victim by $Nick ($UsersCount)"); } } else if($Command=="QUIT") { HandleExploit($Nick, $ShortBody); if(isset($Who[$Nick])) { unset($Who[$Nick]); $UsersCount-=1; PrintLog("QUIT: $Nick ($UsersCount)"); } else PrintLog("$Nick has left IRC, but was not in $WatchedChan"); if(in_array($Nick, $MonitoredNicks) And $ShortBody!='"Maintenance"') { PrintLog("WARN: $Nick has quit IRC, removing limit"); SendMsg("ChanServ", "OP $WatchedChan"); ReleaseLimit(); } if(eregi("reset by peer", $ShortBody) And time()-$ExploitTime<2) { AddBan($Nick."!*@*!#ubuntu-read-topic", "Your router has a bug - Please rejoin and read the channel topic"); } if(!ereg('"', $ShortBody) And strpos($ShortBody, $IRCServer)!==false) { if(!$Emergency) SendMsg($RelayChan, "WARNING: Network split", true); ReleaseLimit(); } } else if($Command=="NICK") { if(isset($Who[$Nick])) { unset($Who[$Nick]); if(!Match($ShortBody, "")) $Who[$ShortBody]=$Host; else if(!Match($Target, "")) $Who[$Target]=$Host; else SendMsg($RelayChan, "EXCEPTION: $Nick changed nickname to an unknown nickname"); PrintLog("NICK: from $Nick to $ShortBody ($UsersCount)"); } else PrintLog("UNEXPECTED: $Command from $Nick to $Target - $Body - $ShortBody ($FromServer)"); } else if($Command=="MODE") { if(Match($Target, $WatchedChan)) { if(ereg('^\+o ([^[:space:]]+)', $Body, $Parts)) { $ModeParameter=$Parts[1]; if(Match($Nick, 'ChanServ') And Match($ModeParameter, $BotNickname)) { // If we've just been opped, that probably means we're in an emergency and -l should be set. if($AttemptingRelease And !$Attack) ReleaseLimit(false); // Or that we're being attacked. if($StoppingAttack) SendCmd("MODE", $WatchedChannel, "+rR-z"); } else if(in_array($ModeParameter, $MonitoredNicks)) { // If another bot was opped, leave +l alone for a little longer, to avoid MODE spamming $LastLimitChangeTime+=rand($MaxRoundtrip, $MaxRoundtrip*2); } } else if(ereg("-o $BotNickname", $Body)) { $Attack=false; } else if($Body=="-l" Or $Body=="-J") { PrintLog("MODE: Emergency release enforced by $Nick"); $EnforcingLimit=false; $AttemptingRelease=false; } else if(ereg('\+[^ ]*J', $Body)) { $EnforcingLimit=true; // $Limit=intval(substr($Body, strrpos($Body, " "))); } else if(ereg('\+[^ ]*(r|R)', $Body) And !Match($Nick, $BotNickname)) { PrintLog("MODE: $Nick set $Body, any attack is now considered handled"); $AttackTime=time(); $StoppingAttack=false; } else if(ereg('\-[^ ]*(r|R)', $Body) And !Match($Nick, $BotNickname)) { PrintLog("MODE: $Nick set $Body, syncing the same mode on my server"); SendCmd("MODE", $WatchedChan, $Body, true); if(in_array($Nick, $MonitoredNicks)) SendCmd("MODE", $WatchedChan, "-o $Nick"); } } else if(Match($Target, $OverflowChan)) { if($Body=="+i") { ReleaseLimit(false); SendCmd("MODE", $OverflowChan, "+if $WatchedChan", true); SendCmd("NAMES", $OverflowChan, "", true); $AttackTime=0; } } } else if($Command=="PONG") { if($FloodCount>=20 And time()-$LastEventTime<3 And $SentBytes<$ExcessFloodQuota) StopAttack("flood"); else $SentBytes=floor($SentBytes/4); $FloodCount=0; } else if($Command=="324") { // This is a MODE reply if(ereg(' \+[^ ]*l', $Body)) { if(!$EnforcingLimit) SendCmd("MODE", $OverflowChan, "-i", true); $EnforcingLimit=true; } else { $EnforcingLimit=false; } } else if($Command=="353" And !$Attack) { // This is a NAMES reply for the overflow channel $Body=substr($Body, strpos($Body, ":")+1); $Names=explode(" ", $Body); // Load balance kicking among the bots (although all the kicks are still sent to the server) shuffle($Names); foreach($Names as $Name) { if(substr($Name, 0, 1)!="@") SendCmd("KICK", $OverflowChan, "$Name :Please join $WatchedChan", true); } } else if($Command=="366") { // End of NAMES list SendCmd("MODE", $OverflowChan, "-i", true); } else if($Command=="NOTICE") { // Check CTCP PING replies from ChanServ and other watched bots if(Match($Nick, 'ChanServ')) $ChanServAlive=true; if(in_array($Nick, $MonitoredNicks) And eregi("^\1PING ".$LastPing, $Body)) { PrintLog("RECV: $Nick replied after ".(time()-$LastPing)." s"); if(!Match($Nick, 'ChanServ')) $PingReplies+=1; // If enough people replied to PING, we are fine if($PingReplies>=2 And $ChanServAlive) { $LastPong=time(); PrintLog("STAT: Everybody replied after ".($LastPong-$LastPing)." s"); } } else { PrintLog("Received $Body from $Nick but seed was $LastPing"); } } else if($Command=="319") { // This is a WHOIS reply $Bot=substr($Body, 0, strpos($Body, ' ')); if(!Match('ChanServ', $Bot) And !Match($BotNickname, $Bot)) { if(ereg('@'.$WatchedChan.'([[:space:]]|$)', $Body)) SendCmd("MODE", $WatchedChan, "-o $Bot", true); } } else if($Command!="311" And $Command!="312" And $Command!="318" And $Command!="320" And $Command!="482") { PrintLog("UNEXPECTED: $Command from $Nick to $Target - $Body - $ShortBody ($FromServer)"); } } // Execute the periodic actions if(time()-$LastLimitChangeTime>$CheckingInterval) { // Send pings to ChanServ and other bots to make sure they're all alive and well $ChanServAlive=false; $PingReplies=0; $LastPing=time(); foreach($MonitoredNicks as $Nick) SendMsg($Nick, "\1PING ".$LastPing."\1", false); // WHOIS other bots to see if they're opped, but only do it half of the times, no need to flood foreach($MonitoredNicks as $Bot) if($Bot!='ChanServ' And rand(0,1)==1) SendCmd("WHOIS", $Bot, "", true); // Do some emergency cleanup operations if(time()-$AttackTime<$CheckingInterval*15) if(rand(0, $FleetSize/2)==0) SendMsg($OverflowChan, $OverflowMessage, true); if($StoppingAttack) SendCmd("MODE", $WatchedChan, "-rRmi", true); FlushBans(); $CTCPCount=0; $FloodCount=0; $Attack=$StoppingAttack=false; $SentBytes=floor($SentBytes/4); // Change the limit if necessary ChangeLimit(); PrintLog("STAT: $UsersCount users, limit $Limit, diff ".($Limit-$UsersCount)); // Remember when we last did all this $LastLimitChangeTime=time(); } } function ReleaseLimit($Force=true) { global $WatchedChan; global $Emergency, $EnforcingLimit, $GraceRound, $AttemptingRelease; if($EnforcingLimit) { if(!$Force Or !$GraceRound) SendCmd("MODE", $WatchedChan, "-J"); if($Force And !$GraceRound) SendMsg("ChanServ", "OP $WatchedChan"); $AttemptingRelease=true; } $Emergency=true; $GraceRound=true; } function ChangeLimit() { global $UsersCount; global $Limit, $Threshold, $Offset, $GrowTolerance, $ShrinkTolerance, $ShrinkCap; global $WatchedChan, $OverflowChan; global $Emergency, $EnforcingLimit, $GraceRound; $NewLimit=$UsersCount+$Offset; // Never go below a defined threshold if($NewLimit<$Threshold) $NewLimit=$Threshold; // Only act if the limit should be changed by more than just one unit (or two on decreasing) if(!$GraceRound) { if(rand(0, 15)==0 Or $NewLimit>$Limit+$GrowTolerance Or $NewLimit<$Limit-$ShrinkTolerance Or !$EnforcingLimit) { if(!$EnforcingLimit) { SendCmd("MODE", $OverflowChan, "-i", true); SendCmd("MODE", $WatchedChan, "+J 2,5", true); } // If many users have parted, decrease the limit cautiously, as it might be a netsplit if($NewLimit<$Limit-$ShrinkCap) $NewLimit=$Limit-$ShrinkCap; if($NewLimit>$Limit+$Threshold) $NewLimit=$Limit+$Threshold; $Limit=$NewLimit; // SendCmd("MODE", $WatchedChan, "+l $Limit"); } $Emergency=false; $EnforcingLimit=true; } else { $Limit=max($Limit-round($ShrinkCap/3), $UsersCount+($Offset+$ShrinkTolerance*2)-1, $Threshold); PrintLog("STAT: Limit ($Limit) is currently not enforced"); $GraceRound=false; } } function SelectNickname($BotPrefix, $PreferredNickname=false) { global $IRCd; // Find a nickname to use $BotNumber=1; if($PreferredNickname) $BotNickname=$PreferredNickname; else $BotNickname=$BotPrefix.$BotNumber; SendCmd("NICK", $BotNickname); for($i=0; $i<6; $i++) { if(ereg(":[^[:space:]]+ 433", fgets($IRCd))) { $BotNumber+=1; $i=0; $BotNickname=$BotPrefix.$BotNumber; PrintLog("INIT: Nickname is already in use, trying ".$BotNickname); SendCmd("NICK", $BotNickname); } } return $BotNickname; } function SendMsg($Target, $Body, $Slow=false, $Log=true) { SendCmd('PRIVMSG', $Target, ":".$Body, $Slow, $Log); } function SendCmd($Command="", $Target="", $Body="", $Slow=false, $Log=true) { global $IRCd, $IRCServer, $BotNickname; global $SentBytes, $ExcessFloodQuota, $ThrottlingQuota; static $SlowQueue=array(); static $FastQueue=array(); static $LastSendTime=0; static $StackedSends=-30; if($Command=="USER") $StackedSends=-30; $CurrentTime=time(); if($CurrentTime-$LastSendTime>3) $StackedSends=0; if($CurrentTime-$LastSendTime>1) $StackedSends-=1; if($StackedSends<0) $StackedSends+=1; if(trim($Command)=="") $Command=":"; if(substr($Command, 0, 1)==":") { // If someone else already did something, we don't need to replicate it $SlowCount=count($SlowQueue); $FastCount=count($FastQueue); $SlowQueue=array_diff($SlowQueue, array(substr($Command, strpos($Command, " ")+1))); $FastQueue=array_diff($FastQueue, array(substr($Command, strpos($Command, " ")+1))); if(count($SlowQueue)!=$SlowCount) PrintLog("FIFO: Remove duplicate (slow) $Command"); if(count($FastQueue)!=$FastCount) PrintLog("FIFO: Remove duplicate (fast) $Command"); if($CurrentTime-$LastSendTime>=1+rand(0, 1)) { // If there is a message in the prioritized queue, pick it $CurrentMsg=array_shift($FastQueue); // Otherwise, pick a message from the slow queue if($CurrentMsg==NULL) $CurrentMsg=array_shift($SlowQueue); // If there's nothing at all to send, return if($CurrentMsg==NULL) return; PrintLog("FIFO: Shift $CurrentMsg"); } else return; } else { $CurrentMsg=substr("$Command $Target ".$Body, 0, 450); } if($Slow Or $StackedSends>=$ThrottlingQuota Or $SentBytes>=$ExcessFloodQuota) { PrintLog("FIFO: Push with".count($SlowQueue)." slow, ".count($FastQueue)." fast ".($Slow?"(Slow) ":"(Fast) ").$CurrentMsg); // Even when throttling everything to avoid Excess Flood, priorities are considered if($Slow) { array_push($SlowQueue, $CurrentMsg); } else { array_push($FastQueue, $CurrentMsg); } } else { // If we're flooding too much, wait for a PING reply before going on if($CurrentTime-$LastSendTime<1) $StackedSends+=1; $SentBytes+=strlen($CurrentMsg); $LastSendTime=$CurrentTime; if($SentBytes>=$ExcessFloodQuota) { if(false===fwrite($IRCd, "PING :$IRCServer\n")) Initialize($BotNickname); PrintLog("WARN: Flood quota reached, sending server PING"); } else { if(false===fwrite($IRCd, $CurrentMsg."\n")) Initialize($BotNickname); if($Log) PrintLog("SENT: ".$CurrentMsg); } } } function WhoChannel() { global $IRCd, $Who, $UsersCount; global $WatchedChan, $RelayChan; $UsersCount=0; SendCmd("WHO", $WatchedChan); while($Response=fgets($IRCd)) { if(ereg('^:[^[:space:]]+ +352', $Response)) { $Parts=explode(" ", $Response); if(Match($Parts[3], $WatchedChan)) { $Who[$Parts[7]]=$Parts[5]; $UsersCount+=1; } } else { if($UsersCount==0) continue; if(!ereg('^:[^[:space:]]+ 315', $Response)) { PrintLog("WHO without end of /WHO"); SendMsg($RelayChan, "EXCEPTION: WHO without end of /WHO"); Initialize(); } break; } } } function AddBan($BanMask, $Message=false) { global $BanQueue; static $BanMemory=array(); if(count($BanMemory)>50) $BanMemory=array(); if(!in_array($BanMask, $BanMemory)) { array_push($BanQueue, $BanMask); array_push($BanMemory, $BanMask); PrintLog("Banning: $BanMask ($Message)"); } if(count($BanQueue)>=4) FlushBans($Message); } function FlushBans($KickMessage=false) { global $Who; global $BanQueue; global $WatchedChan; static $Message=false; static $LastFlushTime=0; if(time()-$LastFlushTime<1 And count($BanQueue)<4) return; if($KickMessage) $Message=$KickMessage; $Bans=array_slice($BanQueue, 0, min(4, count($BanQueue))); if(count($Bans)>0) SendCmd("MODE", $WatchedChan, '+'.str_repeat("b", count($Bans))." ".implode(" ", $Bans)); foreach($Bans as $Mask) { array_pop($BanQueue); $Nick=substr($Mask, 0, strpos($Mask, "!")); if($Message And isset($Who[$Nick])) SendCmd("KICK", $WatchedChan, $Nick." :".$Message, true); } if(!$KickMessage) $Message=false; $LastFlushTime=time(); } function StopAttack($AttackDescription="attack", $Actively=true) { global $WatchedChan, $RelayChan, $OverflowChan; global $LastLimitChangeTime, $CheckingInterval, $MaxRoundtrip, $AttackTime; global $Emergency, $Attack, $StoppingAttack; if($Actively And !$StoppingAttack) SendCmd("MODE", $WatchedChan, "+rR-z"); if(!$Attack Or !$StoppingAttack) { SendMsg($RelayChan, "!ops | $AttackDescription", true); SendCmd("MODE", $OverflowChan, "-i", true); $MaxMultiplier=($CheckingInterval/(abs(time()-$AttackTime)+1))*4; $LastLimitChangeTime=time()-$CheckingInterval+$MaxRoundtrip*rand(2, 3+$MaxMultiplier); if(abs($LastLimitChangeTime-time())>120) { SendMsg($RelayChan, "EXCEPTION: Next round is more than 2 minutes from now", true); $LastLimitChangeTime=time()+120; } SendMsg('ChanServ', "OP $WatchedChan"); $AttackTime=time(); } if($Actively) { if($StoppingAttack And time()-$AttackTime>5) { SendCmd("MODE", $WatchedChan, "+m"); $LastLimitChangeTime=time()-$CheckingInterval+int($MaxRoundtrip/2); } $StoppingAttack=true; } FlushBans(); $Emergency=true; $Attack=true; PrintLog("RECV: $AttackDescription attack".($Actively?", stopping actively":"")); } function HandleExploit($Nick, $Body) { global $ExploitTime; if(ereg("DCC (SEND)|(CHAT) [^[:space:]]{14}", $Body)) { PrintLog("RECV: DCC exploit attempted by $Nick"); StopAttack("exploit", false); $ExploitTime=time(); } } function Terminate($Signal) { global $IRCd; global $WatchedChan; // fwrite($IRCd, "MODE $WatchedChan -l\n"); fwrite($IRCd, "QUIT :Maintenance\n"); fflush($IRCd); sleep(4); $Sockets=array($IRCd); stream_select($Sockets, $Write=NULL, $Except=NULL, 10); sleep(4); fclose($IRCd); exit(0); } function PrintLog($Text) { print date("[d/m/y H:i:s] ", time()).$Text."\n"; } function Match($String1, $String2) { if(strtolower(trim($String1))==strtolower(trim($String2))) return true; else return false; } ?>