1
0
mirror of https://github.com/dkmstr/openuds.git synced 2024-12-23 17:34:17 +03:00

fixing up network & more to support ipv6

This commit is contained in:
Adolfo Gómez García 2022-11-29 04:45:27 +01:00
parent 659013db56
commit 89e1c2eac5
No known key found for this signature in database
GPG Key ID: DD1ABF20724CDA23
22 changed files with 492 additions and 230 deletions

View File

@ -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]))

View File

@ -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'],

View File

@ -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']),

View File

@ -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,

View File

@ -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

View File

@ -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:

View File

@ -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]

View File

@ -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",

View File

@ -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

View File

@ -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='')

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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
)

View File

@ -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"]'

View File

@ -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):
"""

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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 = []