Repository: pyradius/pyrad

Issue:
ClientAsync not able to validate reply
Setup:
Server: Freeradius Server 3.0
Client: pyrad with Python 3.5.3  debian stretch (tested with Python 3.9.2, debian buster, too)

I'm issuing a simple authentication request. With the synchronous client this works without problems.
With the ClientAsync I get this log message (with different payloads per request):

**[192.168.0.1:1812] Ignore invalid reply: b'\x02\x8c\x00\x14\x89\xc3\xa2\x1b\x00\xf7f\xc5(\xc6\xd1#\xb9\x86\xf2%'**

I can observe that the server accepts a request with valid credentials and rejects one with invalid credentials. The log message stays the same. I tried the provided example code, too, but it had the same result. 
So I guess theres a problem with the encoding.

Sample code:
```
import pyrad.packet
from pyrad.client_async import ClientAsync
from pyrad.dictionary import Dictionary
import asyncio

class Settings:
    # sentinel class for settings
    pass

settings = Settings()
settings.server = "192.168.0.1"
settings.dictionary = "dictionary"
settings.radius_secret = "secret"

client = ClientAsync(server=settings.server,secret=bytes(settings.radius_secret, "utf-8"),timeout=4,dict=Dictionary(settings.dictionary))

username = "username"
password = "password"

async def check_radius(username, password, client):
    await client.initialize_transports(
        enable_auth=True,
        local_addr='192.168.0.2',
        local_auth_port=800,
        enable_acct=False,
        enable_coa=False
    )
    request = client.CreateAuthPacket(code=pyrad.packet.AccessRequest,User_Name=username,NAS_Identifier="localhost")
    request["User-Password"] = request.PwCrypt(password)
    response = await client.SendPacket(request)
    await client.deinitialize_transports()
    if response.code == pyrad.packet.AccessAccept:
        print("access accepted")
    else:
        print("access denied")

loop = asyncio.get_event_loop()
loop.run_until_complete(check_radius(username, password, client))
```

Discussion:

Patch:
diff --git a/.gitignore b/.gitignore
index 705b375..b435cec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 .DS_Store
+
 build/
 dist/
 docs/.build/
@@ -7,7 +8,10 @@ pyrad.egg-info/
 *__pycache__
 *.egg/
 
-#virtual environments
+.coverage
+coverage.lcov
+
+# virtual environments
 .env
 env
 .venv
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index e0c6cc0..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-language: python
-python:
-  - "2.7"
-  - "3.6"
-  - "3.7"
-  - "3.8"
-# command to install dependencies
-install:
-  - pip install .
-  - pip install coverage python-coveralls
-# command to run tests
-script: nosetests -v --with-coverage --cover-package=pyrad
-after_success:
-  - coveralls
diff --git a/CHANGES.rst b/CHANGES.rst
index 0e369b9..768d8dc 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,6 +1,14 @@
 Changelog
 =========
 
+* Add salt decryption of encrypted attributes
+
+* Fix create CoA packet in client_async
+
+* Fix #152 and add corresponding unittests
+
+* Fixed unittests
+
 2.4 - Nov 23, 2020
 -------------------
 
diff --git a/LICENSE.txt b/LICENSE.txt
index d68b3a4..1a92f5d 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,5 +1,5 @@
 Copyright 2020 Istvan Ruzman. All rights reserved.
-Copyright 2017-2020 Christian Giese. All rights reserved.
+Copyright 2017-2023 Christian Giese. All rights reserved.
 Copyright 2007-2008 Simplon. All rights reserved.
 Copyright 2002-2008 Wichert Akkerman. All rights reserved.
 
diff --git a/README.rst b/README.rst
index 17b56fc..06a7480 100644
--- a/README.rst
+++ b/README.rst
@@ -1,8 +1,6 @@
 
 .. image:: https://github.com/pyradius/pyrad/workflows/Python%203.X%20test/badge.svg?branch=master
     :target: https://github.com/pyradius/pyrad/actions?query=workflow
