1
0
mirror of https://github.com/dkmstr/openuds.git synced 2025-01-21 18:03:54 +03:00

Advanced A LOT on new tunnel server & client. First test passed

This commit is contained in:
Adolfo Gómez García 2021-01-15 11:31:39 +01:00
parent 865601b3c8
commit f402dadb0a
23 changed files with 380 additions and 101 deletions

View File

@ -33,6 +33,7 @@ import socketserver
import ssl
import threading
import time
import random
import threading
import select
import typing
@ -48,6 +49,7 @@ TUNNEL_LISTENING, TUNNEL_OPENING, TUNNEL_PROCESSING, TUNNEL_ERROR = 0, 1, 2, 3
logger = logging.getLogger(__name__)
class ForwardServer(socketserver.ThreadingTCPServer):
daemon_threads = True
allow_reuse_address = True
@ -69,6 +71,10 @@ class ForwardServer(socketserver.ThreadingTCPServer):
local_port: int = 0,
check_certificate: bool = True,
) -> None:
if local_port == 0:
local_port = random.randrange(33000, 53000)
super().__init__(
server_address=(LISTEN_ADDRESS, local_port), RequestHandlerClass=Handler
)
@ -82,7 +88,9 @@ class ForwardServer(socketserver.ThreadingTCPServer):
self.status = TUNNEL_LISTENING
if timeout:
self.timer = threading.Timer(timeout, ForwardServer.__checkStarted, args=(self,))
self.timer = threading.Timer(
timeout, ForwardServer.__checkStarted, args=(self,)
)
self.timer.start()
else:
self.timer = None
@ -116,7 +124,7 @@ class Handler(socketserver.BaseRequestHandler):
self.server.current_connections += 1
self.server.status = TUNNEL_OPENING
# If server processing is over time
# If server processing is over time
if self.server.stoppable:
logger.info('Rejected timedout connection try')
self.request.close() # End connection without processing it
@ -134,7 +142,7 @@ class Handler(socketserver.BaseRequestHandler):
# If ignore remote certificate
if self.server.check_certificate is False:
context.check_hostname = False
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
logger.warning('Certificate checking is disabled!')
@ -149,7 +157,9 @@ class Handler(socketserver.BaseRequestHandler):
data = ssl_socket.recv(2)
if data != b'OK':
data += ssl_socket.recv(128)
raise Exception(f'Error received: {data.decode()}') # Notify error
raise Exception(
f'Error received: {data.decode()}'
) # Notify error
# All is fine, now we can tunnel data
self.process(remote=ssl_socket)
@ -185,10 +195,17 @@ class Handler(socketserver.BaseRequestHandler):
except Exception as e:
pass
def _run(server: ForwardServer) -> None:
logger.debug('Starting forwarder: %s -> %s, timeout: %d', server.server_address, server.remote, server.timeout)
logger.debug(
'Starting forwarder: %s -> %s, timeout: %d',
server.server_address,
server.remote,
server.timeout,
)
server.serve_forever()
logger.debug('Stoped forwarded %s -> %s', server.server_address, server.remote)
logger.debug('Stoped forwarder %s -> %s', server.server_address, server.remote)
def forward(
remote: typing.Tuple[str, int],
@ -197,6 +214,7 @@ def forward(
local_port: int = 0,
check_certificate=True,
) -> ForwardServer:
fs = ForwardServer(
remote=remote,
ticket=ticket,
@ -209,8 +227,10 @@ def forward(
return fs
if __name__ == "__main__":
import sys
log = logging.getLogger()
log.setLevel(logging.DEBUG)
handler = logging.StreamHandler(sys.stdout)
@ -221,4 +241,12 @@ if __name__ == "__main__":
handler.setFormatter(formatter)
log.addHandler(handler)
fs = forward(('172.27.0.1', 7777), '1'*64, local_port=49999, timeout=10, check_certificate=False)
ticket = 'qcdn2jax6tx4nljdyed61hm3iqbld5nf44zxbh9gf355ofw2'
fs = forward(
('172.27.0.1', 7777),
ticket,
local_port=49999,
timeout=60,
check_certificate=False,
)

View File

@ -154,7 +154,7 @@ class Handler:
"""
return self._headers
def header(self, headerName) -> typing.Optional[str]:
def header(self, headerName: str) -> typing.Optional[str]:
"""
Get's an specific header name from REST request
:param headerName: name of header to get

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014-2020 Virtual Cable S.L.U.
# Copyright (c) 2014-2021 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -50,13 +50,13 @@ from uds.core import VERSION as UDS_VERSION
logger = logging.getLogger(__name__)
CLIENT_VERSION = UDS_VERSION
REQUIRED_CLIENT_VERSION = '3.0.0'
REQUIRED_CLIENT_VERSION = '3.5.0'
# Enclosed methods under /actor path
# Enclosed methods under /client path
class Client(Handler):
"""
Processes actor requests
Processes Client requests
"""
authenticated = False # Client requests are not authenticated
@ -155,7 +155,7 @@ class Client(Handler):
return Client.result(result={
'script': transportScript,
'signature': signature, # It is already on base64
'params': codecs.encode(codecs.encode(json.dumps(params), 'bz2'), 'base64').decode(),
'params': codecs.encode(codecs.encode(json.dumps(params).encode(), 'bz2'), 'base64').decode(),
})
except ServiceNotReadyError as e:
# Refresh ticket and make this retrayable

View File

@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014-2021 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.
"""
@author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import logging
import typing
from uds.models.user_service import UserService
from uds import models
from uds.REST import Handler
from uds.REST import AccessDenied
from uds.core.auths.auth import isTrustedSource
from uds.core.util import log, net
from uds.core.util.stats import events
logger = logging.getLogger(__name__)
# Enclosed methods under /tunnel path
class Tunnel(Handler):
"""
Processes tunnel requests
"""
authenticated = False # Client requests are not authenticated
def get(self) -> typing.MutableMapping[str, typing.Any]:
"""
Processes get requests, currently none
"""
logger.debug(
'Tunnel parameters for GET: %s from %s', self._args, self._request.ip
)
if (
not isTrustedSource(self._request.ip)
or len(self._args) > 2
or len(self._args[0]) != 48
):
# Invalid requests
raise AccessDenied()
# Try to get ticket from DB
try:
user, userService, host, port, _ = models.TicketStore.get_for_tunnel(
self._args[0]
)
start = len(self._args) == 2 # Start requests include source IP request
if net.ipToLong(self._args[1][:32]) == 0:
raise Exception('Invalid from IP')
data = {}
if start:
events.addEvent(
userService.deployed_service,
events.ET_TUNNEL_ACCESS,
username=user.pretty_name,
srcip=self._args[1],
dstip=host,
uniqueid=userService.unique_id,
)
msg = f'User {user.name} started tunnel to {host}:{port} from {self._args[1]}.'
log.doLog(user.manager, log.INFO, msg)
log.doLog(userService, log.INFO, msg)
data = {'host': host, 'port': port}
return data
except Exception as e:
logger.info('Ticket ignored: %s', e)
raise AccessDenied()

View File

@ -154,6 +154,10 @@ def webLoginRequired(
return decorator
# Helper for checking if requests is from trusted source
def isTrustedSource(ip: str) -> bool:
return net.ipInNetwork(ip, GlobalConfig.TRUSTED_SOURCES.get(True))
# Decorator to protect pages that needs to be accessed from "trusted sites"
def trustedSourceRequired(
@ -170,7 +174,7 @@ def trustedSourceRequired(
Wrapped function for decorator
"""
try:
if not net.ipInNetwork(request.ip, GlobalConfig.TRUSTED_SOURCES.get(True)):
if not isTrustedSource(request.ip):
return HttpResponseForbidden() # type: ignore
except Exception as e:
logger.warning(

View File

@ -48,7 +48,7 @@ logger = logging.getLogger(__name__)
OT_USERSERVICE, OT_PUBLICATION, OT_DEPLOYED_SERVICE, OT_SERVICE, OT_PROVIDER, OT_USER, OT_GROUP, OT_AUTHENTICATOR, OT_METAPOOL = range(9) # @UndefinedVariable
# Dict for translations
transDict: typing.Dict['Model', int] = {
transDict: typing.Dict[typing.Type['Model'], int] = {
models.UserService: OT_USERSERVICE,
models.ServicePoolPublication: OT_PUBLICATION,
models.ServicePool: OT_DEPLOYED_SERVICE,

View File

@ -146,7 +146,6 @@ class Transport(Module):
Helper method to check if transport supports requested operating system.
Class method
"""
logger.debug('Checking suported os %s against %s', osName, cls.supportedOss)
return cls.supportedOss.count(osName) > 0
@classmethod
@ -242,7 +241,7 @@ class Transport(Module):
script, signature, params = self.getUDSTransportScript(userService, transport, ip, os, user, password, request)
logger.debug('Transport script: %s', script)
return codecs.encode(codecs.encode(script, 'bz2'), 'base64').decode().replace('\n', ''), signature, params
return codecs.encode(codecs.encode(script.encode(), 'bz2'), 'base64').decode().replace('\n', ''), signature, params
def getLink(
self,

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2015-2019 Virtual Cable S.L.
# Copyright (c) 2015-2021 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -32,6 +32,7 @@
"""
import datetime
import time
import typing
import logging
import bitarray
@ -61,10 +62,10 @@ class CalendarChecker:
cache = Cache('calChecker')
def __init__(self, calendar: Calendar):
def __init__(self, calendar: Calendar) -> None:
self.calendar = calendar
def _updateData(self, dtime: datetime.datetime):
def _updateData(self, dtime: datetime.datetime) -> bitarray.bitarray:
logger.debug('Updating %s', dtime)
# Else, update the array
CalendarChecker.updates += 1
@ -124,7 +125,6 @@ class CalendarChecker:
return data
def _updateEvents(self, checkFrom, startEvent=True):
next_event = None
for rule in self.calendar.rules.all():
# logger.debug('RULE: start = {}, checkFrom = {}, end'.format(rule.start.date(), checkFrom.date()))
@ -141,7 +141,7 @@ class CalendarChecker:
return next_event
def check(self, dtime=None):
def check(self, dtime=None) -> int:
"""
Checks if the given time is a valid event on calendar
@param dtime: Datetime object to check
@ -180,10 +180,9 @@ class CalendarChecker:
return data[dtime.hour * 60 + dtime.minute]
def nextEvent(self, checkFrom=None, startEvent=True, offset=None):
def nextEvent(self, checkFrom=None, startEvent=True, offset=None) -> typing.Optional[datetime.datetime]:
"""
Returns next event for this interval
Returns a list of two elements. First is datetime of event begining, second is timedelta of duration
"""
logger.debug('Obtaining nextEvent')
if checkFrom is None:
@ -215,5 +214,5 @@ class CalendarChecker:
return next_event
def debug(self):
def debug(self) -> str:
return "Calendar checker for {}".format(self.calendar)

View File

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014-2019 Virtual Cable S.L.
# Copyright (c) 2014-2021 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -40,6 +39,7 @@ from uds.core.util import os_detector as OsDetector
if typing.TYPE_CHECKING:
from django.http import HttpRequest # pylint: disable=ungrouped-imports
from uds.core.util.request import ExtendedHttpRequest
logger = logging.getLogger(__name__)
@ -101,7 +101,7 @@ def extractKey(dictionary: typing.Dict, key: typing.Any, **kwargs) -> str:
return value
def checkBrowser(request: 'HttpRequest', browser: str) -> bool:
def checkBrowser(request: 'ExtendedHttpRequest', browser: str) -> bool:
"""
Known browsers right now:
ie[version]
@ -113,7 +113,7 @@ def checkBrowser(request: 'HttpRequest', browser: str) -> bool:
for b, requires in _browsers.items():
if browser.startswith(b):
if request.os.Browser not in requires:
if request.os['Browser'] not in requires:
return False
browser = browser[len(b):] # remove "browser name" from string
break
@ -132,7 +132,7 @@ def checkBrowser(request: 'HttpRequest', browser: str) -> bool:
needs_version = 0
try:
version = int(request.os.Version.split('.')[0])
version = int(request.os['Version'].split('.')[0])
if needs == '<':
return version < needs_version
if needs == '>':

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2013-2019 Virtual Cable S.L.
# Copyright (c) 2013-2021 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -40,33 +40,43 @@ from uds.models import Provider, Service, ServicePool, Authenticator
logger = logging.getLogger(__name__)
EventTupleType = typing.Tuple[datetime.datetime, str, str, str, str, int]
EventClass = typing.TypeVar('EventClass', Provider, Service, ServicePool, Authenticator)
EventClass = typing.Union[Provider, Service, ServicePool, Authenticator]
if typing.TYPE_CHECKING:
from django.db.models import Model
# Posible events, note that not all are used by every possible owner type
(
# Login - logout
ET_LOGIN, ET_LOGOUT,
ET_LOGIN,
ET_LOGOUT,
# Service access
ET_ACCESS,
# Cache performance
ET_CACHE_HIT, ET_CACHE_MISS,
ET_CACHE_HIT,
ET_CACHE_MISS,
# Platforms detected
ET_PLATFORM,
# Plugin downloads
ET_PLUGIN_DOWNLOAD,
) = range(7)
ET_TUNNEL_ACCESS,
) = range(8)
(
OT_PROVIDER, OT_SERVICE, OT_DEPLOYED, OT_AUTHENTICATOR,
OT_PROVIDER,
OT_SERVICE,
OT_DEPLOYED,
OT_AUTHENTICATOR,
) = range(4)
__transDict = {
__transDict: typing.Mapping[typing.Type['Model'], int] = {
ServicePool: OT_DEPLOYED,
Service: OT_SERVICE,
Provider: OT_PROVIDER,
Authenticator: OT_AUTHENTICATOR,
}
def addEvent(obj: EventClass, eventType: int, **kwargs) -> bool:
"""
Adds a event stat to specified object
@ -81,7 +91,9 @@ def addEvent(obj: EventClass, eventType: int, **kwargs) -> bool:
return statsManager().addEvent(__transDict[type(obj)], obj.id, eventType, **kwargs)
def getEvents(obj: EventClass, eventType: int, **kwargs) -> typing.Generator[EventTupleType, None, None]:
def getEvents(
obj: EventClass, eventType: int, **kwargs
) -> typing.Generator[EventTupleType, None, None]:
"""
Get events
@ -108,6 +120,15 @@ def getEvents(obj: EventClass, eventType: int, **kwargs) -> typing.Generator[Eve
else:
owner_id = obj.pk
for i in statsManager().getEvents(__transDict[type_], eventType, owner_id=owner_id, since=since, to=to):
val = (datetime.datetime.fromtimestamp(i.stamp), i.fld1, i.fld2, i.fld3, i.fld4, i.event_type)
for i in statsManager().getEvents(
__transDict[type_], eventType, owner_id=owner_id, since=since, to=to
):
val = (
datetime.datetime.fromtimestamp(i.stamp),
i.fld1,
i.fld2,
i.fld3,
i.fld4,
i.event_type,
)
yield val

View File

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2020 Virtual Cable S.L.U.
# Copyright (c) 2012-2021 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,

View File

@ -40,12 +40,15 @@ from uds.core.managers import cryptoManager
from .uuid_model import UUIDModel
from .util import getSqlDatetime
from .user import User
from .user_service import UserService
logger = logging.getLogger(__name__)
ValidatorType = typing.Callable[[typing.Any], bool]
SECURED = '#SECURE#' # Just a "different" owner. If used anywhere, it's not important (will not fail), but
SECURED = '#SECURE#' # Just a "different" owner. If used anywhere, it's not important (will not fail), but weird enough
class TicketStore(UUIDModel):
"""
@ -142,7 +145,9 @@ class TicketStore(UUIDModel):
data: bytes = t.data
if secure: # Owner has already been tested and it's not emtpy
data = cryptoManager().AESDecrypt(data, typing.cast(str, owner).encode())
data = cryptoManager().AESDecrypt(
data, typing.cast(str, owner).encode()
)
data = pickle.loads(data)
@ -174,7 +179,69 @@ class TicketStore(UUIDModel):
t.validity = validity
t.save(update_fields=['validity', 'stamp'])
except TicketStore.DoesNotExist:
raise Exception('Does not exists')
raise TicketStore.InvalidTicket('Does not exists')
# Especific methods for tunnel
@staticmethod
def create_for_tunnel(
userService: 'UserService',
port: int,
host: typing.Optional[str] = None,
extra: typing.Optional[typing.Mapping[str, typing.Any]] = None,
validity: int = 60 * 60 * 24, # 24 Hours default validity for tunnel tickets
) -> str:
owner = cryptoManager().randomString(length=8)
data = {
'u': userService.user.uuid,
's': userService.uuid,
'h': host,
'p': port,
'e': extra,
}
return (
TicketStore.create(
data=data,
validity=validity,
owner=owner,
secure=True,
)
+ owner
)
@staticmethod
def get_for_tunnel(
ticket: str,
) -> typing.Tuple[
'User',
'UserService',
typing.Optional[str],
int,
typing.Optional[typing.Mapping[str, typing.Any]],
]:
"""
Returns the ticket for a tunneled connection
The returned value is a tuple:
(User, UserService, Host (nullable), Port, Extra Dict)
"""
try:
if len(ticket) != 48:
raise Exception(f'Invalid ticket format: {ticket!r}')
uuid, owner = ticket[:-8], ticket[-8:]
data = TicketStore.get(uuid, invalidate=False, owner=owner, secure=True)
# Now, ensure elements exists, onwershit is fine
# if not found any, will raise an execption
user = User.objects.get(uuid=data['u'])
userService = UserService.objects.get(uuid=data['s'], user=user)
host = data['h']
if not host:
host = userService.getInstance().getIp()
return (user, userService, host, data['p'], data['e'])
except Exception as e:
raise TicketStore.InvalidTicket(str(e))
@staticmethod
def cleanup() -> None:

View File

@ -41,8 +41,7 @@ from uds.core import transports
# TODO: do this
def createADUser():
try:
from . import AD
from . import AD # type: ignore
except ImportError:
return
@ -372,7 +371,9 @@ class BaseRDPTransport(transports.Transport):
user: 'models.User',
password: str,
) -> typing.Dict[str, str]:
return self.processUserPassword(typing.cast('models.UserService', userService), user, password)
return self.processUserPassword(
typing.cast('models.UserService', userService), user, password
)
def getScript(
self, scriptNameTemplate: str, osName: str, params: typing.Dict[str, typing.Any]

View File

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2018 Virtual Cable S.L.
# Copyright (c) 2012-2021 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@ -89,6 +88,27 @@ class TRDPTransport(BaseRDPTransport):
tab=gui.TUNNEL_TAB,
)
ticketValidity = gui.NumericField(
length=3,
label=_('Tunnel ticket validity time (seconds)'),
defvalue='7200',
minValue=60, # One minute as min
maxValue=7*60*60*24, # one week as max
order=3,
tooltip=_('Maximum validity time for user ticket to allow reconnection'),
required=True,
tab=gui.TUNNEL_TAB,
)
verifyCertificate = gui.CheckBoxField(
label=_('Force SSL certificate verification'),
order=23,
tooltip=_('If enabled, the certificate of tunnel server will be verified (recommended).'),
defvalue=gui.TRUE,
tab=gui.TUNNEL_TAB
)
useEmptyCreds = BaseRDPTransport.useEmptyCreds
fixedName = BaseRDPTransport.fixedName
fixedPassword = BaseRDPTransport.fixedPassword
@ -152,15 +172,14 @@ class TRDPTransport(BaseRDPTransport):
width, height = self.screenSize.value.split('x')
depth = self.colorDepth.value
tunpass = ''.join(
random.SystemRandom().choice(string.ascii_letters + string.digits)
for _i in range(12)
ticket = TicketStore.create_for_tunnel(
userService=userService,
port=3389,
validity=self.ticketValidity.num()
)
tunuser = TicketStore.create(tunpass)
sshHost, sshPort = self.tunnelServer.value.split(':')
logger.debug('Username generated: %s, password: %s', tunuser, tunpass)
tunHost, tunPort = self.tunnelServer.value.split(':')
r = RDPFile(
width == '-1' or height == '-1', width, height, depth, target=os['OS']
@ -202,12 +221,11 @@ class TRDPTransport(BaseRDPTransport):
)
sp = {
'tunUser': tunuser,
'tunPass': tunpass,
'tunHost': sshHost,
'tunPort': sshPort,
'tunHost': tunHost,
'tunPort': tunPort,
'tunWait': self.tunnelWait.num(),
'ip': ip,
'tunChk': self.verifyCertificate.isTrue(),
'ticket': ticket,
'password': password,
'this_server': request.build_absolute_uri('/'),
}

View File

@ -5,9 +5,9 @@ from __future__ import unicode_literals
# pylint: disable=import-error, no-name-in-module, too-many-format-args, undefined-variable, invalid-sequence-index
import subprocess
import re
from uds.forward import forward # @UnresolvedImport
from uds.tunnel import forward # type: ignore
from uds import tools # @UnresolvedImport
from uds import tools # type: ignore
# Inject local passed sp into globals for functions
globals()['sp'] = sp # type: ignore # pylint: disable=undefined-variable
@ -41,9 +41,6 @@ if app is None or fnc is None:
''')
else:
# Open tunnel
forwardThread, port = forward(sp['tunHost'], sp['tunPort'], sp['tunUser'], sp['tunPass'], sp['ip'], 3389, waitTime=sp['tunWait'])
fs = forward(remote=(sp['tunHost'], int(sp['tunPort'])), ticket=sp['ticket'], timeout=sp['tunWait'], check_certificate=sp['tunChk'])
if forwardThread.status == 2:
raise Exception('Unable to open tunnel')
fnc(app, port) # @UndefinedVariable
fnc(app, fs.server_address[1])

View File

@ -1 +1 @@
cL4uPkvN73tWpwdwNqedaOEHXLqmKlxKcg9iKW+QG3zCpPmbVn8nedzlumLfImLPQm+6ktpBscuIIsN5LyOZVuEnmoKWorLknlp5Jwzvp93gZEgGfCl8Y/YVaauvT4pPF5Pmczgsk/YykoAIHg5nllMHS0EdY0Pt/wAX7VgBN0RI1R4Ya5TeFC0b3NNZMg8kijmEYfSyNagPrzvM59SPOF93+iMMsZXaNiC7Lc0zbphS5QcjsMvALOHMyzl3omPVpIBFh/B8Wccx0svjfOp4Mdpt/tq0B9aGzhdEoaoqo3TNqINaPAVl90yPrfr/JrC7qEGNHaUWKgk5nImKJRyYBDiTFuv2TNMvJjrJ8JFgALsefdYhFM5EAo5i4G0bllEfn8g77NICnSlHGZal+x6oFORsmhRxJYXLDLDmeD/BJnHfhaNNSsy/dO/hBtAkkrIO3Qqot2MBo3ip6O/uOcCCdTNbwLCwlN90VahRzmf0j4irKNz5fdKT+Iox+1WT7980U9g3nyVCYz/Qw1b227WXJJ1NYSLGuVF1UU+8kIZPDxq+2FGmyvjTEKHw5RwMSA9Y5bRTDSZYhv9B0DGQ0NLfVJ9lCgQ9Zy99A6nTzIaqGi6FH7oxA5kMuGLAhfeT6sC3QSXfD9sfq2aKRsJgvqXrKiS79nZbkgijnj8nFSnTur8=
EGqTg6L0Bu5sqIA7BEUJgNzEwlQkPXfmdXLJ4iA+mRmXx6Z7QCXTYmxdqXmalUKUy4P2x3DvYMut5sM1BTWBYs6LxlE1CbuzBxEOw4VQHXDeW10Zir6C92IOevMZctrJS2zBNIB6RMddhD7HFwQ7LQ/yorUCClXUszjhcCxaTkqjM3KdbVuA4a+R9KF6gHHKCnjGrQXHuGdXjYm2+CRBWv5GBN57htO0VBEvCIrq7ZM/NzWDjBLlsrbkyUHxUoX3Tq0vXS03F3Gu3cxCP24yfYZoJeAHF4iOzU9XqomAYHvhNFEl7bvZz3ZyAIieT+zJJ+/WtGLjxL+ek8Va7V1ZYw9bWYnY0YyEkccupfoOXBy+phCJvcT6UgsL2dRO3yJma+GwejZAzv0JuDCvRmXN/xTbuSexyjIN7fLTmwT8q3DCA+m1CXXEQxLv9D1v2rhGPQOhwvomMKNRwZP3fi1zwL9d0FkgRnS36cz6+YLSf1dyXBDWK3Ez0vqJlRmLVz4GmVuidEnQ1pigzL3HLh3X32b9bd4nqCdSAVqP4dDcZsAQuf7JWYF0k7fA91ROT8nNEVg0zyN/YZU2Qxcr16fyVq1aBTTsDuEnKe0x0GruBRgEBU6Fr1i4eirgpcD+FddlLPvGUvgyH2lfotTcub+sL/BcgOaRvG7niiysJGfCxkc=

View File

@ -43,6 +43,7 @@ import uds.web.util.errors as errors
# Not imported at runtime, just for type checking
if typing.TYPE_CHECKING:
from django.http import HttpRequest # pylint: disable=ungrouped-imports
from uds.core.util.request import ExtendedHttpRequest
from uds.web.forms.LoginForm import LoginForm
from uds.models import User
@ -54,7 +55,7 @@ logger = logging.getLogger(__name__)
# (None, NumericError) if errorview redirection
# (User, password_string) if all is ok
def checkLogin( # pylint: disable=too-many-branches, too-many-statements
request: 'HttpRequest',
request: 'ExtendedHttpRequest',
form: 'LoginForm',
tag: typing.Optional[str] = None
) -> typing.Tuple[typing.Optional['User'], typing.Any]:

View File

@ -33,6 +33,7 @@ import socketserver
import ssl
import threading
import time
import random
import threading
import select
import typing
@ -48,6 +49,7 @@ TUNNEL_LISTENING, TUNNEL_OPENING, TUNNEL_PROCESSING, TUNNEL_ERROR = 0, 1, 2, 3
logger = logging.getLogger(__name__)
class ForwardServer(socketserver.ThreadingTCPServer):
daemon_threads = True
allow_reuse_address = True
@ -69,6 +71,9 @@ class ForwardServer(socketserver.ThreadingTCPServer):
local_port: int = 0,
check_certificate: bool = True,
) -> None:
local_port = local_port or random.randrange(33000, 53000)
super().__init__(
server_address=(LISTEN_ADDRESS, local_port), RequestHandlerClass=Handler
)
@ -82,7 +87,9 @@ class ForwardServer(socketserver.ThreadingTCPServer):
self.status = TUNNEL_LISTENING
if timeout:
self.timer = threading.Timer(timeout, ForwardServer.__checkStarted, args=(self,))
self.timer = threading.Timer(
timeout, ForwardServer.__checkStarted, args=(self,)
)
self.timer.start()
else:
self.timer = None
@ -116,7 +123,7 @@ class Handler(socketserver.BaseRequestHandler):
self.server.current_connections += 1
self.server.status = TUNNEL_OPENING
# If server processing is over time
# If server processing is over time
if self.server.stoppable:
logger.info('Rejected timedout connection try')
self.request.close() # End connection without processing it
@ -134,7 +141,7 @@ class Handler(socketserver.BaseRequestHandler):
# If ignore remote certificate
if self.server.check_certificate is False:
context.check_hostname = False
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
logger.warning('Certificate checking is disabled!')
@ -149,7 +156,9 @@ class Handler(socketserver.BaseRequestHandler):
data = ssl_socket.recv(2)
if data != b'OK':
data += ssl_socket.recv(128)
raise Exception(f'Error received: {data.decode()}') # Notify error
raise Exception(
f'Error received: {data.decode()}'
) # Notify error
# All is fine, now we can tunnel data
self.process(remote=ssl_socket)
@ -185,10 +194,17 @@ class Handler(socketserver.BaseRequestHandler):
except Exception as e:
pass
def _run(server: ForwardServer) -> None:
logger.debug('Starting forwarder: %s -> %s, timeout: %d', server.server_address, server.remote, server.timeout)
logger.debug(
'Starting forwarder: %s -> %s, timeout: %d',
server.server_address,
server.remote,
server.timeout,
)
server.serve_forever()
logger.debug('Stoped forwarded %s -> %s', server.server_address, server.remote)
logger.debug('Stoped forwarder %s -> %s', server.server_address, server.remote)
def forward(
remote: typing.Tuple[str, int],
@ -197,6 +213,7 @@ def forward(
local_port: int = 0,
check_certificate=True,
) -> ForwardServer:
fs = ForwardServer(
remote=remote,
ticket=ticket,
@ -209,8 +226,10 @@ def forward(
return fs
if __name__ == "__main__":
import sys
log = logging.getLogger()
log.setLevel(logging.DEBUG)
handler = logging.StreamHandler(sys.stdout)
@ -221,4 +240,12 @@ if __name__ == "__main__":
handler.setFormatter(formatter)
log.addHandler(handler)
fs = forward(('172.27.0.1', 7777), '1'*64, local_port=49999, timeout=10, check_certificate=False)
ticket = 'qcdn2jax6tx4nljdyed61hm3iqbld5nf44zxbh9gf355ofw2'
fs = forward(
('172.27.0.1', 7777),
ticket,
local_port=49999,
timeout=60,
check_certificate=False,
)

View File

@ -73,10 +73,17 @@ def read() -> ConfigurationType:
uds = cfg['uds']
# Gets and create secret hash
h = hashlib.sha256()
h.update(uds.get('secret', '').encode())
secret = h.hexdigest()
# Now load and fix uds server url
uds_server = uds['uds_server']
if uds_server[:4] != 'http':
raise Exception('Invalid url for uds server')
if uds_server[-1] == '/':
uds_server = uds_server[:-1]
try:
# log size
@ -84,7 +91,7 @@ def read() -> ConfigurationType:
if logsize[-1] == 'M':
logsize = logsize[:-1]
return ConfigurationType(
pidfile=uds.get('pidfile', '/dev/null'),
pidfile=uds.get('pidfile', ''),
log_level=uds.get('loglevel', 'ERROR'),
log_file=uds.get('logfile', ''),
log_size=int(logsize)*1024*1024,
@ -96,10 +103,10 @@ def read() -> ConfigurationType:
ssl_certificate_key=uds['ssl_certificate_key'],
ssl_ciphers=uds.get('ssl_ciphers'),
ssl_dhparam=uds.get('ssl_dhparam'),
uds_server=uds['uds_server'],
uds_server=uds_server,
secret=secret,
allow=set(uds.get('allow', '127.0.0.1').split(',')),
storage=uds['storage']
storage=uds.get('storage', '')
)
except ValueError as e:
raise Exception(f'Mandatory configuration file in incorrect format: {e.args[0]}. Please, revise {CONFIGFILE}')

View File

@ -44,7 +44,7 @@ BUFFER_SIZE = 1024 * 16
# Handshake for conversation start
HANDSHAKE_V1 = b'\x5AMGB\xA5\x01\x00'
# Ticket length
TICKET_LENGTH = 64
TICKET_LENGTH = 48
# Admin password length, (size of an hex sha256)
PASSWORD_LENGTH = 64
# Bandwidth calc time lapse

View File

@ -54,7 +54,8 @@ class Proxy:
@staticmethod
def getFromUds(
cfg: config.ConfigurationType, ticket: bytes
cfg: config.ConfigurationType, ticket: bytes,
address: typing.Tuple[str, int]
) -> typing.MutableMapping[str, typing.Any]:
# Sanity checks
if len(ticket) != consts.TICKET_LENGTH:
@ -69,14 +70,16 @@ class Proxy:
continue # Correctus
raise Exception(f'TICKET INVALID (char {i} at pos {n})')
# Gets the UDS connection data
# r = requests.get(f'{cfg.uds_server}/XXXX/ticket')
# if not r.ok:
# raise Exception(f'TICKET INVALID (check {r.json})')
return {
'host': ['172.27.1.15', '172.27.0.10'][int(ticket[0]) - 0x30],
'port': '3389',
}
try:
url = cfg.uds_server + '/' + ticket.decode() + '/' + address[0]
r = requests.get(url, headers={'content-type': 'application/json'})
if not r.ok:
raise Exception(r.content)
return r.json()
except Exception as e:
raise Exception(f'TICKET COMMS ERROR: {e!s}')
@staticmethod
async def doProxy(source, destination, counter: stats.StatsSingleCounter) -> None:
@ -155,7 +158,7 @@ class Proxy:
# Ticket received, now process it with UDS
try:
result = await curio.run_in_thread(Proxy.getFromUds, self.cfg, ticket)
result = await curio.run_in_thread(Proxy.getFromUds, self.cfg, ticket, address)
except Exception as e:
logger.error('ERROR %s', e.args[0] if e.args else e)
await source.sendall(b'ERROR INVALID TICKET')

View File

@ -1,11 +1,13 @@
# Sample DS tunnel configuration
# Pid file location
pidfile = /tmp/udstunnel.pid
# Log level, valid are DEBUG, INFO, WARN, ERROR. Defaults to ERROR
loglevel = DEBUG
# Log file, No default
logfile = /tmp/tunnel.log
# Log file, Defaults to stdout
# logfile = /tmp/tunnel.log
# Max log size before rotating it. Defaults to 32 MB.
# The value is in MB. You can include or not the M string at end.
logsize = 20M
@ -28,8 +30,12 @@ ssl_certificate_key = tests/testing.key
ssl_ciphers = ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384
ssl_dhparam = /etc/certs/dhparam.pem
# UDS server location. https NEEDS valid certificate
uds_server = http://172.27.0.1:8000
# UDS server location. https NEEDS valid certificate if https
# Must point to tunnel ticket dispatcher URL, that is under /uds/rest/tunnel/ on tunnel server
# Valid examples:
# http://www.example.com/uds/rest/tunnel/
# https://www.example.com:14333/uds/rest/tunnel/
uds_server = http://172.27.0.1:8000/uds/rest/tunnel
# Secret to get access to admin commands. No default for this.
# Admin commands and only allowed from localhost
@ -39,6 +45,3 @@ secret = MySecret
# List of af allowed admin commands ips (only IPs, no networks or whatever)
# defaults to localhost (change if listen address is different from 0.0.0.0)
allow = 127.0.0.1
# Local storage configuration, for stats, etc...
storage = .

View File

@ -157,8 +157,9 @@ def tunnel_main():
# Create pid file
try:
setup_log(cfg)
with open(cfg.pidfile, mode='w') as f:
f.write(str(os.getpid()))
if cfg.pidfile:
with open(cfg.pidfile, mode='w') as f:
f.write(str(os.getpid()))
except Exception as e:
sys.stderr.write(f'Tunnel startup error: {e}\n')
return
@ -234,6 +235,12 @@ def tunnel_main():
except Exception as e:
logger.info('KILLING child %s: %s', i[2], e)
try:
if cfg.pidfile:
os.unlink(cfg.pidfile)
except Exception:
pass
logger.info('FINISHED')