📜 ⬆️ ⬇️

Integration of asterisk with Active Directory

At some stage of development of our organization, it was decided to switch to VoIP telephony. Asterisk-PBX was unconditionally chosen as the platform. The terminal equipment took the budget from the affordable - DLink DPH-150.
As a result of the work done, an automated VoIP system was obtained, controlled through the standard MS ActiveDirectory snap-in.

Asterisk 1.8.4 was compiled from sources on Ubuntu 9.04.
Walking through the Internet with the search phrase “asterisk active directory”, it was decided to use partial integration based on perl scripts that create configuration files for asterisk. Full integration with AD based on the Asterisk kernel looked depressing in view of the meager amount of information about this on the Internet. The reason for using the selected integration option was one simple perl script (unfortunately, it’s already impossible to find its sources), which was started by crown and formed the 'users.conf' configuration file, after which the asterisk service was restarted. After a thorough study of the possibilities of asterisk and the expansion of the functionality of the found script, the following happened.

Formation and connection users.conf


The script below finds all users in AD who have the 'phone' attribute specified and lists the users in the config format users.conf to stdout.
In the users.conf configuration file, the script is connected as follows:
#exec /etc/asterisk/scripts/users.pl 

The script users.pl:
 #!/usr/bin/perl # users.pl v1.1 # # Script to generate asterisk 'users.conf' file from Active Directory (LADP) on users which contains 'phone' attribute # # Using: # 1. Print users to STDOUT: # users.pl # # 2. Print users to file: # users.pl users_custom.conf use strict; use warnings; use Net::LDAP; use Lingua::Translit; ###################### ### BEGIN SETTINGS ### ###################### my $debug = 0; my $warning = 0; # name of Domain my $AD="domain"; # Domain name in format AD # for example mydomain.ru my $ADDC="DC=domain"; # user in Active directory # example: "CN=asterisk,CN=Users,$ADDC" my $ADUserBind="CN=asterisk,CN=Users,$ADDC"; my $ADpass="p@s$w0rd"; # base search tree # example "OU=Users,$ADDC" my $ADUsersSearchBase="OU=Organisation,$ADDC"; # Field in active directory where telephone number, display name, phone stored # "telephonenumber", "displayname", "mail" my $ADfieldTelephone="telephonenumber"; my $ADfieldFullName="displayname"; my $ADfieldMail="mail"; my $ADfieldUser="samaccountname"; # You need to create a dialplan in your asterisk server; my $dialplan="office"; # default settings my $user_static = "context = $dialplan call-limit = 100 type = friend registersip = no host = dynamic callgroup = 1 threewaycalling = no hasdirectory = no callwaiting = no hasmanager = no hasagent = no hassip = yes hasiax = yes nat=yes qualify=yes dtmfmode = rfc2833 insecure = no pickupgroup = 1 autoprov = no label = macaddress = linenumber = 1 LINEKEYS = 1 callcounter = yes disallow = all allow = ulaw,alaw,iLBC,h263,h263p "; ####################### ### END OF SETTINGS ### ####################### my $ldap; # get array DNS names of AD controllers my $dig = "dig -t srv _ldap._tcp.$AD" . '| grep -v "^;\|^$" | grep SRV | awk "{print \$8}"'; my @adControllers = `$dig`; # try connect to AD controllers foreach my $controller (@adControllers){ $controller =~ s/\n//; #INITIALIZING $ldap = Net::LDAP->new ( $controller ) or next; print STDERR "Connected to AD controller: $controller\n" if $debug > 0; last; } die "$@" unless $ldap; my $mesg = $ldap->bind ( dn=>$ADUserBind, password =>$ADpass); #PROCESSING - Displaying SEARCH Results # Accessing the data as if in a structure # ie Using the "as_struct" method my $ldapUsers = LDAPsearch ( $ADUsersSearchBase, "$ADfieldTelephone=*", [ $ADfieldFullName, $ADfieldTelephone, $ADfieldMail, $ADfieldUser ] )->as_struct; # translit RUS module. # GOST 7.79 RUS, reversible, GOST 7.79:2000 (table B), Cyrillic to Latin, Russian my $tr = new Lingua::Translit("GOST 7.79 RUS"); my %hashPhones = (); my $phones = \%hashPhones; my @out; while ( my ($distinguishedName, $attrs) = each(%$ldapUsers) ) { # if not exist phone or name - skipping my $attrPhone = $attrs->{ "$ADfieldTelephone" } || next; my $attrUser = $attrs->{ "$ADfieldUser" } || next; my $attrName = $attrs->{ "$ADfieldFullName" } || next; my $encName = $tr->translit("@$attrName"); my $attrMail = $attrs->{ "$ADfieldMail" } || [""]; # check for duplicates phone number if ( $phones -> {"@$attrPhone"} ){ my $currUser = "@$attrName"; my $existUser = $phones -> {"@$attrPhone"}; print STDERR "@$attrPhone alredy exist! Exist:'$existUser' Current:'$currUser'... skipping - '[@$attrPhone] $currUser'\n" if $warning; next; } else { $phones -> {"@$attrPhone"} = "@$attrName"; } # password for SID = (telephonenumber without first digit) + 1 # example: phone=6232 pass=233 #$phsecret =sprintf("%03d",( substr("@$attrVal",1,100)+1)); my $phsecret = "@$attrPhone"; push (@out, "[@$attrPhone]\n" . "fullname = $encName\n" . "email = @$attrMail\n" . "username = @$attrUser\n" #. "mailbox = @$attrPhone\n" . "cid_number = @$attrPhone\n" . "vmsecret = $phsecret\n" . "secret = $phsecret\n" . "transfer = yes\n" . "$user_static\n" ); } # End of that DN # print to file if (@ARGV){ open FILE, "> $ARGV[0]" or die "Error create file '$ARGV[0]': $!"; print STDOUT "Printing to file '$ARGV[0]'"; print FILE @out; close FILE; print STDOUT " ...done!\n"; } # print to STDOUT else{ print @out; } exit 0; #OPERATION - Generating a SEARCH #$base, $searchString, $attrsArray sub LDAPsearch { my ($base, $searchString, $attrs) = @_; my $ret = $ldap->search ( base => $base, scope => "sub", filter => $searchString, attrs => $attrs ); LDAPerror("LDAPsearch", $ret) && die if( $ret->code ); return $ret; } sub LDAPerror { my ($from, $mesg) = @_; my $err = "[$from] - error" ."\nCode: " . $mesg->code ."\nError: " . $mesg->error . " (" . $mesg->error_name . ")" ."\nDescripton: " . $mesg->error_desc . ". " . $mesg->error_text; print STDERR $err if $warning; } 

