diff --git a/server/src/tests/core/util/test_net.py b/server/src/tests/core/util/test_net.py index 9b2339244..6abd394a9 100644 --- a/server/src/tests/core/util/test_net.py +++ b/server/src/tests/core/util/test_net.py @@ -41,33 +41,58 @@ logger = logging.getLogger(__name__) class NetTest(UDSTestCase): - - def testNetworkFromString(self): + def testNetworkFromStringIPv4(self): for n in ( - ('*', 0, 4294967295), - ('192.168.0.1', 3232235521, 3232235521), - ('192.168.0.*', 3232235520, 3232235775), - ('192.168.*.*', 3232235520, 3232301055), - ('192.168.*', 3232235520, 3232301055), - ('192.*.*.*', 3221225472, 3238002687), - ('192.*.*', 3221225472, 3238002687), - ('192.*', 3221225472, 3238002687), - ('192.168.0.1 netmask 255.255.255.0', 3232235520, 3232235775), - ('192.168.0.1/8', 3221225472, 3238002687), - ('192.168.0.1/28', 3232235520, 3232235535), - ('192.168.0.1-192.168.0.87', 3232235521, 3232235607), - ('192.168.0.1 netmask 255.255.255.0', 3232235520, 3232235775), - ): + ('*', 0, 4294967295), + ('192.168.0.1', 3232235521, 3232235521), + ('192.168.0.*', 3232235520, 3232235775), + ('192.168.*.*', 3232235520, 3232301055), + ('192.168.*', 3232235520, 3232301055), + ('192.*.*.*', 3221225472, 3238002687), + ('192.*.*', 3221225472, 3238002687), + ('192.*', 3221225472, 3238002687), + ('192.168.0.1 netmask 255.255.255.0', 3232235520, 3232235775), + ('192.168.0.1/8', 3221225472, 3238002687), + ('192.168.0.1/28', 3232235520, 3232235535), + ('192.168.0.1-192.168.0.87', 3232235521, 3232235607), + ('192.168.0.1 netmask 255.255.255.0', 3232235520, 3232235775), + ): try: - multiple_net: typing.List[net.NetworkType] = net.networksFromString(n[0]) - self.assertEqual(len(multiple_net), 1, 'Incorrect number of network returned from {0}'.format(n[0])) - self.assertEqual(multiple_net[0][0], n[1], 'Incorrect network start value for {0}'.format(n[0])) - self.assertEqual(multiple_net[0][1], n[2], 'Incorrect network end value for {0}'.format(n[0])) + multiple_net: typing.List[net.NetworkType] = net.networksFromString( + n[0] + ) + self.assertEqual( + len(multiple_net), + 1, + 'Incorrect number of network returned from {0}'.format(n[0]), + ) + self.assertEqual( + multiple_net[0][0], + n[1], + 'Incorrect network start value for {0}'.format(n[0]), + ) + self.assertEqual( + multiple_net[0][1], + n[2], + 'Incorrect network end value for {0}'.format(n[0]), + ) single_net: net.NetworkType = net.networkFromString(n[0]) - self.assertEqual(len(single_net), 2, 'Incorrect number of network returned from {0}'.format(n[0])) - self.assertEqual(single_net[0], n[1], 'Incorrect network start value for {0}'.format(n[0])) - self.assertEqual(single_net[1], n[2], 'Incorrect network end value for {0}'.format(n[0])) + self.assertEqual( + len(single_net), + 3, + 'Incorrect number of network returned from {0}'.format(n[0]), + ) + self.assertEqual( + single_net[0], + n[1], + 'Incorrect network start value for {0}'.format(n[0]), + ) + self.assertEqual( + single_net[1], + n[2], + 'Incorrect network end value for {0}'.format(n[0]), + ) except Exception as e: logger.exception('Running test') raise Exception('Value Error: {}. Input string: {}'.format(e, n[0])) @@ -76,12 +101,91 @@ class NetTest(UDSTestCase): with self.assertRaises(ValueError): net.networksFromString(n) - self.assertEqual(net.ipToLong('192.168.0.5'), 3232235525) - self.assertEqual(net.longToIp(3232235525), '192.168.0.5') + self.assertEqual(net.ipToLong('192.168.0.5').ip, 3232235525) + self.assertEqual(net.longToIp(3232235525, 4), '192.168.0.5') for n in range(0, 255): self.assertTrue(net.ipInNetwork('192.168.0.{}'.format(n), '192.168.0.0/24')) for n in range(4294): - self.assertTrue(net.ipInNetwork(n*1000, [net.NetworkType(0, 4294967295)])) - self.assertTrue(net.ipInNetwork(n*1000, net.NetworkType(0, 4294967295))) + self.assertTrue( + net.ipInNetwork(n * 1000, [net.NetworkType(0, 4294967295, 4)]) + ) + self.assertTrue( + net.ipInNetwork(n * 1000, net.NetworkType(0, 4294967295, 4)) + ) + def testNetworkFromStringIPv6(self): + # IPv6 only support standard notation, and '*', but not "netmask" or "range" + for n in ( + ( + '*', + 0, + 2**128 - 1, + ), # This could be confused with ipv4 *, so we take care + ( + '2001:db8::1', + 42540766411282592856903984951653826561, + 42540766411282592856903984951653826561, + ), + ( + '2001:db8::1/64', + 42540766411282592856903984951653826560, + 42540766411282592875350729025363378175, + ), + ( + '2001:db8::1/28', + 42540765777457292742789284203302223872, + 42540767045107892971018685700005429247, + ), + ( + '2222:3333:4444:5555:6666:7777:8888:9999/64', + 45371328414530988873481865147602436096, + 45371328414530988891928609221311987711, + ), + ( + 'fe80::/10', + 33828852492726108965401889684134769408, + 33828852492726108965401889684134769408 + 2**118 - 1, + ), + ): + try: + multiple_net: typing.List[net.NetworkType] = net.networksFromString( + n[0], version=(6 if n[0] == '*' else 0) + ) + self.assertEqual( + len(multiple_net), + 1, + 'Incorrect number of network returned from {0}'.format(n[0]), + ) + self.assertEqual( + multiple_net[0][0], + n[1], + 'Incorrect network start value for {0}'.format(n[0]), + ) + self.assertEqual( + multiple_net[0][1], + n[2], + 'Incorrect network end value for {0}'.format(n[0]), + ) + + single_net: net.NetworkType = net.networkFromString( + n[0], version=(6 if n[0] == '*' else 0) + ) + self.assertEqual( + len(single_net), + 3, + 'Incorrect number of network returned from {0}'.format(n[0]), + ) + self.assertEqual( + single_net[0], + n[1], + 'Incorrect network start value for {0}'.format(n[0]), + ) + self.assertEqual( + single_net[1], + n[2], + 'Incorrect network end value for {0}'.format(n[0]), + ) + except Exception as e: + logger.exception('Running test') + raise Exception('Value Error: {}. Input string: {}'.format(e, n[0])) diff --git a/server/src/uds/REST/methods/actor_v3.py b/server/src/uds/REST/methods/actor_v3.py index 50b1eb24f..14aad801f 100644 --- a/server/src/uds/REST/methods/actor_v3.py +++ b/server/src/uds/REST/methods/actor_v3.py @@ -229,6 +229,7 @@ class Register(ActorV3Action): username=self._user.pretty_name, ip_from=self._request.ip, ip=self._params['ip'], + ip_version=self._request.ip_version, hostname=self._params['hostname'], mac=self._params['mac'], pre_command=self._params['pre_command'], diff --git a/server/src/uds/REST/methods/networks.py b/server/src/uds/REST/methods/networks.py index 6317ba12f..ad6074174 100644 --- a/server/src/uds/REST/methods/networks.py +++ b/server/src/uds/REST/methods/networks.py @@ -67,21 +67,23 @@ class Networks(ModelHandler): } }, {'net_string': {'title': _('Range')}}, - {'transports_count': {'title': _('Transports'), 'type': 'numeric', 'width': '8em'}}, - {'authenticators_count': {'title': _('Authenticators'), 'type': 'numeric', 'width': '8em'}}, + { + 'transports_count': { + 'title': _('Transports'), + 'type': 'numeric', + 'width': '8em', + } + }, + { + 'authenticators_count': { + 'title': _('Authenticators'), + 'type': 'numeric', + 'width': '8em', + } + }, {'tags': {'title': _('tags'), 'visible': False}}, ] - def beforeSave(self, fields: typing.Dict[str, typing.Any]) -> None: - logger.debug('Before %s', fields) - try: - nr = net.networkFromString(fields['net_string']) - fields['net_start'] = nr[0] - fields['net_end'] = nr[1] - except Exception as e: - raise SaveException(gettext('Invalid network: {}').format(e)) - logger.debug('Processed %s', fields) - def getGui(self, type_: str) -> typing.List[typing.Any]: return self.addField( self.addDefaultFields([], ['name', 'tags']), diff --git a/server/src/uds/REST/methods/tunnel.py b/server/src/uds/REST/methods/tunnel.py index 71c956825..226eef1c3 100644 --- a/server/src/uds/REST/methods/tunnel.py +++ b/server/src/uds/REST/methods/tunnel.py @@ -116,11 +116,11 @@ class TunnelTicket(Handler): received=recv, tunnel=extra.get('t', 'unknown'), ) - except Exception: - pass + except Exception as e: + logger.warning('Error logging tunnel close event: %s', e) else: - if net.ipToLong(self._args[1][:32]) == 0: + if net.ipToLong(self._args[1][:32]).version == 0: raise Exception('Invalid from IP') events.addEvent( userService.deployed_service, diff --git a/server/src/uds/core/util/middleware/request.py b/server/src/uds/core/util/middleware/request.py index 52b62e926..76fb63d46 100644 --- a/server/src/uds/core/util/middleware/request.py +++ b/server/src/uds/core/util/middleware/request.py @@ -162,6 +162,9 @@ class GlobalRequestMiddleware: request.ip_proxy = proxies[1] if len(proxies) > 1 else request.ip logger.debug('Behind a proxy is active') + # Check if ip are ipv6 and set version field + request.ip_version = 6 if ':' in request.ip else 4 + logger.debug('ip: %s, ip_proxy: %s', request.ip, request.ip_proxy) @staticmethod diff --git a/server/src/uds/core/util/net.py b/server/src/uds/core/util/net.py index 33d2fc95b..e14561362 100644 --- a/server/src/uds/core/util/net.py +++ b/server/src/uds/core/util/net.py @@ -34,60 +34,63 @@ import re import socket import logging import typing +import ipaddress +import enum + +class IpType(typing.NamedTuple): + ip: int + version: typing.Literal[4, 6, 0] # 0 is only used for invalid detected ip class NetworkType(typing.NamedTuple): start: int end: int + version: typing.Literal[4, 6] # 4 or 6 + logger = logging.getLogger(__name__) -# Test patters for networks -reCIDR = re.compile( +# Test patters for networks IPv4 +reCIDRIPv4 = re.compile( r'^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/([0-9]{1,2})$' ) -reMask = re.compile( +reMaskIPv4 = re.compile( r'^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})netmask([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$' ) -re1Asterisk = re.compile(r'^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.\*$') -re2Asterisk = re.compile(r'^([0-9]{1,3})\.([0-9]{1,3})\.\*\.?\*?$') -re3Asterisk = re.compile(r'^([0-9]{1,3})\.\*\.?\*?\.?\*?$') -reRange = re.compile( +re1AsteriskIPv4 = re.compile(r'^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.\*$') +re2AsteriskIPv4 = re.compile(r'^([0-9]{1,3})\.([0-9]{1,3})\.\*\.?\*?$') +re3AsteriskIPv4 = re.compile(r'^([0-9]{1,3})\.\*\.?\*?\.?\*?$') +reRangeIPv4 = re.compile( r'^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})-([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$' ) -reHost = re.compile(r'^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$') +reSingleIPv4 = re.compile(r'^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$') -def ipToLong(ip: str) -> int: +def ipToLong(ip: str) -> IpType: """ - convert decimal dotted quad string to long integer + Convert an ipv4 or ipv6 address to its long representation """ + # First, check if it's an ipv6 address try: - hexn = int(''.join(["%02X" % int(i) for i in ip.split('.')]), 16) - logger.debug('IP %s is %s', ip, hexn) - return hexn + if ':' in ip: + return IpType(int(ipaddress.IPv6Address(ip)), 6) + else: # ipv4 + return IpType(int(ipaddress.IPv4Address(ip)), 4) except Exception as e: logger.error('Ivalid value: %s (%s)', ip, e) - return 0 # Invalid values will map to "0.0.0.0" --> 0 + return IpType(0, 0) # Invalid values will map to "0.0.0.0" --> 0 -def longToIp(n: int) -> str: +def longToIp(n: int, version: typing.Literal[0, 4, 6] = 0) -> str: """ - convert long int to dotted quad string + convert long int to ipv4 or ipv6 address, depending on size """ - try: - d = 1 << 24 - q = [] - while d > 0: - m, n = divmod(n, d) - q.append(str(m)) # As m is an integer, this works on py2 and p3 correctly - d >>= 8 - - return '.'.join(q) - except Exception: - return '0.0.0.0' # nosec: Invalid values will map to "0.0.0.0" + if n > 2**32 or version == 6: + return str(ipaddress.IPv6Address(n)) + else: + return str(ipaddress.IPv4Address(n)) -def networkFromString(strNets: str) -> NetworkType: +def networkFromStringIPv4(strNets: str, version: typing.Literal[0, 4, 6] = 0) -> NetworkType: ''' Parses the network from strings in this forms: - A.* (or A.*.* or A.*.*.*) @@ -99,7 +102,7 @@ def networkFromString(strNets: str) -> NetworkType: - A.B.C.D returns a named tuple with networks start and network end ''' - + inputString = strNets logger.debug('Getting network from %s', strNets) @@ -125,11 +128,11 @@ def networkFromString(strNets: str) -> NetworkType: strNets = strNets.replace(' ', '') if strNets == '*': - return NetworkType(0, 4294967295) + return NetworkType(0, 2**32 - 1, 4) try: # Test patterns - m = reCIDR.match(strNets) + m = reCIDRIPv4.match(strNets) if m is not None: logger.debug('Format is CIDR') check(*m.groups()) @@ -139,18 +142,18 @@ def networkFromString(strNets: str) -> NetworkType: val = toNum(*m.groups()) bits = maskFromBits(bits) noBits = ~bits & 0xFFFFFFFF - return NetworkType(val & bits, val | noBits) + return NetworkType(val & bits, val | noBits, 4) - m = reMask.match(strNets) + m = reMaskIPv4.match(strNets) if m is not None: logger.debug('Format is network mask') check(*m.groups()) val = toNum(*(m.groups()[0:4])) bits = toNum(*(m.groups()[4:8])) noBits = ~bits & 0xFFFFFFFF - return NetworkType(val & bits, val | noBits) + return NetworkType(val & bits, val | noBits, 4) - m = reRange.match(strNets) + m = reRangeIPv4.match(strNets) if m is not None: logger.debug('Format is network range') check(*m.groups()) @@ -158,23 +161,23 @@ def networkFromString(strNets: str) -> NetworkType: val2 = toNum(*(m.groups()[4:8])) if val2 < val: raise Exception() - return NetworkType(val, val2) + return NetworkType(val, val2, 4) - m = reHost.match(strNets) + m = reSingleIPv4.match(strNets) if m is not None: logger.debug('Format is a single host') check(*m.groups()) val = toNum(*m.groups()) - return NetworkType(val, val) + return NetworkType(val, val, 4) - for v in ((re1Asterisk, 3), (re2Asterisk, 2), (re3Asterisk, 1)): + for v in ((re1AsteriskIPv4, 3), (re2AsteriskIPv4, 2), (re3AsteriskIPv4, 1)): m = v[0].match(strNets) if m is not None: check(*m.groups()) val = toNum(*(m.groups()[0 : v[1] + 1])) bits = maskFromBits(v[1] * 8) noBits = ~bits & 0xFFFFFFFF - return NetworkType(val & bits, val | noBits) + return NetworkType(val & bits, val | noBits, 4) # No pattern recognized, invalid network raise Exception() @@ -183,8 +186,38 @@ def networkFromString(strNets: str) -> NetworkType: raise ValueError(inputString) +def networkFromStringIPv6(strNets: str, version: typing.Literal[0, 4, 6] = 0) -> NetworkType: + ''' + returns a named tuple with networks start and network end + ''' + logger.debug('Getting network from %s', strNets) + + # if '*' or '::*', return the whole IPv6 range + if strNets == '*' or strNets == '::*': + return NetworkType(0, 2**128 - 1, 6) + + try: + # using ipaddress module + net = ipaddress.ip_network(strNets, strict=False) + return NetworkType(int(net.network_address), int(net.broadcast_address), 6) + except Exception as e: + logger.error('Invalid network found: %s %s', strNets, e) + raise ValueError(strNets) + + +def networkFromString( + strNets: str, + version: typing.Literal[0, 4, 6] = 0, +) -> NetworkType: + if not ':' in strNets and version != 6: + return networkFromStringIPv4(strNets, version) + else: # ':' in strNets or version == 6: + return networkFromStringIPv6(strNets, version) + + def networksFromString( - strNets: str + strNets: str, + version: typing.Literal[0, 4, 6] = 0, ) -> typing.List[NetworkType]: """ If allowMultipleNetworks is True, it allows ',' and ';' separators (and, ofc, more than 1 network) @@ -192,35 +225,39 @@ def networksFromString( """ res = [] for strNet in re.split('[;,]', strNets): - if strNet != '': - res.append(typing.cast(NetworkType, networkFromString(strNet))) + if strNet: + res.append(networkFromString(strNet, version)) return res def ipInNetwork( - ip: typing.Union[str, int], network: typing.Union[str, NetworkType, typing.List[NetworkType]] + ip: typing.Union[str, int], + networks: typing.Union[str, NetworkType, typing.List[NetworkType]], + version: typing.Literal[0, 4, 6] = 0, ) -> bool: if isinstance(ip, str): - ip = ipToLong(ip) - if isinstance(network, str): - network = networksFromString(network) - elif isinstance(network, NetworkType): - network = [network] + ip, version = ipToLong(ip) # Ip overrides protocol version + if isinstance(networks, str): + if networks == '*': + return True # All IPs are in the * network + networks = networksFromString(networks, version) + elif isinstance(networks, NetworkType): + networks = [networks] - for net in network: - if net[0] <= ip <= net[1]: + # Ensure that the IP is in the same family as the network on checks + for net in networks: + if net.start <= ip <= net.end: return True return False -def isValidIp(value: str) -> bool: - return ( - re.match( - r'^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$', - value, - ) - is not None - ) +def isValidIp(value: str, version: typing.Literal[0, 4, 6] = 0) -> bool: + # Using ipaddress module + try: + addr = ipaddress.ip_address(value) + return version == 0 or addr.version == version + except ValueError: + return False def isValidFQDN(value: str) -> bool: diff --git a/server/src/uds/core/util/request.py b/server/src/uds/core/util/request.py index 3dfb0f74c..837636979 100644 --- a/server/src/uds/core/util/request.py +++ b/server/src/uds/core/util/request.py @@ -41,6 +41,7 @@ logger = logging.getLogger(__name__) class ExtendedHttpRequest(HttpRequest): ip: str + ip_version: int ip_proxy: str os: DictAsObj user: typing.Optional[User] diff --git a/server/src/uds/migrations/0044_notification_notifier_servicetokenalias_and_more.py b/server/src/uds/migrations/0044_notification_notifier_servicetokenalias_and_more.py index b078c01be..3bea8f406 100644 --- a/server/src/uds/migrations/0044_notification_notifier_servicetokenalias_and_more.py +++ b/server/src/uds/migrations/0044_notification_notifier_servicetokenalias_and_more.py @@ -1,5 +1,4 @@ -# Generated by Django 4.1.3 on 2022-11-12 21:03 -import logging +# Generated by Django 4.1.3 on 2022-11-29 02:37 from django.db import migrations, models import django.db.models.deletion @@ -8,21 +7,33 @@ import uds.models.notifications import uds.models.user_service_session import uds.models.util -logger = logging.getLogger('uds') -def remove_servicepool_with_null_service(apps, schema_editor): - """ In fact, there should be no Service Pool with no service, but we have found some in the wild""" +# Remove ServicePools with null service field +def remove_null_service_pools(apps, schema_editor): ServicePool = apps.get_model('uds', 'ServicePool') - # Log in the django.db.backends logger removed services - logger.info('Removing ServicePools with null service') - for i in ServicePool.objects.filter(service=None): - logger.info(' * Removing ServicePool %s - %s', i.uuid, i.name) - i.delete() + ServicePool.objects.filter(service__isnull=True).delete() -def null_backwards(apps, schema_editor): - # Remove null services backwards is not possible, we have deleted them +# No-Op backwards migration +def nop(apps, schema_editor): # pragma: no cover pass + +# Python update network fields to allow ipv6 +# We will +def update_network_model(apps, schema_editor): + import uds.models.network + Network = apps.get_model('uds', 'Network') + try: + for net in Network.objects.all(): + # Store the net_start and net_end on new fields "start" and "end", that are strings + # to allow us to store ipv6 addresses + net.start = uds.models.network.Network._hexlify(net.net_start) + net.end = uds.models.network.Network._hexlify(net.net_end) + net.version = 4 # Previous versions only supported ipv4 + net.save() + except Exception as e: + print('Error updating network model: {}'.format(e)) + class Migration(migrations.Migration): dependencies = [ @@ -30,8 +41,7 @@ class Migration(migrations.Migration): ] operations = [ - # First, we remove all DeployedServices with null service because we have fixed foreign key - migrations.RunPython(remove_servicepool_with_null_service, null_backwards), + migrations.RunPython(remove_null_service_pools, nop), migrations.CreateModel( name="Notification", fields=[ @@ -137,6 +147,10 @@ class Migration(migrations.Migration): migrations.DeleteModel( name="DBFile", ), + migrations.RemoveField( + model_name="userpreference", + name="user", + ), migrations.RemoveField( model_name="authenticator", name="visible", @@ -153,6 +167,11 @@ class Migration(migrations.Migration): model_name="userservice", name="cluster_node", ), + migrations.AddField( + model_name="actortoken", + name="ip_version", + field=models.IntegerField(default=4), + ), migrations.AddField( model_name="authenticator", name="net_filtering", @@ -182,6 +201,31 @@ class Migration(migrations.Migration): to="uds.authenticator", ), ), + migrations.AddField( + model_name="network", + name="end", + field=models.CharField(db_index=True, default="0", max_length=40), + ), + migrations.AddField( + model_name="network", + name="start", + field=models.CharField(db_index=True, default="0", max_length=40), + ), + migrations.AddField( + model_name="network", + name="version", + field=models.IntegerField(default=4), + ), + # Run python code to update network model + migrations.RunPython(update_network_model, nop), + migrations.RemoveField( + model_name="network", + name="net_end", + ), + migrations.RemoveField( + model_name="network", + name="net_start", + ), migrations.AddField( model_name="service", name="max_services_count_type", @@ -211,6 +255,21 @@ class Migration(migrations.Migration): default=uds.core.util.model.generateUuid, max_length=50, unique=True ), ), + migrations.AlterField( + model_name="actortoken", + name="hostname", + field=models.CharField(max_length=255), + ), + migrations.AlterField( + model_name="actortoken", + name="ip", + field=models.CharField(max_length=45), + ), + migrations.AlterField( + model_name="actortoken", + name="ip_from", + field=models.CharField(max_length=45), + ), migrations.AlterField( model_name="authenticator", name="uuid", @@ -288,6 +347,11 @@ class Migration(migrations.Migration): default=uds.core.util.model.generateUuid, max_length=50, unique=True ), ), + migrations.AlterField( + model_name="network", + name="net_string", + field=models.CharField(default="", max_length=240), + ), migrations.AlterField( model_name="network", name="uuid", @@ -316,6 +380,11 @@ class Migration(migrations.Migration): default=uds.core.util.model.generateUuid, max_length=50, unique=True ), ), + migrations.AlterField( + model_name="scheduler", + name="owner_server", + field=models.CharField(db_index=True, default="", max_length=255), + ), migrations.AlterField( model_name="service", name="token", @@ -381,15 +450,20 @@ class Migration(migrations.Migration): default=uds.core.util.model.generateUuid, max_length=50, unique=True ), ), + migrations.AlterField( + model_name="tunneltoken", + name="hostname", + field=models.CharField(max_length=255), + ), migrations.AlterField( model_name="tunneltoken", name="ip", - field=models.CharField(max_length=128), + field=models.CharField(max_length=45), ), migrations.AlterField( model_name="tunneltoken", name="ip_from", - field=models.CharField(max_length=128), + field=models.CharField(max_length=45), ), migrations.AlterField( model_name="user", @@ -398,10 +472,15 @@ class Migration(migrations.Migration): default=uds.core.util.model.generateUuid, max_length=50, unique=True ), ), + migrations.AlterField( + model_name="userservice", + name="src_hostname", + field=models.CharField(default="", max_length=255), + ), migrations.AlterField( model_name="userservice", name="src_ip", - field=models.CharField(default="", max_length=128), + field=models.CharField(default="", max_length=45), ), migrations.AlterField( model_name="userservice", @@ -413,6 +492,9 @@ class Migration(migrations.Migration): migrations.DeleteModel( name="Proxy", ), + migrations.DeleteModel( + name="UserPreference", + ), migrations.AddField( model_name="userservicesession", name="user_service", diff --git a/server/src/uds/models/__init__.py b/server/src/uds/models/__init__.py index 680e76274..91e43a873 100644 --- a/server/src/uds/models/__init__.py +++ b/server/src/uds/models/__init__.py @@ -55,7 +55,6 @@ from .network import Network # Authenticators from .authenticator import Authenticator from .user import User -from .user_preference import UserPreference from .group import Group # Provisioned services diff --git a/server/src/uds/models/actor_token.py b/server/src/uds/models/actor_token.py index 1921807a0..9ed89d7fa 100644 --- a/server/src/uds/models/actor_token.py +++ b/server/src/uds/models/actor_token.py @@ -30,6 +30,7 @@ ''' from django.db import models +from .util import MAX_IPV6_LENGTH, MAX_DNS_NAME_LENGTH class ActorToken(models.Model): """ @@ -37,9 +38,11 @@ class ActorToken(models.Model): """ username = models.CharField(max_length=128) - ip_from = models.CharField(max_length=128) - ip = models.CharField(max_length=128) - hostname = models.CharField(max_length=128) + ip_from = models.CharField(max_length=MAX_IPV6_LENGTH) + ip = models.CharField(max_length=MAX_IPV6_LENGTH) + ip_version = models.IntegerField(default=4) # Version of ip fields + + hostname = models.CharField(max_length=MAX_DNS_NAME_LENGTH) mac = models.CharField(max_length=128, db_index=True, unique=True) pre_command = models.CharField(max_length=255, blank=True, default='') post_command = models.CharField(max_length=255, blank=True, default='') diff --git a/server/src/uds/models/authenticator.py b/server/src/uds/models/authenticator.py index 6bb209bde..676b42000 100644 --- a/server/src/uds/models/authenticator.py +++ b/server/src/uds/models/authenticator.py @@ -224,10 +224,10 @@ class Authenticator(ManagedObjectModel, TaggingMixin): """ if self.net_filtering == Authenticator.NO_FILTERING: return True - ip = net.ipToLong(ipStr) + ip, version = net.ipToLong(ipStr) # Allow if self.net_filtering == Authenticator.ALLOW: - return self.networks.filter(net_start__lte=ip, net_end__gte=ip).exists() + return self.networks.filter(net_start__lte=ip, net_end__gte=ip, version=version).exists() # Deny, must not be in any network return self.networks.filter(net_start__lte=ip, net_end__gte=ip).exists() is False diff --git a/server/src/uds/models/network.py b/server/src/uds/models/network.py index 5e6fae341..bc0c3f085 100644 --- a/server/src/uds/models/network.py +++ b/server/src/uds/models/network.py @@ -52,9 +52,16 @@ class Network(UUIDModel, TaggingMixin): # type: ignore """ name = models.CharField(max_length=64, unique=True) - net_start = models.BigIntegerField(db_index=True) - net_end = models.BigIntegerField(db_index=True) - net_string = models.CharField(max_length=128, default='') + + start = models.CharField( + max_length=32, default='0', db_index=True + ) # 128 bits, for IPv6, network byte order, hex + end = models.CharField( + max_length=32, default='0', db_index=True + ) # 128 bits, for IPv6, network byte order, hex + + version = models.IntegerField(default=4) # network type, ipv4 or ipv6 + net_string = models.CharField(max_length=240, default='') transports = models.ManyToManyField( Transport, related_name='networks', db_table='uds_net_trans' ) @@ -73,29 +80,87 @@ class Network(UUIDModel, TaggingMixin): # type: ignore ordering = ('name',) app_label = 'uds' + @staticmethod + def _hexlify(number: int) -> str: + """ + Converts a number to hex, but with 32 chars, and with leading zeros + """ + return '{:032x}'.format(number) + + @staticmethod + def _unhexlify(number: str) -> int: + """ + Converts a hex string to a number + """ + return int(number, 16) + @staticmethod def networksFor(ip: str) -> typing.Iterable['Network']: """ Returns the networks that are valid for specified ip in dotted quad (xxx.xxx.xxx.xxx) """ - ipInt = net.ipToLong(ip) - return Network.objects.filter(net_start__lte=ipInt, net_end__gte=ipInt) + ipInt, version = net.ipToLong(ip) + hex_value = Network._hexlify(ipInt) + # hexlify is used to convert to hex, and then decode to convert to string + return Network.objects.filter( + version=version, + start__lte=hex_value, + end__gte=hex_value, + ) @staticmethod def create(name: str, netRange: str) -> 'Network': """ - Creates an network record, with the specified net start and net end (dotted quad) + Creates an network record, with the specified network range. Supports IPv4 and IPv6 + IPV4 has a versatile format, that can be: + - A single IP + - A range of IPs, in the form of "startIP - endIP" + - A network, in the form of "network/mask" + - A network, in the form of "network netmask mask" + - A network, in the form of "network*' Args: - netStart: Network start + name: Name of the network + netRange: Network range in any supported format - netEnd: Network end """ nr = net.networkFromString(netRange) return Network.objects.create( - name=name, net_start=nr[0], net_end=nr[1], net_string=netRange + name=name, + start=Network._hexlify(nr.start), + end=Network._hexlify(nr.end), + net_string=netRange, + version=nr.version, ) + @property + def net_start(self) -> int: + """ + Returns the network start as an integer + """ + return Network._unhexlify(self.start) + + @net_start.setter + def net_start(self, value: int) -> None: + """ + Sets the network start + """ + self.start = Network._hexlify(value) + + @property + def net_end(self) -> int: + """ + Returns the network end as an integer + """ + return Network._unhexlify(self.end) + + @net_end.setter + def net_end(self, value: int) -> None: + """ + Sets the network end + """ + self.end = Network._hexlify(value) + @property def netStart(self) -> str: """ @@ -120,7 +185,21 @@ class Network(UUIDModel, TaggingMixin): # type: ignore """ Returns true if the specified ip is in this network """ - return net.ipToLong(ip) >= self.net_start and net.ipToLong(ip) <= self.net_end + # if net_string is '*', then we are in all networks, return true + if self.net_string == '*': + return True + ipInt, version = net.ipToLong(ip) + return self.net_start <= ipInt <= self.net_end and self.version == version + + def save(self, *args, **kwargs) -> None: + """ + Overrides save to update the start, end and version fields + """ + rng = net.networkFromString(self.net_string) + self.start = Network._hexlify(rng.start) + self.end = Network._hexlify(rng.end) + self.version = rng.version + super().save(*args, **kwargs) def update(self, name: str, netRange: str): """ @@ -134,18 +213,16 @@ class Network(UUIDModel, TaggingMixin): # type: ignore netEnd: new Network end (quad dotted) """ self.name = name - nr = net.networkFromString(netRange) - self.net_start = nr[0] - self.net_end = nr[1] self.net_string = netRange self.save() def __str__(self) -> str: - return u'Network {} ({}) from {} to {}'.format( + return u'Network {} ({}) from {} to {} ({})'.format( self.name, self.net_string, net.longToIp(self.net_start), net.longToIp(self.net_end), + self.version, ) @staticmethod diff --git a/server/src/uds/models/scheduler.py b/server/src/uds/models/scheduler.py index 6d0870843..8fc75d866 100644 --- a/server/src/uds/models/scheduler.py +++ b/server/src/uds/models/scheduler.py @@ -39,7 +39,7 @@ from uds.core.util.state import State from uds.core.environment import Environment from uds.core import jobs -from .util import NEVER +from .util import NEVER, MAX_DNS_NAME_LENGTH logger = logging.getLogger(__name__) @@ -67,7 +67,7 @@ class Scheduler(models.Model): frecuency = models.PositiveIntegerField(default=DAY) last_execution = models.DateTimeField(db_index=True) next_execution = models.DateTimeField(default=NEVER, db_index=True) - owner_server = models.CharField(max_length=64, db_index=True, default='') + owner_server = models.CharField(max_length=MAX_DNS_NAME_LENGTH, db_index=True, default='') state = models.CharField(max_length=1, default=State.FOR_EXECUTE, db_index=True) # primary key id declaration (for type checking) diff --git a/server/src/uds/models/transport.py b/server/src/uds/models/transport.py index b32575718..a2e88a687 100644 --- a/server/src/uds/models/transport.py +++ b/server/src/uds/models/transport.py @@ -124,10 +124,10 @@ class Transport(ManagedObjectModel, TaggingMixin): """ if self.net_filtering == Transport.NO_FILTERING: return True - ip = net.ipToLong(ipStr) + ip, version = net.ipToLong(ipStr) # Allow if self.net_filtering == Transport.ALLOW: - return self.networks.filter(net_start__lte=ip, net_end__gte=ip).exists() + return self.networks.filter(net_start__lte=ip, net_end__gte=ip, version=version).exists() # Deny, must not be in any network return self.networks.filter(net_start__lte=ip, net_end__gte=ip).exists() is False diff --git a/server/src/uds/models/tunnel_token.py b/server/src/uds/models/tunnel_token.py index febf6390c..2e17fd206 100644 --- a/server/src/uds/models/tunnel_token.py +++ b/server/src/uds/models/tunnel_token.py @@ -33,6 +33,8 @@ import typing from django.db import models from uds.core.util.request import ExtendedHttpRequest +from .util import MAX_DNS_NAME_LENGTH, MAX_IPV6_LENGTH + class TunnelToken(models.Model): """ @@ -40,9 +42,9 @@ class TunnelToken(models.Model): """ username = models.CharField(max_length=128) - ip_from = models.CharField(max_length=128) - ip = models.CharField(max_length=128) - hostname = models.CharField(max_length=128) + ip_from = models.CharField(max_length=MAX_IPV6_LENGTH) + ip = models.CharField(max_length=MAX_IPV6_LENGTH) + hostname = models.CharField(max_length=MAX_DNS_NAME_LENGTH) token = models.CharField(max_length=48, db_index=True, unique=True) stamp = models.DateTimeField() # Date creation or validation of this entry diff --git a/server/src/uds/models/user_preference.py b/server/src/uds/models/user_preference.py deleted file mode 100644 index 88baed87a..000000000 --- a/server/src/uds/models/user_preference.py +++ /dev/null @@ -1,62 +0,0 @@ -# -*- coding: utf-8 -*- - -# -# Copyright (c) 2012-2020 Virtual Cable S.L.U. -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * Neither the name of Virtual Cable S.L. nor the names of its contributors -# may be used to endorse or promote products derived from this software -# without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -""" -.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com -""" -import logging - -from django.db import models - -from .user import User - - -logger = logging.getLogger(__name__) - - -class UserPreference(models.Model): - """ - This class represents a single user preference for an user and a module - """ - - module = models.CharField(max_length=32, db_index=True) - name = models.CharField(max_length=32, db_index=True) - value = models.CharField(max_length=128, db_index=True) - user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='preferences') - - # "fake" declarations for type checking - # objects: 'models.manager.Manager[UserPreference]' - - class Meta: - app_label = 'uds' - - def __str__(self) -> str: - return '{}.{} = "{}" for user {}'.format( - self.module, self.name, self.value, self.user - ) diff --git a/server/src/uds/models/user_service.py b/server/src/uds/models/user_service.py index 68d28bd8f..7dd2eff90 100644 --- a/server/src/uds/models/user_service.py +++ b/server/src/uds/models/user_service.py @@ -36,7 +36,6 @@ import typing from django.db import models from django.db.models import signals -from uds.core.managers import cryptoManager from uds.core.environment import Environment from uds.core.util import log, unique from uds.core.util.state import State @@ -45,8 +44,7 @@ from uds.models.uuid_model import UUIDModel from uds.models.service_pool import ServicePool from uds.models.service_pool_publication import ServicePoolPublication from uds.models.user import User -from uds.models.util import NEVER -from uds.models.util import getSqlDatetime +from uds.models.util import NEVER, getSqlDatetime, MAX_IPV6_LENGTH, MAX_DNS_NAME_LENGTH # Not imported at runtime, just for type checking if typing.TYPE_CHECKING: @@ -113,8 +111,8 @@ class UserService(UUIDModel): # pylint: disable=too-many-public-methods db_index=True, default=0 ) # Cache level must be 1 for L1 or 2 for L2, 0 if it is not cached service - src_hostname = models.CharField(max_length=64, default='') - src_ip = models.CharField(max_length=128, default='') + src_hostname = models.CharField(max_length=MAX_DNS_NAME_LENGTH, default='') + src_ip = models.CharField(max_length=MAX_IPV6_LENGTH, default='') # Source IP of the user connecting to the service. Max length is 45 chars (ipv6) # "fake" declarations for type checking # objects: 'models.manager.Manager["UserService"]' diff --git a/server/src/uds/models/util.py b/server/src/uds/models/util.py index c5f04e236..98b46b60c 100644 --- a/server/src/uds/models/util.py +++ b/server/src/uds/models/util.py @@ -31,6 +31,7 @@ .. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com """ import logging +import typing from datetime import datetime from time import mktime @@ -38,9 +39,12 @@ from django.db import connection, models logger = logging.getLogger(__name__) -NEVER = datetime(1972, 7, 1) -NEVER_UNIX = int(mktime(NEVER.timetuple())) +NEVER: typing.Final[datetime] = datetime(1972, 7, 1) +NEVER_UNIX: typing.Final[int] = int(mktime(NEVER.timetuple())) +# Max ip v6 string length representation, allowing ipv4 mapped addresses +MAX_IPV6_LENGTH: typing.Final = 45 +MAX_DNS_NAME_LENGTH: typing.Final = 255 class UnsavedForeignKey(models.ForeignKey): """ diff --git a/server/src/uds/services/PhysicalMachines/deployment.py b/server/src/uds/services/PhysicalMachines/deployment.py index 4edc59506..6bc6a3897 100644 --- a/server/src/uds/services/PhysicalMachines/deployment.py +++ b/server/src/uds/services/PhysicalMachines/deployment.py @@ -75,15 +75,21 @@ class IPMachineDeployed(services.UserDeployment, AutoAttributes): # If multiple and has a ';' on IP, the values is IP;MAC ip = self._ip.split('~')[0].split(';')[0] # If ip is in fact a hostname... - if not net.ipToLong(ip): + if not net.ipToLong(ip).version: # Try to resolve name... try: + # Prefer ipv4 first res = dns.resolver.resolve(ip) ip = res[0].address except Exception: - self.service().parent().doLog( - log.WARN, f'User service could not resolve Name {ip}.' - ) + # If not found, try ipv6 + try: + res = dns.resolver.resolve(ip, 'AAAA') + ip = res[0].address + except Exception as e: + self.service().parent().doLog( + log.WARN, f'User service could not resolve Name {ip} ({e}).' + ) return ip diff --git a/server/src/uds/services/PhysicalMachines/provider.py b/server/src/uds/services/PhysicalMachines/provider.py index 02f1b90f5..9129e0c90 100644 --- a/server/src/uds/services/PhysicalMachines/provider.py +++ b/server/src/uds/services/PhysicalMachines/provider.py @@ -132,17 +132,22 @@ class PhysicalMachinesProvider(services.ServiceProvider): return '' # If ip is in fact a hostname... - if not net.ipToLong(ip): + if not net.ipToLong(ip).version: # Try to resolve name... try: + # Prefer ipv4 res = dns.resolver.resolve(ip) ip = res[0].address except Exception: - self.doLog(log.WARN, f'Name {ip} could not be resolved') - logger.warning('Name %s could not be resolved', ip) - return '' + # Try ipv6 + try: + res = dns.resolver.resolve(ip, 'AAAA') + ip = res[0].address + except Exception as e: + self.doLog(log.WARN, f'Name {ip} could not be resolved') + logger.warning('Name %s could not be resolved (%s)', ip, e) + return '' - url = '' try: config = configparser.ConfigParser() config.read_string(self.config.value) diff --git a/server/src/uds/services/PhysicalMachines/service_base.py b/server/src/uds/services/PhysicalMachines/service_base.py index 6db09668b..d71e89acb 100644 --- a/server/src/uds/services/PhysicalMachines/service_base.py +++ b/server/src/uds/services/PhysicalMachines/service_base.py @@ -71,13 +71,13 @@ class IPServiceBase(services.Service): def unassignMachine(self, ip: str) -> None: raise NotADirectoryError('unassignMachine') - def wakeup(self, ip: str, mac: typing.Optional[str]) -> None: + def wakeup(self, ip: str, mac: typing.Optional[str], verify_ssl: bool = False) -> None: if mac: wolurl = self.parent().wolURL(ip, mac) if wolurl: logger.info('Launching WOL: %s', wolurl) try: - requests.get(wolurl, verify=False) + requests.get(wolurl, verify=verify_ssl) # logger.debug('Result: %s', result) except Exception as e: logger.error('Error on WOL: %s', e) diff --git a/server/src/uds/services/PhysicalMachines/service_multi.py b/server/src/uds/services/PhysicalMachines/service_multi.py index 8d1c3278a..1e5dcf9d2 100644 --- a/server/src/uds/services/PhysicalMachines/service_multi.py +++ b/server/src/uds/services/PhysicalMachines/service_multi.py @@ -28,9 +28,9 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ -@author: Adolfo Gómez, dkmaster at dkmon dot com +Author: Adolfo Gómez, dkmaster at dkmon dot com """ -import pickle +import pickle # nosec # Pickle use is controled by app, never by non admin user input import logging import typing @@ -163,7 +163,7 @@ class IPMachinesService(IPServiceBase): ] # Allow duplicates right now # Current stored data, if it exists d = self.storage.readData('ips') - old_ips = pickle.loads(d) if d and isinstance(d, bytes) else [] + old_ips = pickle.loads(d) if d and isinstance(d, bytes) else [] # nosec: pickle is safe here # dissapeared ones dissapeared = set( IPServiceBase.getIp(i.split('~')[0]) for i in old_ips @@ -210,9 +210,9 @@ class IPMachinesService(IPServiceBase): values: typing.List[bytes] = data.split(b'\0') d = self.storage.readData('ips') if isinstance(d, bytes): - self._ips = pickle.loads(d) + self._ips = pickle.loads(d) # nosec: pickle is safe here elif isinstance(d, str): # "legacy" saved elements - self._ips = pickle.loads(d.encode('utf8')) + self._ips = pickle.loads(d.encode('utf8')) # nosec: pickle is safe here self.marshal() # Ensure now is bytes.. else: self._ips = []