Курсы по использованию Asterisk

IP-телефония — технология будущего. Обучитесь работе с IP-АТС Asterisk для того чтобы внедрить и профессионально использовать при решении коммуникационных задач.

Работайте с Asterisk профессионально!

Многоуровневая защита IP-АТС Asterisk

Телефонные станции очень часто становятся объектами хакерских атак. Узнайте, каким образом необходимо строить многоуровневую защиту для Вашей IP-АТС.

Не оставьте хакерам шансов. Защитите свой Asterisk от атак.

Используйте Веб-Интерфейс для удобства настройки

Панель управление FreePBX позволяет легко и удобно управлять всей системой. Научитесь эффективно использовать FreePBX для решения своих задач.

Управление станцией и статистика в окне браузера.

Научитесь работать с Asterisk из консоли

Для понимания работы с Asterisk необходимо уметь настраивать его вручную с конфигурационными файлами и командной строкой CLI Asterisk.

Научитесь «тонкой» настройке Asterisk

Цель курсов - максимум практики.

Обучение нацелено на практическую работу с IP-оборудованием: платы потоков E1, VoIP-телефонные аппараты, голосовые шлюзы FXS и прочее.

Обучение на реальном оборудовании — залог успеха.

Проверка доступности городских номеров средствами Asterisk

База знаний Настройка Asterisk
Для владельца любой компании важно быть уверенным в том, что телефонные номера компании всегда доступны для звонков клиентов. К сожалению, полностью исключить отказ телефонии невозможно, но можно быстро узнать о проблеме и начать предпринимать меры по её устранению. В этой статье будет описан способ реализации периодической проверки городских номеров средствами Asterisk.
Для реализации данной схемы понадобится, как минимум, один дополнительный провайдер телефонии в добавок к тому провайдеру, доступность номеров которого мы будем проверять. В качестве примера предположим, что в Asterisk заведено два SIP-транка:
in_prov - провайдер используемый для входящей связи, доступность его номеров для входящих вызовов мы и будем проверять. Провайдер предоставляет номера 71111111111, 72222222222 и 73333333333.
out_prov - провайдер используемый для исходящей связи, он будет использоваться для совершения проверочных вызовов. Провайдер предоставляет номер 74444444444.
Проверочные вызовы будут идти с номера 74444444444 на номера 71111111111, 72222222222 и 73333333333
Скрипт для проверки доступности городских номеров /opt/trunk_checker/trunk_checker.pl будет запускаться раз в две минуты с помощью crond. После запуска скрипт поочерёдно совершает вызов на каждый из проверяемых номеров. После получения проверочного вызова, Asterisk записывает в AstDB информацию о получении вызова и отбивает вызов (т.к. Asterisk не принимает вызов, расходы на исходящую и, в случае номеров 8800, входящую связь будут минимизированы). Список неответивших номеров должен быть отправлен на адреса admin@example.com и boss@example.com.

Диалплан
Нам понадобится контекст для обработки проверочных вызовов, код контекста:

[trunk-checker-dst]
exten => _X.,1,ResetCDR
same => n,NoCDR
same => n,Set(DB(trunk_checker/${EXTEN})=${EPOCH})
same => n(end),Busy(2)

Теперь добавим в контекст, куда приходят входящие вызовы, переход в контекст trunk-checker-dst, если вызов пришёл с номера 74444444444. В качестве примера будем считать, что входящие вызовы приходят в контекст from-trunk, но мы не будем вносить в него изменения, вместо этого создадим контекст from-trunk-pre, код контекста:

[from-trunk-pre]
exten => _X.,1,Set(DIALED_NUM=${EXTEN})
same => n,Goto(cont,1)
exten => _+X.,1,Set(DIALED_NUM=${EXTEN})
same => n,Goto(cont,1)
exten => cont,1,Gosub(sub-fix-cid,s,1)
same => n,GotoIf($["${CALLERID(num)}" = "84444444444"]?trunk-checker-dst,${DIALED_NUM},1)
same => n,Goto(from-trunk,${DIALED_NUM},1)
[sub-fix-cid]
exten => s,1,GotoIf($[${REGEX("^[0-9]{7}$" ${CALLERID(num)})} = 0]?cont1) ;"
same => n,Set(CALLERID(num)=8495${CALLERID(num)})
same => n,Set(CALLERID(ANI-num)=${CALLERID(num)})
same => n,Goto(end)
same => n(cont1),GotoIf(${REGEX("^[78]?[2-9][0-9]{9}$" ${CALLERID(num)})}?fix_cid)
same => n,GotoIf(${REGEX("^\\+?7[2-9][0-9]{9}$" ${CALLERID(num)})}?fix_cid)
same => n,Goto(end)
same => n(fix_cid),Set(CALLERID(num)=8${CALLERID(num):-10})
same => n,Set(CALLERID(ANI-num)=${CALLERID(num)})
same => n(end),Return