Formation of telephone groups based on AD groups

The main numbering plan is painted manually in the extensions.conf configuration file. But in our organization, quite often employees move from one department to another, because of which they would have to constantly reorganize the extensions.conf config, which together with the human factor would lead to inevitable errors. The essence of the alternative solution is that in AD, in a specific OU (in the $ ADGroupsSearchBase script), groups are created in which the telephone number of the group is written, and the subscribers to whom the call will be received when dialing the group number are included in the "members".
')
The script in the config is connected in the same way:
 #exec /etc/asterisk/scripts/exten.pl 

Script:
 #!/usr/bin/perl # exten.pl v1.1 # # Script to generate extensions 'extensions_custom.conf' file, # from Active Directory (LADP) on groups in OU=ADGroupsSearchBase # which groups contains 'description' attribute # # Using: # 1. Print users to STDOUT: # exten.pl # # 2. Print users to file: # exten.pl exten_custom.conf use strict; use warnings; use Net::LDAP; use Lingua::Translit; ###################### ### BEGIN SETTINGS ### ###################### my $debug = 0; my $warning = 1; #name of Domain my $AD="domain"; #Domain name in format AD #for example mydomain.ru my $ADDC="DC=domain"; # user in Active directory # example: "CN=asterisk,CN=Users,$ADDC" my $ADUserBind="CN=asterisk,CN=Users,$ADDC"; my $ADpass="p@s$w0rd"; # base search Groups tree example "OU=Users,$ADDC" my $ADGroupsSearchBase = "OU=asterisk,OU=Groups,OU=Organisation,$ADDC"; # base search Users tree example "OU=Users,$ADDC" my $ADUsersSearchBase = "OU=Organisation,$ADDC"; # default email to send voicemail if email user not set my $defaultEmail = 'asterisk@Organisation.com'; # Field in active directory where telephone number, display name, phone stored ... # "telephonenumber", "displayname", "mail", ... my $ADfieldTelephone = "telephonenumber"; my $ADfieldMember = "member"; my $ADfieldMemberOf = "memberof"; my $ADfieldInfo = "info"; my $ADfieldDescription = "description"; my $ADfieldMail = "mail"; ####################### ### END OF SETTINGS ### ####################### my $ldap; # get array DNS names of AD controllers my @adControllers = `dig -t srv _ldap._tcp.$AD | grep -v '^;\\|^\$' | grep SRV | awk '{print \$8}'`; # try connect to AD controllers foreach my $controller (@adControllers){ $controller =~ s/\n//; #INITIALIZING $ldap = Net::LDAP->new ( $controller ) or next; print STDERR "Connected to AD controller: $controller\n" if $debug > 0; last; } die "$@" unless $ldap; my $mesg = $ldap->bind ( dn=>$ADUserBind, password =>$ADpass); #PROCESSING - Displaying SEARCH Results # Accessing the data as if in a structure # ie Using the "as_struct" method my $ldapGroups = LDAPsearch ( $ADGroupsSearchBase, "$ADfieldDescription=*", [ $ADfieldMember, $ADfieldDescription ] )->as_struct; # translit RUS module. # GOST 7.79 RUS, reversible, GOST 7.79:2000 (table B), Cyrillic to Latin, Russian my $tr = new Lingua::Translit("GOST 7.79 RUS"); my $hash = (); # process each group in $ADGroupsSearchBase with phone while ( my ($distinguishedName, $groupAttrs) = each(%$ldapGroups) ) { print STDERR "Processing GROUP: [$distinguishedName]\n" if $debug > 1; my $attrMembers = $groupAttrs->{ $ADfieldMember } or next; my $desc = $groupAttrs->{ $ADfieldDescription } or next; my $groupNumber = "@$desc"; print STDERR "MEMBERS: @$attrMembers\nDESC: $groupNumber (Count=$#$attrMembers+1)" if $debug > 1; # process members in current group foreach my $member (@$attrMembers) { my $ldapMember = LDAPsearch( $ADUsersSearchBase, "$ADfieldTelephone=*", [ $ADfieldTelephone ] ) -> as_struct; my $memberAttrs = $ldapMember->{$member}; my $memberPhone = $memberAttrs->{$ADfieldTelephone}[0] or next; print STDERR "\nMEMBER: $member" if $debug > 1; print STDERR "\tPHONE:$memberPhone" if $debug > 1; if ($hash -> {$groupNumber}){ my $a = $hash -> {$groupNumber}; push @$a, $memberPhone; } else { $hash -> {$groupNumber} = [$memberPhone]; } } print STDERR "\n\n" if $debug > 1; } # End of that groups in $ADGroupsSearchBase my @out; while ( my ($groupPhone, $userPhones) = each (%$hash) ) { print STDERR "GROUP: $groupPhone\t PHONES: @$userPhones\n" if $debug > 1; #foreach my $userPhone (@$userPhones) { push (@out, "exten => $groupPhone,1,Dial(sip/" . join('&sip/', @$userPhones) . ")\n"); } # print to file if (@ARGV){ open FILE, "> $ARGV[0]" or die "Error create file '$ARGV[0]': $!"; print STDOUT "Printing to file '$ARGV[0]'"; print FILE @out; close FILE; print STDOUT " ...done!\n"; } # print to STDOUT else{ print @out; } exit 0; #OPERATION - Generating a SEARCH # $base, $searchString, $attrsArray sub LDAPsearch { my ($base, $searchString, $attrs) = @_; my $ret = $ldap->search ( base => $base, scope => "sub", filter => $searchString, attrs => $attrs ); LDAPerror("LDAPsearch", $ret) && die if( $ret->code ); return $ret; } sub LDAPerror { my ($from, $mesg) = @_; my $err = "[$from] - error" ."\nCode: " . $mesg->code ."\nError: " . $mesg->error . " (" . $mesg->error_name . ")" ."\nDescripton: " . $mesg->error_desc . ". " . $mesg->error_text; print STDERR $err if $warning; #print STDERR "\nServer error: " . $mesg->server_error if $debug; } 