-.. image:: https://travis-ci.org/pyradius/pyrad.svg?branch=master
-    :target: https://travis-ci.org/pyradius/pyrad
 .. image:: https://coveralls.io/repos/github/pyradius/pyrad/badge.svg?branch=master
     :target: https://coveralls.io/github/pyradius/pyrad?branch=master
 .. image:: https://img.shields.io/pypi/v/pyrad.svg
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 4def982..7d15d8e 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -49,8 +49,8 @@ master_doc = 'index'
 
 # General information about the project.
 project = u'pyrad'
-copyright = u'Copyright 2002-2020 Wichert Akkerman, Istvan Ruzman and Christian Giese. All rights reserved.'
-author = u'Christian Giese <developer@gicnet.de> and Istvan Ruzman <istvan@ruzman.eu>'
+copyright = u'Copyright 2002-2023 Wichert Akkerman, Istvan Ruzman and Christian Giese. All rights reserved.'
+author = u'Christian Giese <gic@gicnet.de> and Istvan Ruzman <istvan@ruzman.eu>'
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
diff --git a/pyrad/__init__.py b/pyrad/__init__.py
index feb21f8..56f924e 100644
--- a/pyrad/__init__.py
+++ b/pyrad/__init__.py
@@ -40,7 +40,7 @@ __docformat__ = 'epytext en'
 
 __author__ = 'Christian Giese <developer@gicnet.de>'
 __url__ = 'http://pyrad.readthedocs.io/en/latest/?badge=latest'
-__copyright__ = 'Copyright 2002-2020 Wichert Akkerman and Christian Giese. All rights reserved.'
+__copyright__ = 'Copyright 2002-2023 Wichert Akkerman, Istvan Ruzman and Christian Giese. All rights reserved.'
 __version__ = '2.4'
 
 __all__ = ['client', 'dictionary', 'packet', 'server', 'tools', 'dictfile']
diff --git a/pyrad/client_async.py b/pyrad/client_async.py
index bdf1af2..b30089c 100644
--- a/pyrad/client_async.py
+++ b/pyrad/client_async.py
@@ -121,7 +121,7 @@ class DatagramProtocolClient(asyncio.Protocol):
         try:
             reply = Packet(packet=data, dict=self.client.dict)
 
-            if reply and reply.id in self.pending_requests:
+            if reply.code and reply.id in self.pending_requests:
                 req = self.pending_requests[reply.id]
                 packet = req['packet']
 
diff --git a/pyrad/tools.py b/pyrad/tools.py
index 8425418..2e51175 100644
--- a/pyrad/tools.py
+++ b/pyrad/tools.py
@@ -1,8 +1,8 @@
 # tools.py
 #
 # Utility functions
-from netaddr import IPAddress
-from netaddr import IPNetwork
+from ipaddress import IPv4Address, IPv6Address
+from ipaddress import IPv4Network, IPv6Network
 import struct
 import six
 import binascii
@@ -43,25 +43,25 @@ def EncodeOctets(octetstring):
 def EncodeAddress(addr):
     if not isinstance(addr, six.string_types):
         raise TypeError('Address has to be a string')
-    return IPAddress(addr).packed
+    return IPv4Address(addr).packed
 
 
 def EncodeIPv6Prefix(addr):
     if not isinstance(addr, six.string_types):
         raise TypeError('IPv6 Prefix has to be a string')
-    ip = IPNetwork(addr)
+    ip = IPv6Network(addr)
     return struct.pack('2B', *[0, ip.prefixlen]) + ip.ip.packed
 
 
 def EncodeIPv6Address(addr):
     if not isinstance(addr, six.string_types):
         raise TypeError('IPv6 Address has to be a string')