Контекст from-trunk-pre также вызывает контекст sub-fix-cid для приведения номера звонящего к общему виду (11 цифр с восьмёрки), что является хорошей идеей на случай, если нужно будет проверять доступность номеров, предоставляемых разными провайдерами (т.к. разные провайдеры могут передавать номер звонящего в разном виде). Далее все вызовы не с номера 74444444444 будут попадать в контекст from-trunk.
Теперь нужно завернуть входящие вызовы через проверяемый транк в контекст from-trunk-pre (указать для транка in_prov параметр context=from-trunk-pre). Обязательно убедитесь, что входящие вызовы через транк in_prov работают нормально перед продолжением настройки.
Контекст для совершения проверочных вызовов:

[trunk-checker-dial]
exten => _X.,1,GotoIf($["${CHECK_THRU_TRUNK}" = ""]?end)
same => n,Dial(SIP/${CHECK_THRU_TRUNK}/${EXTEN},60)
same => n(end),Hangup

Нужен ещё один контекст для приёма второго плеча, создаваемого AMI-действием Originate. Всё, что будет делать этот контекст - принимать вызов ожидать две секунды и разрывать вызов, код контекста:

[trunk-checker-src]
exten => s,1,ResetCDR
same => n,NoCDR
same => n,Answer
same => n,Wait(2)
same => n,Hangup

На самом деле, при текущем алгоритме проверки доступности номеров, контекст trunk-checker-src не должен вызываться никогда, т.к. Asterisk будет всегда отбивать проверочный вызов, но контекст лучше всё равно создать на тот случай, если Asterisk всё-таки ответит на проверочный вызов, например, из-за ошибки в настройках.

Скрипт проверки городских номеров
Код скрипта /opt/trunk_checker/trunk_checker.pl приведён ниже:

#!/usr/bin/perl
use warnings;
use strict;
use IO::Socket;
#use Data::Dumper;
use utf8;
use constant AMI_CONNECT_TIMEOUT => 3;
use constant AMI_READ_TIMEOUT => 30;
use constant AMI_RES_VALUE => 0;
use constant AMI_RES_ACTIONID => 1;
use constant AMI_RES_DATA => 2;
# PID
use constant PID_DIR => '/var/run/trunk_checker';
use constant PID_FILE => 'trunk_checker';
# Get script name and path
my $cwd = '';
my $script_name = '';
if($0 =~ /^(.*)\/([^\/]+)$/) { $cwd = $1; $script_name = $2 } else { print "Couldn't determine cwd (WTF?)"; exit 1 }
# Variables
my $config_file = $cwd.'/trunk_checker.conf';
our @check_nums = ();
my @failed_check = ();
our %config = ();
my $check_delay = 6;
my $allowed_time_diff = 40;
my $sendEmail = $cwd.'/sendEmail.pl';
unless(do $config_file) {
print "ERROR Config file $config_file is missing or invalid: $!\n";
exit 1;
}
if(not defined $config{trunk} or $config{trunk} eq '' or not defined $config{context} or $config{context} eq '' or not defined $config{cid} or not defined $config{db_family} or $config{db_family} eq '') {
print "ERROR Config file $config_file is missing required parameters (trunk, context, cid, db_family are required)\n";
exit 1;
}
if(not defined $config{ami_host} or $config{ami_host} eq '' or not defined $config{ami_port} or $config{ami_port} eq '' or not defined $config{ami_user} or $config{ami_user} eq '' or not defined $config{ami_secret} or $config{ami_secret} eq '') {
print "ERROR Config file $config_file is missing AMI connection parameters (ami_host, ami_port, ami_user, ami_secret)\n";
exit 1;
}
### PID stuff
my $pid_file = PID_DIR.'/'.PID_FILE;
# Check if still running
my $cur_pid = '';
if(-r $pid_file) {
if(open PID, $pid_file) {
$cur_pid = <PID>;
chomp $cur_pid;
$cur_pid = '' unless $cur_pid =~ /^[0-9]+$/;
close PID;
}
}
if($cur_pid) {
if(open PS, "ps -p $cur_pid -o comm,args --no-headers |") {
while(<PS>) {
chomp $_;
if($_ =~ /$script_name/) { print "Still running $cur_pid\n"; exit }
}
close PS;
}
}
# Leave PID file
unless(-d PID_DIR) { mkdir PID_DIR or print "Couldn't create ".PID_DIR."\n" }
if(open PID, ">$pid_file") {
PID->autoflush(1);
print PID "$$\n";
close PID;
} else { print "Couldn't write PID file $pid_file\n" }
###
# connect to Asterisk AMI
my $ami_sock = ami_connect($config{ami_host},$config{ami_port},$config{ami_user},$config{ami_secret});
if(! $ami_sock) { print "ERROR Couldn't connect to ".$config{ami_host}.":".$config{ami_port}."\n"; exit 1 }
my $num_i = 0;
foreach my $num (@check_nums) {
$num_i++;
my $chan = 'Local/'.$num.'@trunk-checker-dial/n';
my $exten = 's';
my $prio = '1';
ami_orig_app($ami_sock,$chan,$config{context},$exten,$prio,$config{cid},'',{ CHECK_THRU_TRUNK => $config{trunk} });
sleep($check_delay);
my $db_key = $num;
my $resp = ami_dbget($ami_sock,$config{db_family},$db_key);
if(defined $resp->[AMI_RES_DATA] and @{$resp->[AMI_RES_DATA]}) {
my $call_receive_time = $resp->[AMI_RES_DATA]->[0];
if($call_receive_time =~ /^\d+$/) {
my $now = time();
if(abs($call_receive_time - $now) > $allowed_time_diff) {
push(@failed_check,$num);
}
} else { push(@failed_check,$num); }
} else { push(@failed_check,$num); }
sleep(3);
}
if(@failed_check) {
my $subj = 'Часть номеров не ответила при проверке доступности номеров';
my $message = "Следующие номера не ответили на тестовый вызов:\n".join(', ',@failed_check);
if(defined $config{mail_to} and $config{mail_to} ne '') {
'$sendEmail -u '$subj' -t '$config{mail_to}' -m '$message' -o message-charset=UTF-8';
}
}
close $ami_sock;
###
### SCRIPT END
###
### Connect to Asterisk manager interface
### login and password are read from /etc/asterisk/manager.conf
sub ami_connect {
my ($ami_host, $ami_port, $ami_user, $ami_pass) = @_;
my $sock = 0;
$sock = IO::Socket::INET->new(
Proto => "tcp",
PeerAddr => $ami_host,
PeerPort => $ami_port,
Timeout => AMI_CONNECT_TIMEOUT
);
$sock or return 0;
$sock->autoflush(1);
my $action_id = gen_pass(10);
my $login_msg = "Action: login\r\nActionID: $action_id\r\nUsername: $ami_user\r\nSecret: $ami_pass\r\nEvents: off\r\n\r\n";
print $sock "$login_msg";
my $response = ["", ""];
while($response->[AMI_RES_ACTIONID] ne $action_id) {
$response = read_ami_message($sock);
if($response->[AMI_RES_VALUE] eq "Error") {
print "ERROR Authentication failed\n";
close $sock;
return 0;
} elsif($response->[AMI_RES_VALUE] eq "") {
print "ERROR AMI read timed out\n";
close $sock;
return 0;
}
}
return $sock;
}
sub ami_orig_app {
my ($sock, $chan, $cont, $ext, $prio, $cid, $src_ext, $vars) = @_;
if($cid =~ /[^0-9+]/) {
if($src_ext) { $cid = $cid.'<'.$src_ext.'>' } else { $cid = $cid.'<'.$ext.'>' }
}
my $action_id = gen_pass(10);
my $msg = "Action: Originate\r\nActionID: $action_id\r\nChannel: $chan\r\nContext: $cont\r\nExten: $ext\r\nPriority: $prio\r\nTimeout: 14000\r\nCallerid: $cid\r\n";
my $var_str="";
foreach (keys %$vars) {
$var_str = $var_str."$_=$vars->{$_}|";
}
$msg = $msg."Variable: ".substr($var_str,0,-1)."\r\n" if $var_str ne "";
$msg = $msg."\r\n";
print $sock "$msg";
my $ami_message = read_ami_message($sock);
if($ami_message->[AMI_RES_VALUE] eq "") { print "ERROR AMI read timed out\n" }
return $ami_message;
}
sub ami_dbget {
my $sock = ${shift(@_)};
my ($family, $key) = @_;
my $action_id = gen_pass(10);
my $msg = "Action: DBGet\r\nActionID: $action_id\r\nFamily: $family\r\nKey: $key\r\n\r\n";
print $sock "$msg";
my $response = ['', ''];
my $res = ['dummy', ''];
while($res->[AMI_RES_ACTIONID] ne $action_id) {
$res = read_ami_message($sock,'DBGet',AMI_READ_TIMEOUT);
if($res->[AMI_RES_VALUE] eq "") {
print STDERR "ERROR AMI DBGet read timed out\n$msg";
return $response;
} elsif($res->[AMI_RES_VALUE] eq "Success") {
$response->[AMI_RES_DATA] = [];
my $res1 = ['dummy', ''];
while($res1->[AMI_RES_VALUE] ne '') {
$res1 = read_ami_message($sock,'DBGetResponse',2);
if($res1->[AMI_RES_ACTIONID] eq $action_id) {
if($res1->[AMI_RES_VALUE] eq 'DBGetResponse') {
if(defined $res1->[AMI_RES_DATA]->{'Val'}) {
push(@{$response->[AMI_RES_DATA]},$res1->[AMI_RES_DATA]->{'Val'});
} else {
print STDERR "ERROR 'Val' not defined in DBGetResponse\n";
}
} elsif($res1->[AMI_RES_VALUE] eq 'DBGetComplete') {
last;
} elsif($res1->[AMI_RES_VALUE] eq '') {
print STDERR "ERROR AMI DBGetResponse read timed out\n";
last;
} else {
print STDERR "ERROR AMI DBGetResponse read returned unexpected response $res1->[AMI_RES_VALUE]\n";
last;
}
}
}
}
}
return $response;
}
#
# Parse AMI message, return @result
# $result[0] = Event or Response
# $result[1] = ActionID
# $result[2] = hash of parameters
#
sub read_ami_message {
my ($sock,$log_label,$timeout) = shift(@_);
if(not defined $log_label) { $log_label = 'Unspecified'; }
if(not defined $timeout) { $timeout = AMI_READ_TIMEOUT; }
my @result = ("", "", {});
eval {
local $SIG{ALRM} = sub { exit };
alarm $timeout;
my $ami_line = "start";
while($ami_line ne "") {
$ami_line = <$sock>;
if(not defined $ami_line) { last; }
$ami_line =~ s/\r\n//g;
#print "$ami_line\n";
if($ami_line =~ /^([^:]+): (.*)$/) {
if($1 eq "Event" or $1 eq "Response") { $result[AMI_RES_VALUE] = $2 }
elsif($1 eq "ActionID") { $result[AMI_RES_ACTIONID] = $2 }
else { $result[AMI_RES_DATA]->{$1} = $2 }
}
}
};
return \@result;
}
### Generate password
### gen_pass(pass_len,var,num_only)
### var - if set, pass_len will randomly vary by +-2 characters
### num_len - if set, only numbers will be used in password generation
sub gen_pass {
my $pass_len = shift(@_);
my $var = shift(@_);
my $num_only = shift(@_);
my @chars = ('a'..'z','A'..'Z','0'..'9','_','-');
if($var) { $pass_len = int(rand 4) + $pass_len - 2 }
if($num_only) { @chars = ('0'..'9') }
my $pass = "";
foreach (1..$pass_len) { $pass .= $chars[rand @chars]; }
return $pass;
}

