sip_to_pjsip: Migrate IPv4/IPv6 (Dual Stack) configurations.
authorAlexander Traud <pabstraud@compuserve.com>
Sat, 20 Aug 2016 14:04:13 +0000 (16:04 +0200)
committerAlexander Traud <pabstraud@compuserve.com>
Fri, 26 Aug 2016 10:49:50 +0000 (12:49 +0200)
When using the migration script sip_to_pjsip.py, and your sip.conf is
configured with bindaddr=::, two transports are written to pjsip.conf, one for
0.0.0.0 (IPv4) and one for [::] (IPv6). That way, PJProject listens on the IPv4
and IPv6 wildcards; a IPv4/IPv6 Dual Stack configuration on a single interface
like in chan_sip.

Furthermore, the script internal functions "build_host" and "split_hostport"
did not parse Literal IPv6 addresses as expected (like [::1]:5060). This change
makes sure, even such addresses are parsed correctly.

ASTERISK-26309

Change-Id: Ia4799a0f80fc30c0550fc373efc207c3330aeb48

contrib/scripts/sip_to_pjsip/sip_to_pjsip.py

index ee01edf..1a0d6a4 100755 (executable)
@@ -1,10 +1,11 @@
 #!/usr/bin/python
 
 import optparse
+import socket
+import urlparse  # Python 2.7 required for Literal IPv6 Addresses
+
 import astdicts
 import astconfigparser
-import socket
-import re
 
 PREFIX = 'pjsip_'
 
@@ -213,37 +214,42 @@ def from_progressinband(key, val, section, pjsip, nmapped):
     set_value('inband_progress', val, section, pjsip, nmapped)
 
 
-def build_host(config, host, section, port_key):
+def build_host(config, host, section='general', port_key=None):
     """
     Returns a string composed of a host:port. This assumes that the host
-    may have a port as part of the initial value. The port_key is only used
-    if the host does not already have a port set on it.
-    Throws a LookupError if the key does not exist
+    may have a port as part of the initial value. The port_key overrides
+    a port in host, see parameter 'bindport' in chan_sip.
     """
-    port = None
-
     try:
         socket.inet_pton(socket.AF_INET6, host)
         if not host.startswith('['):
             # SIP URI will need brackets.
             host = '[' + host + ']'
-        else:
-            # If brackets are present, there may be a port as well
-            port = re.match('\[.*\]:(\d+)', host)
     except socket.error:
-        # No biggie. It's just not an IPv6 address
-        port = re.match('.*:(\d+)', host)
+        pass
 
-    result = host
+    # Literal IPv6 (like [::]), IPv4, or hostname
+    # does not work for IPv6 without brackets; case catched above
+    url = urlparse.urlparse('sip://' + host)
 
-    if not port:
+    if port_key:
         try:
             port = config.get(section, port_key)[0]
-            result += ':' + port
+            host = url.hostname # no port, but perhaps no brackets
+            try:
+                socket.inet_pton(socket.AF_INET6, host)
+                if not host.startswith('['):
+                    # SIP URI will need brackets.
+                    host = '[' + host + ']'
+            except socket.error:
+                pass
+            return host + ':' + port
         except LookupError:
             pass
 
-    return result
+    # Returns host:port, in brackets if required
+    # TODO Does not compress IPv6, for example 0:0:0:0:0:0:0:0 should get [::]
+    return url.netloc
 
 
 def from_host(key, val, section, pjsip, nmapped):
@@ -506,17 +512,59 @@ peer_map = [
 ]
 
 
-def set_transport_common(section, pjsip, nmapped):
+def split_hostport(addr):
+    """
+    Given an address in the form 'host:port' separate the host and port
+    components.
+    Returns a two-tuple of strings, (host, port). If no port is present in the
+    string, then the port section of the tuple is None.
+    """
+    try:
+        socket.inet_pton(socket.AF_INET6, addr)
+        if not addr.startswith('['):
+            return (addr, None)
+    except socket.error:
+        pass
+
+    # Literal IPv6 (like [::]), IPv4, or hostname
+    # does not work for IPv6 without brackets; case catched above
+    url = urlparse.urlparse('sip://' + addr)
+    # TODO Does not compress IPv6, for example 0:0:0:0:0:0:0:0 should get [::]
+    return (url.hostname, url.port)
+
+
+def set_transport_common(section, sip, pjsip, protocol, nmapped):
     """
     sip.conf has several global settings that in pjsip.conf apply to individual
     transports. This function adds these global settings to each individual
     transport.
 
     The settings included are:
+    externaddr (or externip)
+    externhost
+    externtcpport for TCP
+    externtlsport for TLS
     localnet
     tos_sip
     cos_sip
     """