The output of the script is something like this:
exten => 605,1,Dial(sip/157&sip/130&sip/444&sip/103&sip/119&sip/151&sip/117)
exten => 602,1,Dial(sip/122&sip/110&sip/106)
exten => 607,1,Dial(sip/444&sip/122&sip/110&sip/100&sip/101)
exten => 601,1,Dial(sip/155&sip/101)
exten => 606,1,Dial(sip/444&sip/110&sip/100&sip/101)


Automation

To automatically upload new data from AD to cron, the task of reloading the asterisk configuration has been added:
 asterisk -rx reload 

With such a reboot, unlike the restart of the entire service, telephone sessions do not terminate.

Continuation

If the article is of interest to the community, then I am ready to continue the narration in which I would like to include the following topics:
  1. Automatically expand the configuration of DLINK DPH-150 phones, and other devices that support autoprovision
  2. Using DialFox software for automatic dialing with NTLM authorization via AD. In particular, screwing mod_ntlm to apache2


Thank you for your attention.

PS In the course of writing scripts, I tried to compile all comments in English for universality. But unfortunately the grammar in the foreign language leaves much to be desired. I hope the basic meaning of the comments will be clear.

UPD: Updated scripts. Added by:
1. Definition of domain controllers from a DNS server.
2. Ability to run a script with a parameter - the name of the file to which stdout will be written.

Source: https://habr.com/ru/post/125359/


All Articles