A lightweight LDAP server providing a searchable directory from the FOP2 Visual Phonebook used with FreePBX/Asterisk.
This project is a modified fork of affordablemobiles/freepbx-ldap,
adapted to use the FOP2 Visual Phonebook as its contact source and to
improve compatibility with different IP phones and phone number formats.
- Uses the FOP2 Visual Phonebook as a single source of contacts.
- Provides LDAP directory searches without the need to synchronize or export contacts.
- Supports searches by name and telephone number.
- Handles national and international phone number formats such as:
071XXXXXXX+3371XXXXXXX003371XXXXXXX
- Uses
phonenumbersfor international phone number parsing and normalization. - Supports configurable MySQL connection settings.
- Supports configurable LDAP listening port.
- Configuration changes do not require recompiling the program.
- Supports IPv4 and IPv6 MySQL server addresses.
- Optional verbose debug logging.
- Designed for use with Grandstream, Snom and other LDAP-capable IP phones.
The application starts a lightweight LDAP service on the port configured
by LDAP_PORT. The default port is:
10389
LDAP directory search requests from an IP phone are translated into SQL queries against the FOP2 Visual Phonebook tables in the FreePBX/Asterisk MySQL database.
The relevant FOP2 tables are:
visual_phonebook
visual_phonebook_phones
The LDAP attributes returned to the phone are:
displayName
telephoneNumber
The MySQL to LDAP mapping is:
lastname + firstname -> displayName
number -> telephoneNumber
Because the contacts are read directly from the FOP2 Visual Phonebook, the LDAP directory is always up-to-date. There is no separate contact import, export or synchronization process.
Many IP phones require an LDAP bind with a username and password before performing a directory search.
This server intentionally accepts any LDAP bind request and returns success without validating the supplied username or password.
The LDAP credentials configured in the phone therefore do not provide access control.
For this reason, the LDAP service should only be exposed to trusted networks or protected by appropriate firewall rules.
Telephone numbers may be stored or entered in different formats.
For example, the same French number may appear as:
071XXXXXXX
+3371XXXXXXX
003371XXXXXXX
The server normalizes telephone number searches to improve matching between national and international representations.
International calling codes are handled using:
github.com/nyaruka/phonenumbers/v2
This avoids hard-coding individual country codes and also supports international numbering plans including overseas territories.
Partial number searches are supported as well.
The server reads its configuration from:
/opt/freepbx-ldap/freepbx-ldap.conf
Example:
FREEPBX_SQLSERVER=localhost
FREEPBX_SQLUSER=freepbxldap
FREEPBX_SQLPASSWORD=secret
FREEPBX_SQLDATABASE=asterisk
LDAP_PORT=10389
DEBUG=OFFEnvironment variables with the same names override values from the configuration file.
A hostname or IPv4 address may be specified with or without a port:
FREEPBX_SQLSERVER=localhostor:
FREEPBX_SQLSERVER=192.168.111.9A non-standard MySQL port can be specified explicitly:
FREEPBX_SQLSERVER=192.168.111.9:3307If no port is specified, MySQL port 3306 is used.
IPv6 addresses must be enclosed in square brackets:
FREEPBX_SQLSERVER=[2001:db8::2]With an explicit MySQL port:
FREEPBX_SQLSERVER=[2001:db8::2]:3307If no port is given, port 3306 is used.
The LDAP listening port can be changed with:
LDAP_PORT=10389The default is 10389.
Remember to update the LDAP port configured on the phones if this value is changed.
Verbose application logging can be enabled with:
DEBUG=ONFor normal operation as a systemd service:
DEBUG=OFFThe following values enable debug mode:
ON
TRUE
YES
1
Values are case-insensitive.
With debug mode enabled, additional information such as LDAP search filters, generated SQL queries and returned directory entries is written to the log.
DEBUG=OFF is recommended for normal operation as a systemd service.
The Go runtime is required to build the application.
Clone the repository and run:
go buildThe required Go modules, including the phone number parsing library, are managed through the Go module configuration.
Create the application directory:
mkdir -p /opt/freepbx-ldapCopy the compiled binary:
cp <freepbx-ldap binary location> /opt/freepbx-ldap/freepbx-ldapCopy or create the configuration file:
cp <freepbx-ldap.conf location> /opt/freepbx-ldap/freepbx-ldap.confSet ownership:
chown -R asterisk:asterisk /opt/freepbx-ldapMake the binary executable:
chmod +x /opt/freepbx-ldap/freepbx-ldapProtect the configuration file because it contains the MySQL credentials:
chmod 600 /opt/freepbx-ldap/freepbx-ldap.confInstall the systemd service:
cp <systemd/freepbx-ldap.service location> /etc/systemd/system/freepbx-ldap.serviceReload systemd:
systemctl daemon-reloadEnable the service at boot:
systemctl enable freepbx-ldapStart it:
systemctl start freepbx-ldapCheck its status:
systemctl status freepbx-ldapLogs can be viewed with:
journalctl -u freepbx-ldapFor live logging:
journalctl -fu freepbx-ldapIP phones must be configured to use the FreePBX/FOP2 LDAP server.
The examples below provide working starting points for several phone families.
Grandstream phones should use LDAP version 3.
This is particularly important after a factory reset, as some firmware versions may revert to LDAP version 2. LDAPv2 may result in the phone connecting to the server and immediately closing the connection after the bind.
Partial example from a Grandstream GXP1620 configuration:
# Generated by GXP1620
boot 1.0.7.4; core 1.0.7.9; base 1.0.7.23; prog 1.0.7.64
[...]
P8020=192.168.111.9
P8021=10389
P8022=dc=asterisk
P8023=asterisk
P8025=(telephoneNumber=%)
P8026=(|(displayName=%)(telephoneNumber=%))
P8028=displayName
P8029=telephoneNumber
P8030=%displayName %telephoneNumber
P8031=100
P8032=15
P8033=1
P8034=1
P8035=1
P8036=displayName
P8037=0
[...]
# End of exported configuration
The LDAP server address and port must of course be adapted to the local installation.
<?xml version="1.0" encoding="utf-8"?>
<settings>
<phone-settings>
*** Other Settings ***
<ldap_server perm="">***server_ip***</ldap_server>
<ldap_port perm="">10389</ldap_port>
<ldap_base perm="">dc=asterisk</ldap_base>
<ldap_username perm="">asterisk</ldap_username>
<ldap_max_hits perm="">100</ldap_max_hits>
<ldap_search_filter perm="">(&(telephoneNumber=*)(displayName=%))</ldap_search_filter>
<ldap_number_filter perm="">(&(telephoneNumber=%)(displayName=*))</ldap_number_filter>
<ldap_name_attributes perm="">displayName</ldap_name_attributes>
<ldap_number_attributes perm="">telephoneNumber</ldap_number_attributes>
<ldap_display_name perm="">%displayName</ldap_display_name>
<gui_fkey1 perm="">keyevent F_DIRECTORY_SEARCH</gui_fkey1>
</phone-settings>
</settings><?xml version="1.0" encoding="utf-8"?>
<settings>
<phone-settings>
*** Other Settings ***
<ldap_server perm="">***server_ip***</ldap_server>
<ldap_port perm="">10389</ldap_port>
<ldap_base perm="">dc=asterisk</ldap_base>
<ldap_username perm="">asterisk</ldap_username>
<ldap_max_hits perm="">25</ldap_max_hits>
<ldap_search_filter perm="">(&(telephoneNumber=*)(displayName=%))</ldap_search_filter>
<ldap_number_filter perm="">(&(telephoneNumber=%)(displayName=*))</ldap_number_filter>
<ldap_name_attributes perm="">displayName</ldap_name_attributes>
<ldap_number_attributes perm="">telephoneNumber</ldap_number_attributes>
<ldap_display_name perm="">%displayName</ldap_display_name>
<idle_cancel_key_action perm="">keyevent F_DIRECTORY_SEARCH</idle_cancel_key_action>
</phone-settings>
<functionKeys e="2">
<fkey idx="3" context="active" label="" perm="">keyevent F_DIRECTORY_SEARCH</fkey>
</functionKeys>
</settings>Firmware UC 4 or newer is required.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<localcfg>
*** Other Settings ***
<dir>
<dir.corp
dir.corp.address="ldap://***server_ip***"
dir.corp.port="10389"
dir.corp.transport="TCP"
dir.corp.baseDN="dc=asterisk"
dir.corp.scope="sub"
dir.corp.filterPrefix=""
dir.corp.user="uid=asterisk,dc=asterisk"
dir.corp.pageSize="32"
dir.corp.password="supersecret"
dir.corp.cacheSize="128"
dir.corp.leg.pageSize="8"
dir.corp.leg.cacheSize="32"
dir.corp.autoQuerySubmitTimeout="1"
dir.corp.viewPersistence="0"
dir.corp.leg.viewPersistence="0"
dir.corp.sortControl="0">
<dir.corp.attribute
dir.corp.attribute.1.name="displayName"
dir.corp.attribute.1.label="Display Name"
dir.corp.attribute.1.type="first_name"
dir.corp.attribute.1.searchable="1"
dir.corp.attribute.1.filter=""
dir.corp.attribute.1.sticky="0"
dir.corp.attribute.2.name="telephoneNumber"
dir.corp.attribute.2.label="phone number"
dir.corp.attribute.2.type="phone_number"
dir.corp.attribute.2.filter=""
dir.corp.attribute.2.sticky="0"
dir.corp.attribute.2.searchable="1">
</dir.corp.attribute>
<dir.corp.backGroundSync
dir.corp.backGroundSync.period="3600">
</dir.corp.backGroundSync>
<dir.corp.vlv
dir.corp.vlv.allow="1"
dir.corp.vlv.sortOrder="displayName telephoneNumber">
</dir.corp.vlv>
</dir.corp>
</dir>
<feature feature.corporateDirectory.enabled="1"/>
<softkey softkey.feature.directories="1"/>
</localcfg>This LDAP server does not authenticate LDAP users.
Any username and password supplied in an LDAP bind request are accepted.
The service should therefore only be accessible from trusted local networks or explicitly permitted hosts. Do not expose the LDAP port directly to the public Internet.
The MySQL configuration file contains database credentials and should be protected accordingly:
chmod 600 /opt/freepbx-ldap/freepbx-ldap.confRun the application directly with:
DEBUG=ONto obtain verbose logging while testing LDAP searches.
Typical useful information includes:
Request BaseDn=...
Request FilterString=...
Request Attributes=...
Query SQL: ...
To verify that the LDAP port is listening:
ss -lntp | grep 10389A basic LDAP query can be tested with:
ldapsearch \
-H ldap://127.0.0.1:10389 \
-D "asterisk" \
-w "0000" \
-b "dc=asterisk"The username and password used for this test are arbitrary because LDAP bind authentication is intentionally not enforced.
For Grandstream phones, verify that LDAP version 3 is enabled if the phone connects, sends a bind request and then immediately disconnects without performing a search.