В одной директории со скриптом должен находиться файл настроек trunk_checker.conf, пример файла настроек:

%config = (
mail_to => 'admin@example.com,boss@example.com',
trunk => 'out_prov',
context => 'trunk-checker-src',
cid => '74444444444',
db_family => 'trunk_checker',
ami_host => 'localhost',
ami_port => '5038',
ami_user => 'admin',
ami_secret => 'admin_secret',
);
@check_nums = (
'81111111111',
'82222222222',
'83333333333',
);

Описание параметров скрипта:
mail_to - список адресов электронной почты, на которые нужно отправлять уведомление о неответивших номерах (разделитель - запятая)
trunk - имя SIP-транка, через который будут совершаться проверочные вызовы
context - имя контекста для второго плеча AMI-действия Originate (в контексте должен быть определён экстеншен s)
cid - номер, который нужно подставлять при исходящих вызовах через out_prov
db_family - имя ветки в AstDB, в которой скрипт будет проверять время получения последнего проверочного вызова (в диалплане Asterisk должна быть указана та же ветка для сохранения время получения последнего проверочного вызова)
ami_host - хост для подключения по AMI
ami_port - порт для подключения по AMI
ami_user - имя пользователя для подключения по AMI
ami_secret - пароль для подключения по AMI
Для отправки уведомлений по почте используется скрипт sendEmail.pl, он должен находиться в одной директории со скриптом trunk_checker.pl. Скрипт sendEmail.pl можно скачать здесь http://caspian.dotconf.net/menu/Software/SendEmail/sendEmail-v1.56.tar.gz. Настройки SMTP-сервера для отправки почты нужно указать в теле скрипта sendEmail.pl (параметры server, port, username, password и from).
Для тестирования запускайте скрипт вручную, следите за выводом скрипта и за выводом в консоли Asterisk, После того, как скрипт заработает нормально, добавьте его в crontab, с помощью похожей строки:

*/2 * * * * /opt/trunk_checker/trunk_checker.pl

asterisk, sip, Провайдеры, Call-файл, Event, Time, call, callerid