+    try:
+        extern_addr = sip.multi_get('general', ['externaddr', 'externip',
+                                                'externhost'])[0]
+        host, port = split_hostport(extern_addr)
+        try:
+            port = sip.get('general', 'extern' + protocol + 'port')[0]
+        except LookupError:
+            pass
+        set_value('external_media_address', host, section, pjsip,
+                  nmapped, 'transport')
+        set_value('external_signaling_address', host, section, pjsip,
+                  nmapped, 'transport')
+        if port:
+            set_value('external_signaling_port', port, section, pjsip,
+                      nmapped, 'transport')
+    except LookupError:
+        pass
 
     try:
         merge_value('localnet', sip.get('general', 'localnet')[0], 'general',
@@ -538,34 +586,69 @@ def set_transport_common(section, pjsip, nmapped):
         pass
 
 
-def split_hostport(addr):
+def get_bind(sip, pjsip, protocol):
     """
-    Given an address in the form 'addr:port' separate the addr and port
-    components.
-    Returns a two-tuple of strings, (addr, port). If no port is present in the
-    string, then the port section of the tuple is None.
+    Given the protocol (udp, tcp, or tls), return
+    - the bind address, like [::] or 0.0.0.0
+    - name of the section to be created
     """
+    section = 'transport-' + protocol
+
+    # UDP cannot be disabled in chan_sip
+    if protocol != 'udp':
+        try:
+            enabled = sip.get('general', protocol + 'enable')[0]
+        except LookupError:
+            # No value means disabled by default. Don't create this transport
+            return (None, section)
+        if enabled != 'yes':
+            return (None, section)
+
     try:
-        socket.inet_pton(socket.AF_INET6, addr)
-        if not addr.startswith('['):
-            return (addr, None)
-        else:
-            # If brackets are present, there may be a port as well
-            match = re.match('\[(.*\)]:(\d+)', addr)
-            if match:
-                return (match.group(1), match.group(2))
-            else:
-                return (addr, None)
-    except socket.error:
+        bind = pjsip.get(section, 'bind')[0]
+        # The first run created an transport already but this
+        # server was not configured for IPv4/IPv6 Dual Stack
+        return (None, section)
+    except LookupError:
         pass
 
-    # IPv4 address or hostname
-    host, sep, port = addr.rpartition(':')
-
-    if not sep and not port:
-        return (host, None)
+    try:
+        bind = pjsip.get(section + '6', 'bind')[0]
+        # The first run created an IPv6 transport, because
+        # the server was configured with :: as bindaddr.
+        # Now, re-use its port and create the IPv4 transport
+        host, port = split_hostport(bind)
+        bind = '0.0.0.0'
+        if port:
+            bind += ':' + str(port)
+    except LookupError:
+        # This is the first run, no transport in pjsip exists.
+        try:
+            bind = sip.get('general', protocol + 'bindaddr')[0]
+        except LookupError:
+            if protocol == 'udp':
+                try:
+                    bind = sip.get('general', 'bindaddr')[0]
+                except LookupError:
+                    bind = '0.0.0.0'
+            else:
+                try:
+                    bind = pjsip.get('transport-udp6', 'bind')[0]
+                except LookupError:
+                    bind = pjsip.get('transport-udp', 'bind')[0]
+                # Only TCP reuses host:port of UDP, others reuse just host
+                if protocol == 'tls':
+                    bind, port = split_hostport(bind)
+        host, port = split_hostport(bind)
+        if host == '::':
+            section += '6'
+
+    if protocol == 'udp':
+        host = build_host(sip, bind, 'general', 'bindport')
     else:
-        return (host, port)
+        host = build_host(sip, bind)
+
+    return (host, section)
 
 
 def create_udp(sip, pjsip, nmapped):
@@ -575,34 +658,13 @@ def create_udp(sip, pjsip, nmapped):
 
     bindaddr (or udpbindaddr)
     bindport
-    externaddr (or externip)
-    externhost
     """
+    protocol = 'udp'
+    bind, section = get_bind(sip, pjsip, protocol)
 
-    try:
-        bind = sip.multi_get('general', ['udpbindaddr', 'bindaddr'])[0]
-    except LookupError:
-        bind = ''
-
-    bind = build_host(sip, bind, 'general', 'bindport')
-
-    try:
-        extern_addr = sip.multi_get('general', ['externaddr', 'externip',
-                                    'externhost'])[0]
-        host, port = split_hostport(extern_addr)
-        set_value('external_media_address', host, 'transport-udp', pjsip,
-                  nmapped, 'transport')
-        set_value('external_signaling_address', host, 'transport-udp', pjsip,
-                  nmapped, 'transport')
-        if port:
-            set_value('external_signaling_port', port, 'transport-udp', pjsip,
-                      nmapped, 'transport')
-    except LookupError:
-        pass
-
-    set_value('protocol', 'udp', 'transport-udp', pjsip, nmapped, 'transport')
-    set_value('bind', bind, 'transport-udp', pjsip, nmapped, 'transport')
-    set_transport_common('transport-udp', pjsip, nmapped)
+    set_value('protocol', protocol, section, pjsip, nmapped, 'transport')
+    set_value('bind', bind, section, pjsip, nmapped, 'transport')
+    set_transport_common(section, sip, pjsip, protocol, nmapped)
 
 
 def create_tcp(sip, pjsip, nmapped):
@@ -611,131 +673,61 @@ def create_tcp(sip, pjsip, nmapped):
     on the following settings from sip.conf:
 
     tcpenable
-    tcpbindaddr
-    externtcpport
+    tcpbindaddr (or bindaddr)
     """
-
-    try:
-        enabled = sip.get('general', 'tcpenable')[0]
-    except:
-        # No value means disabled by default. No need for a tranport
-        return
-
-    if enabled == 'no':
+    protocol = 'tcp'
+    bind, section = get_bind(sip, pjsip, protocol)
+    if not bind:
         return
 
-    try:
-        bind = sip.get('general', 'tcpbindaddr')[0]
-        bind = build_host(sip, bind, 'general', 'bindport')
-    except LookupError:
-        # No tcpbindaddr means to default to the udpbindaddr
-        bind = pjsip.get('transport-udp', 'bind')[0]
-
-    try:
-        extern_addr = sip.multi_get('general', ['externaddr', 'externip',
-                                    'externhost'])[0]
-        host, port = split_hostport(extern_addr)
-        try:
-            tcpport = sip.get('general', 'externtcpport')[0]
-        except:
-            tcpport = port
-        set_value('external_media_address', host, 'transport-tcp', pjsip,
-                  nmapped, 'transport')
-        set_value('external_signaling_address', host, 'transport-tcp', pjsip,
-                  nmapped, 'transport')
-        if tcpport:
-            set_value('external_signaling_port', tcpport, 'transport-tcp',
-                      pjsip, nmapped, 'transport')
-    except LookupError:
-        pass
-
-    set_value('protocol', 'tcp', 'transport-tcp', pjsip, nmapped, 'transport')
-    set_value('bind', bind, 'transport-tcp', pjsip, nmapped, 'transport')
-    set_transport_common('transport-tcp', pjsip, nmapped)
-
-
-def set_tls_bindaddr(val, pjsip, nmapped):
-    """
-    Creates the TCP bind address. This has two possible methods of
-    working:
-    Use the 'tlsbindaddr' option from sip.conf directly if it has both
-    an address and port. If no port is present, use 5061
-    If there is no 'tlsbindaddr' option present in sip.conf, use the
-    previously-established UDP bind address and port 5061
-    """
-    try:
-        bind = sip.get('general', 'tlsbindaddr')[0]
-        explicit = True
-    except LookupError:
-        # No tlsbindaddr means to default to the bindaddr but with standard TLS
-        # port
-        bind = pjsip.get('transport-udp', 'bind')[0]
-        explicit = False
-
-    matchv4 = re.match('\d+\.\d+\.\d+\.\d+:\d+', bind)
-    matchv6 = re.match('\[.*\]:d+', bind)
-    if matchv4 or matchv6:
-        if explicit:
-            # They provided a port. We'll just use it.
-            set_value('bind', bind, 'transport-tls', pjsip, nmapped,
-                      'transport')
-            return
-        else:
-            # Need to strip the port from the UDP address
-            index = bind.rfind(':')
-            bind = bind[:index]
-
-    # Reaching this point means either there was no port provided or we
-    # stripped the port off. We need to add on the default 5061 port
-
-    bind += ':5061'
+    set_value('protocol', protocol, section, pjsip, nmapped, 'transport')
+    set_value('bind', bind, section, pjsip, nmapped, 'transport')
+    set_transport_common(section, sip, pjsip, protocol, nmapped)
 
-    set_value('bind', bind, 'transport-tls', pjsip, nmapped, 'transport')
 
-
-def set_tls_cert_file(val, pjsip, nmapped):
+def set_tls_cert_file(val, pjsip, section, nmapped):
     """Sets cert_file based on sip.conf tlscertfile"""
-    set_value('cert_file', val, 'transport-tls', pjsip, nmapped,
+    set_value('cert_file', val, section, pjsip, nmapped,
               'transport')
 
 
-def set_tls_private_key(val, pjsip, nmapped):
+def set_tls_private_key(val, pjsip, section, nmapped):
     """Sets privkey_file based on sip.conf tlsprivatekey or sslprivatekey"""
-    set_value('priv_key_file', val, 'transport-tls', pjsip, nmapped,
+    set_value('priv_key_file', val, section, pjsip, nmapped,
               'transport')
 
 
-def set_tls_cipher(val, pjsip, nmapped):
+def set_tls_cipher(val, pjsip, section, nmapped):
     """Sets cipher based on sip.conf tlscipher or sslcipher"""
-    set_value('cipher', val, 'transport-tls', pjsip, nmapped, 'transport')
+    set_value('cipher', val, section, pjsip, nmapped, 'transport')
 
 
-def set_tls_cafile(val, pjsip, nmapped):
+def set_tls_cafile(val, pjsip, section, nmapped):
     """Sets ca_list_file based on sip.conf tlscafile"""
-    set_value('ca_list_file', val, 'transport-tls', pjsip, nmapped,
+    set_value('ca_list_file', val, section, pjsip, nmapped,
               'transport')
 
 
-def set_tls_capath(val, pjsip, nmapped):
+def set_tls_capath(val, pjsip, section, nmapped):
     """Sets ca_list_path based on sip.conf tlscapath"""
-    set_value('ca_list_path', val, 'transport-tls', pjsip, nmapped,
+    set_value('ca_list_path', val, section, pjsip, nmapped,
               'transport')
 
 
-def set_tls_verifyclient(val, pjsip, nmapped):
+def set_tls_verifyclient(val, pjsip, section, nmapped):
     """Sets verify_client based on sip.conf tlsverifyclient"""
-    set_value('verify_client', val, 'transport-tls', pjsip, nmapped,
+    set_value('verify_client', val, section, pjsip, nmapped,
               'transport')
 
 
-def set_tls_verifyserver(val, pjsip, nmapped):
+def set_tls_verifyserver(val, pjsip, section, nmapped):
     """Sets verify_server based on sip.conf tlsdontverifyserver"""
 
     if val == 'no':
-        set_value('verify_server', 'yes', 'transport-tls', pjsip, nmapped,
+        set_value('verify_server', 'yes', section, pjsip, nmapped,
                   'transport')
     else:
-        set_value('verify_server', 'no', 'transport-tls', pjsip, nmapped,
+        set_value('verify_server', 'no', section, pjsip, nmapped,
                   'transport')
 
 
@@ -745,7 +737,7 @@ def create_tls(sip, pjsip, nmapped):
     settings from sip.conf:
 
     tlsenable (or sslenable)
-    tlsbindaddr (or sslbindaddr)
+    tlsbindaddr (or sslbindaddr or bindaddr)
     tlsprivatekey (or sslprivatekey)
     tlscipher (or sslcipher)
     tlscafile
@@ -755,9 +747,16 @@ def create_tls(sip, pjsip, nmapped):
     tlsdontverifyserver
     tlsclientmethod (or sslclientmethod)
     """
+    protocol = 'tls'
+    bind, section = get_bind(sip, pjsip, protocol)
+    if not bind:
+        return
+
+    set_value('protocol', protocol, section, pjsip, nmapped, 'transport')
+    set_value('bind', bind, section, pjsip, nmapped, 'transport')
+    set_transport_common(section, sip, pjsip, protocol, nmapped)
 
     tls_map = [
-        (['tlsbindaddr', 'sslbindaddr'], set_tls_bindaddr),
         (['tlscertfile', 'sslcert', 'tlscert'], set_tls_cert_file),
         (['tlsprivatekey', 'sslprivatekey'], set_tls_private_key),
         (['tlscipher', 'sslcipher'], set_tls_cipher),
@@ -767,26 +766,21 @@ def create_tls(sip, pjsip, nmapped):
         (['tlsdontverifyserver'], set_tls_verifyserver)
     ]
 
-    try:
-        enabled = sip.multi_get('general', ['tlsenable', 'sslenable'])[0]
-    except LookupError:
-        # Not enabled. Don't create a transport
-        return
-
-    if enabled == 'no':
-        return
-
-    set_value('protocol', 'tls', 'transport-tls', pjsip, nmapped, 'transport')
-
     for i in tls_map:
         try:
-            i[1](sip.multi_get('general', i[0])[0], pjsip, nmapped)
+            i[1](sip.multi_get('general', i[0])[0], pjsip, section, nmapped)
         except LookupError:
             pass
 
     try:
-        method = sip.multi_get('general', ['tlsclientmethod', 'sslclientmethod'])[0]
-        print 'In chan_sip, you specified the TLS version. With chan_sip, this was just for outbound client connections. In chan_pjsip, this value is for client and server. Instead, consider not to specify \'tlsclientmethod\' for chan_sip and \'method = sslv23\' for chan_pjsip.'
+        method = sip.multi_get('general', ['tlsclientmethod',
+                                           'sslclientmethod'])[0]
+        if section != 'transport-' + protocol + '6':  # print only once
+            print 'In chan_sip, you specified the TLS version. With chan_sip,' \
+                  ' this was just for outbound client connections. In' \
+                  ' chan_pjsip, this value is for client and server. Instead,' \
+                  ' consider not to specify \'tlsclientmethod\' for chan_sip' \
+                  ' and \'method = sslv23\' for chan_pjsip.'
     except LookupError:
         """
         OpenSSL emerged during the 90s. SSLv2 and SSLv3 were the only
@@ -799,26 +793,7 @@ def create_tls(sip, pjsip, nmapped):
         'sslv23' as default when unspecified, which gives TLSv1.0 and v1.2.
         """
         method = 'sslv23'
-    set_value('method', method, 'transport-tls', pjsip, nmapped, 'transport')
-
-    set_transport_common('transport-tls', pjsip, nmapped)
-    try:
-        extern_addr = sip.multi_get('general', ['externaddr', 'externip',
-                                    'externhost'])[0]
-        host, port = split_hostport(extern_addr)
-        try:
-            tlsport = sip.get('general', 'externtlsport')[0]
-        except:
-            tlsport = port
-        set_value('external_media_address', host, 'transport-tls', pjsip,
-                  nmapped, 'transport')
-        set_value('external_signaling_address', host, 'transport-tls', pjsip,
-                  nmapped, 'transport')
-        if tlsport:
-            set_value('external_signaling_port', tlsport, 'transport-tls',
-                      pjsip, nmapped, 'transport')
-    except LookupError:
-        pass
+    set_value('method', method, section, pjsip, nmapped, 'transport')
 
 
 def map_transports(sip, pjsip, nmapped):
@@ -828,23 +803,29 @@ def map_transports(sip, pjsip, nmapped):
     configuration sections in pjsip.conf.
 
     sip.conf only allows a single UDP transport, TCP transport,
-    and TLS transport. As such, the mapping into PJSIP can be made
-    consistent by defining three sections:
+    and TLS transport for each IP version. As such, the mapping
+    into PJSIP can be made consistent by defining six sections:
 
+    transport-udp6
     transport-udp
+    transport-tcp6
     transport-tcp
+    transport-tls6
     transport-tls
 
     To accommodate the default behaviors in sip.conf, we'll need to
-    create the UDP transport first, followed by the TCP and TLS transports.
+    create the UDP transports first, followed by the TCP and TLS transports.
     """
 
     # First create a UDP transport. Even if no bind parameters were provided
     # in sip.conf, chan_sip would always bind to UDP 0.0.0.0:5060
     create_udp(sip, pjsip, nmapped)
+    create_udp(sip, pjsip, nmapped)
 
     # TCP settings may be dependent on UDP settings, so do it second.
     create_tcp(sip, pjsip, nmapped)
+    create_tcp(sip, pjsip, nmapped)
+    create_tls(sip, pjsip, nmapped)
     create_tls(sip, pjsip, nmapped)
 
 
@@ -1219,6 +1200,8 @@ if __name__ == "__main__":
     sip_filename, pjsip_filename = cli_options()
     # configuration parser for sip.conf
     sip = astconfigparser.MultiOrderedConfigParser()
+    print 'Please, report any issue at:'
+    print '    https://issues.asterisk.org/'
     print 'Reading', sip_filename
     sip.read(sip_filename)
     print 'Converting to PJSIP...'