-    return IPAddress(addr).packed
+    return IPv6Address(addr).packed
 
 
 def EncodeAscendBinary(str):
     """
-    Format: List of type=value pairs sperated by spaces.
+    Format: List of type=value pairs separated by spaces.
 
     Example: 'family=ipv4 action=discard direction=in dst=10.10.255.254/32'
 
@@ -100,9 +100,11 @@ def EncodeAscendBinary(str):
         'dportq':       b'\x00'
     }
 
+    family = 'ipv4'
     for t in str.split(' '):
         key, value = t.split('=')
         if key == 'family' and value == 'ipv6':
+            family = 'ipv6'
             terms[key] = b'\x03'
             if terms['src'] == b'\x00\x00\x00\x00':
                 terms['src'] = 16 * b'\x00'
@@ -113,8 +115,11 @@ def EncodeAscendBinary(str):
         elif key == 'direction' and value == 'out':
             terms[key] = b'\x00'
         elif key == 'src' or key == 'dst':
-            ip = IPNetwork(value)
-            terms[key] = ip.ip.packed
+            if family == 'ipv4':
+                ip = IPv4Network(value)
+            else: 
+                ip = IPv6Network(value)
+            terms[key] = ip.network_address.packed
             terms[key+'l'] = struct.pack('B', ip.prefixlen)
         elif key == 'sport' or key == 'dport':
             terms[key] = struct.pack('!H', int(value))
@@ -167,13 +172,13 @@ def DecodeAddress(addr):
 def DecodeIPv6Prefix(addr):
     addr = addr + b'\x00' * (18-len(addr))
     _, length, prefix = ':'.join(map('{0:x}'.format, struct.unpack('!BB'+'H'*8, addr))).split(":", 2)
-    return str(IPNetwork("%s/%s" % (prefix, int(length, 16))))
+    return str(IPv6Network("%s/%s" % (prefix, int(length, 16))))
 
 
 def DecodeIPv6Address(addr):
     addr = addr + b'\x00' * (16-len(addr))
     prefix = ':'.join(map('{0:x}'.format, struct.unpack('!'+'H'*8, addr)))
-    return str(IPAddress(prefix))
+    return str(IPv6Address(prefix))
 
 
 def DecodeAscendBinary(str):
diff --git a/setup.py b/setup.py
index ba93ffb..3af8807 100644
--- a/setup.py
+++ b/setup.py
@@ -20,6 +20,8 @@ setup(name='pyrad',
           'Programming Language :: Python :: 3.6',
           'Programming Language :: Python :: 3.7',
           'Programming Language :: Python :: 3.8',
+          'Programming Language :: Python :: 3.9',
+          'Programming Language :: Python :: 3.10',
           'Topic :: Software Development :: Libraries :: Python Modules',
           'Topic :: System :: Systems Administration :: Authentication/Directory',
       ],
@@ -27,7 +29,7 @@ setup(name='pyrad',
       keywords=['radius', 'authentication'],
       zip_safe=True,
       include_package_data=True,
-      install_requires=['six', 'netaddr'],
+      install_requires=['six'],
       tests_require='nose>=0.10.0b1',
       test_suite='nose.collector',
-      )
\ No newline at end of file
+      )


Test patch:
diff --git a/tests/testTools.py b/tests/testTools.py
index bd7d1e0..14c3891 100644
--- a/tests/testTools.py
+++ b/tests/testTools.py
@@ -1,4 +1,4 @@
-from netaddr import AddrFormatError
+from ipaddress import AddressValueError
 from pyrad import tools
 import unittest
 import six
@@ -17,7 +17,7 @@ class EncodingTests(unittest.TestCase):
         self.assertRaises(TypeError, tools.EncodeString, 1)
 
     def testAddressEncoding(self):
-        self.assertRaises(AddrFormatError, tools.EncodeAddress, 'TEST123')
+        self.assertRaises(AddressValueError, tools.EncodeAddress, 'TEST123')
         self.assertEqual(
                 tools.EncodeAddress('192.168.0.255'),
                 six.b('\xc0\xa8\x00\xff'))