diff options
Diffstat (limited to 'misc/freeswitch/scripts/common')
-rw-r--r-- | misc/freeswitch/scripts/common/database.lua | 40 | ||||
-rw-r--r-- | misc/freeswitch/scripts/common/fapi.lua | 5 | ||||
-rw-r--r-- | misc/freeswitch/scripts/common/intruder.lua | 51 | ||||
-rw-r--r-- | misc/freeswitch/scripts/common/perimeter.lua | 241 | ||||
-rw-r--r-- | misc/freeswitch/scripts/common/sip_account.lua | 8 | ||||
-rw-r--r-- | misc/freeswitch/scripts/common/str.lua | 17 |
6 files changed, 359 insertions, 3 deletions
diff --git a/misc/freeswitch/scripts/common/database.lua b/misc/freeswitch/scripts/common/database.lua index 1f39135..345f69d 100644 --- a/misc/freeswitch/scripts/common/database.lua +++ b/misc/freeswitch/scripts/common/database.lua @@ -16,6 +16,7 @@ function Database.new(self, arg) self.class = 'database'; self.log = arg.log; self.conn = nil; + self.ignore_on_update = arg.ignore_on_update or {}; return object; end @@ -71,6 +72,45 @@ function Database.last_insert_id(self) end +function Database.insert_or_update(self, db_table, record, use_on_update) + ignore_on_update = ignore_on_update or self.ignore_on_update; + local record_sql_create = {}; + local record_sql_update = {}; + + for key, value in pairs(record) do + if ignore_on_update[key] ~= false then + table.insert(record_sql_update, self:key_value(key, value)); + end + table.insert(record_sql_create, self:key_value(key, value)); + end + + local sql_query = 'INSERT INTO `' .. db_table .. '` SET ' .. table.concat(record_sql_create, ', ') .. ' ON DUPLICATE KEY UPDATE ' .. table.concat(record_sql_update, ', '); + + return self:query(sql_query); +end + + +function Database.key_value(self, key, value) + return self:escape(key, '`') .. ' = ' .. self:escape(value, '"'); +end + + +function Database.escape(self, value, str_quotes) + str_quotes = str_quotes or ''; + if type(value) == 'boolean' then + return tostring(value):upper(); + elseif type(value) == 'number' then + return tostring(value); + elseif type(value) == 'string' then + return str_quotes .. value:gsub('"', '\\"'):gsub("'", "\\'") .. str_quotes; + elseif type(value) == 'table' and value.raw then + return tostring(value[1]); + else + return 'NULL'; + end +end + + function Database.release(self) if self.conn then self.conn:release(); diff --git a/misc/freeswitch/scripts/common/fapi.lua b/misc/freeswitch/scripts/common/fapi.lua index 5b96633..b749a69 100644 --- a/misc/freeswitch/scripts/common/fapi.lua +++ b/misc/freeswitch/scripts/common/fapi.lua @@ -32,8 +32,10 @@ function FApi.return_result(self, result, positive, negative, unspecified) return negative; elseif result:match('^+OK') then return positive; - else + elseif type(unspecified) ~= 'nil' then return unspecified; + else + return result; end end @@ -75,6 +77,7 @@ function FApi.create_uuid(self, uuid) end function FApi.execute(self, function_name, function_parameters) + function_parameters = function_parameters or ''; local result = self.fs_api:execute(function_name, function_parameters); return self:return_result(result, true); end diff --git a/misc/freeswitch/scripts/common/intruder.lua b/misc/freeswitch/scripts/common/intruder.lua new file mode 100644 index 0000000..083ec37 --- /dev/null +++ b/misc/freeswitch/scripts/common/intruder.lua @@ -0,0 +1,51 @@ +-- Gemeinschaft 5 module: intruder class +-- (c) AMOOMA GmbH 2013 +-- + +module(...,package.seeall) + + +Intruder = {} + + +function Intruder.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.log = arg.log; + self.class = 'intruder' + self.database = arg.database; + + return object; +end + + +function Intruder.update_blacklist(self, event) + local intruder_record = { + list_type = 'blacklist', + key = event.key, + points = event.points, + bans = event.record.banned, + contact_ip = event.received_ip, + contact_port = event.received_port, + contact_count = event.record.contact_count + 1, + contact_last = { 'FROM_UNIXTIME(' .. tostring(math.floor(event.timestamp/1000000)) .. ')', raw = true }, + contacts_per_second = event.contacts_per_second, + contacts_per_second_max = event.contacts_per_second_max, + user_agent = event.user_agent, + to_user = event.to_user, + comment = 'Permimeter', + created_at = {'NOW()', raw = true }, + updated_at = {'NOW()', raw = true }, + }; + + if tonumber(event.ban_time) then + intruder_record.ban_last = { 'FROM_UNIXTIME(' .. event.ban_time .. ')', raw = true }; + end + if tonumber(event.ban_end) then + intruder_record.ban_end = { 'FROM_UNIXTIME(' .. event.ban_end .. ')', raw = true }; + end + + self.database:insert_or_update('intruders', intruder_record, { created_at = false, comment = false }); +end diff --git a/misc/freeswitch/scripts/common/perimeter.lua b/misc/freeswitch/scripts/common/perimeter.lua new file mode 100644 index 0000000..0815d33 --- /dev/null +++ b/misc/freeswitch/scripts/common/perimeter.lua @@ -0,0 +1,241 @@ +-- Gemeinschaft 5 module: perimeter class +-- (c) AMOOMA GmbH 2013 +-- + +module(...,package.seeall) + + +Perimeter = {} + + +function Perimeter.new(self, arg) + arg = arg or {} + object = arg.object or {} + setmetatable(object, self); + self.__index = self; + self.log = arg.log; + self.class = 'perimeter' + self.database = arg.database; + self.domain = arg.domain; + self.sources = {}; + + self.checks_available = { + check_frequency = self.check_frequency, + check_username_scan = self.check_username_scan, + check_bad_headers = self.check_bad_headers, + }; + + return object; +end + + +function Perimeter.setup(self, event) + require 'common.configuration_table'; + local config = common.configuration_table.get(self.database, 'perimeter'); + + self.contact_count_threshold = 10; + self.contact_span_threshold = 2; + self.name_changes_threshold = 2; + self.blacklist_file = '/var/opt/gemeinschaft/firewall/blacklist'; + self.blacklist_file_comment = '# PERIMETER_BAN - points: {points}, generated: {date}'; + self.blacklist_file_entry = '{received_ip} udp 5060'; + self.ban_command = 'sudo /sbin/service shorewall refresh'; + self.ban_threshold = 20; + self.ban_tries = 1; + self.checks = { register = {}, call = {} }; + self.bad_headers = { register = {}, call = {} }; + + if config and config.general then + for key, value in pairs(config.general) do + self[key] = value; + end + end + + self.checks.register = config.checks_register or {}; + self.checks.call = config.checks_call or {}; + self.bad_headers.register = config.bad_headers_register; + self.bad_headers.call = config.bad_headers_call; + + self.log:info('[perimeter] PERIMETER - setup perimeter defense'); +end + + +function Perimeter.record_load(self, event) + if not self.sources[event.key] then + self.sources[event.key] = { + contact_first = event.timestamp, + contact_last = event.timestamp, + contact_count = 0, + span_contact_count = 0, + span_start = event.timestamp, + points = 0, + banned = 0, + }; + end + + return self.sources[event.key]; +end + + +function Perimeter.format_date(self, value) + local epoch = tonumber(tostring(value/1000000):match('^(%d-)%.')); + return os.date('%Y-%m-%d %X', tonumber(epoch)) .. '.' .. (value-(epoch*1000000)); +end + + +function Perimeter.record_update(self, event) + event.record.contact_last = event.timestamp; + event.record.contact_count = event.record.contact_count + 1; + event.record.points = event.points or event.record.points; + event.record.span_start = event.span_start or event.record.span_start; + event.record.span_contact_count = (event.span_contact_count or event.record.span_contact_count) + 1; + event.record.users = event.users or event.record.users; +end + + +function Perimeter.check(self, event) + if not event or not event.key then + self.log:warning('[perimeter] PERIMETER_CHECK - no event/key'); + return; + end + + event.record = self:record_load(event); + if event.record.banned <= self.ban_tries then + for check_name, check_points in pairs(self.checks[event.action]) do + if self.checks_available[check_name] then + local result = self.checks_available[check_name](self, event); + if tonumber(result) then + event.points = (event.points or event.record.points) + result * check_points; + end + end + end + end + + if tonumber(event.points) and event.points < 0 then + event.points = 0; + end + + if event.points then + self.log:info('[', event.key, '/', event.sequence, '] PERIMETER suspicion rising - points: ', event.points,', ', event.action, '=', event.class, ', from: ', event.from_user, '@', event.from_host, ', to: ', event.to_user, '@', event.to_host, ', user_agent: ', event.user_agent); + end + + if (event.points or event.record.points) > self.ban_threshold and event.record.banned <= self.ban_tries then + if event.record.banned > 0 and event.record.banned == self.ban_tries then + self.log:warning('[', event.key, '/', event.sequence, '] PERIMETER_BAN_FUTILE - points: ', event.points,', event: ', event.class, ', from: ', event.from_user, '@', event.from_host, ', to: ', event.to_user, '@', event.to_host); + else + self.log:notice('[', event.key, '/', event.sequence, '] PERIMETER_BAN - threshold reached: ', event.points,', event: ', event.class, ', from: ', event.from_user, '@', event.from_host, ', to: ', event.to_user, '@', event.to_host); + if event.record.banned == 0 then + self:append_blacklist_file(event); + end + self:execute_ban(event); + event.ban_time = os.time(); + end + + event.record.banned = event.record.banned + 1; + event.span_start = event.timestamp; + event.span_contact_count = 0; + event.points = 0; + end + + if event.points then + self:update_intruder(event); + end + + self:record_update(event); +end + + +function Perimeter.check_frequency(self, event) + if event.record.span_contact_count >= self.contact_count_threshold then + self.log:debug('[', event.key, '/', event.sequence, '] PERIMETER_FREQUENCY_CHECK - contacts: ', event.record.span_contact_count, ' in < ', (event.timestamp - event.record.span_start)/1000000, ' sec, threshold: ', self.contact_count_threshold, ' in ', self.contact_span_threshold, ' sec'); + event.span_contact_count = 0; + event.span_start = event.timestamp; + event.contacts_per_second = event.record.span_contact_count / ((event.timestamp - event.record.span_start)/1000000) + return 1; + elseif (event.timestamp - event.record.span_start) > (self.contact_span_threshold * 1000000) then + event.span_contact_count = 0; + event.span_start = event.timestamp; + end +end + + +function Perimeter.check_username_scan(self, event) + if not event.to_user then + return; + end + + if not event.record.users or tostring(event.auth_result) == 'SUCCESS' or tostring(event.auth_result) == 'RENEWED' then + event.users = { event.to_user }; + return; + end + + if #event.record.users >= self.name_changes_threshold then + self.log:debug('[', event.key, '/', event.sequence, '] PERIMETER_USER_SCAN - user names: ', #event.record.users, ', threshold: ', self.name_changes_threshold); + event.users = {}; + return 1; + else + for index=1, #event.record.users do + if event.record.users[index] == tostring(event.to_user) then + return + end + end + + if not event.users then + event.users = event.record.users or {}; + end + table.insert(event.users, tostring(event.to_user)); + end +end + + +function Perimeter.check_bad_headers(self, event) + local points = nil; + for name, pattern in pairs(self.bad_headers[event.action]) do + pattern = self:expand_variables(pattern, event); + local success, result = pcall(string.find, event[name], pattern); + if success and result then + self.log:debug('[', event.key, '/', event.sequence, '] PERIMETER_BAD_HEADERS - ', name, '=', event[name], ' ~= ', pattern); + points = (points or 0) + 1; + end + end + + return points; +end + + +function Perimeter.append_blacklist_file(self, event) + local blacklist = io.open(self.blacklist_file, 'a'); + if not blacklist then + self.log:error('[', event.key, '/', event.sequence, '] PERIMETER_APPEND_BLACKLIST - could not open file: ', self.blacklist_file); + return false; + end + + event.date = self:format_date(event.timestamp); + + if self.blacklist_file_comment then + blacklist:write(self:expand_variables(self.blacklist_file_comment, event), '\n'); + end + + self.log:debug('[', event.key, '/', event.sequence, '] PERIMETER_APPEND_BLACKLIST - file: ', self.blacklist_file); + blacklist:write(self:expand_variables(self.blacklist_file_entry, event), '\n'); + blacklist:close(); +end + + +function Perimeter.execute_ban(self, event) + local command = self:expand_variables(self.ban_command, event); + self.log:debug('[', event.key, '/', event.sequence, '] PERIMETER_EXECUTE_BAN - command: ', command); + local result = os.execute(command); +end + +function Perimeter.update_intruder(self, event) + require 'common.intruder'; + local result = common.intruder.Intruder:new{ log = self.log, database = self.database }:update_blacklist(event); +end + + +function Perimeter.expand_variables(self, line, variables) + return (line:gsub('{([%a%d%._]+)}', function(captured) + return variables[captured] or ''; + end)) +end diff --git a/misc/freeswitch/scripts/common/sip_account.lua b/misc/freeswitch/scripts/common/sip_account.lua index 8dd432b..d023f20 100644 --- a/misc/freeswitch/scripts/common/sip_account.lua +++ b/misc/freeswitch/scripts/common/sip_account.lua @@ -38,8 +38,12 @@ function SipAccount.find_by_sql(self, where) `a`.`sip_accountable_id`, \ `a`.`hotdeskable`, \ `a`.`gs_node_id`, \ - `b`.`host` \ - FROM `sip_accounts` `a` JOIN `sip_domains` `b` ON `a`.`sip_domain_id` = `b`.`id` \ + `b`.`host`, \ + `c`.`sip_host`, \ + `c`.`profile_name` \ + FROM `sip_accounts` `a` \ + JOIN `sip_domains` `b` ON `a`.`sip_domain_id` = `b`.`id` \ + LEFT JOIN `sip_registrations` `c` ON `a`.`auth_name` = `c`.`sip_user` \ WHERE ' .. where .. ' LIMIT 1'; local sip_account = nil; diff --git a/misc/freeswitch/scripts/common/str.lua b/misc/freeswitch/scripts/common/str.lua index 793c191..72ff388 100644 --- a/misc/freeswitch/scripts/common/str.lua +++ b/misc/freeswitch/scripts/common/str.lua @@ -18,6 +18,23 @@ function try(array, arguments) return result; end + +function set(array, arguments, value) + local nop, arguments_count = arguments:gsub('%.', ''); + local structure = array; + arguments:gsub('([^%.]+)', function(entry) + if arguments_count <= 0 then + structure[entry] = value; + elseif type(structure[entry]) == 'table' then + structure = structure[entry]; + else + structure[entry] = {}; + structure = structure[entry]; + end + arguments_count = arguments_count - 1; + end); +end + -- to number function to_n(value) value = tostring(value):gsub('[^%d%.%+%-]', ''); |