Updated Guacamole to only accept authenticated tunnel connections

* Added handshake check BEFORE opening SSL tunnel
This commit is contained in:
Adolfo Gómez García 2021-07-28 12:57:58 +02:00
parent 29b6613c95
commit a8a5063083
7 changed files with 42 additions and 31 deletions

View File

@ -51,6 +51,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
@ -113,11 +114,15 @@ class ForwardServer(socketserver.ThreadingTCPServer):
rsocket.connect(self.remote)
rsocket.sendall(HANDSHAKE_V1) # No response, just the handshake
context = ssl.create_default_context()
# Do not "recompress" data, use only "base protocol" compression
context.options |= ssl.OP_NO_COMPRESSION
context.load_verify_locations(tools.getCaCertsFile()) # Load certifi certificates
context.load_verify_locations(
tools.getCaCertsFile()
) # Load certifi certificates
# If ignore remote certificate
if self.check_certificate is False:
@ -135,7 +140,7 @@ class ForwardServer(socketserver.ThreadingTCPServer):
try:
with self.connect() as ssl_socket:
ssl_socket.sendall(HANDSHAKE_V1 + b'TEST')
ssl_socket.sendall(b'TEST')
resp = ssl_socket.recv(2)
if resp != b'OK':
raise Exception({'Invalid tunnelresponse: {resp}'})
@ -183,7 +188,7 @@ class Handler(socketserver.BaseRequestHandler):
logger.debug('Ticket %s', self.server.ticket)
with self.server.connect() as ssl_socket:
# Send handhshake + command + ticket
ssl_socket.sendall(HANDSHAKE_V1 + b'OPEN' + self.server.ticket.encode())
ssl_socket.sendall(b'OPEN' + self.server.ticket.encode())
# Check response is OK
data = ssl_socket.recv(2)
if data != b'OK':

View File

@ -31,11 +31,11 @@
@author: Adolfo Gómez, dkmaster at dkmon dot com
"""
from django.conf.urls import url
from .views import guacamole, guacamole_authenticated
from .views import guacamole
urlpatterns = [
# Authenticated path
url(r'^uds/guacamole/auth/(?P<token>[^/]+)/(?P<tunnelId>.+)$', guacamole_authenticated, name='dispatcher.guacamole'),
url(r'^uds/guacamole/auth/(?P<token>[^/]+)/(?P<tunnelId>.+)$', guacamole, name='dispatcher.guacamole'),
# Non authenticated path. Disabled
# url(r'^uds/guacamole/(?P<tunnelId>.+)$', guacamole, name='dispatcher.guacamole.noauth'),
]

View File

@ -54,8 +54,11 @@ def dict2resp(dct):
return '\r'.join((k + '\t' + v for k, v in dct.items()))
@auth.trustedSourceRequired
def guacamole(request: ExtendedHttpRequestWithUser, tunnelId: str) -> HttpResponse:
def guacamole(request: ExtendedHttpRequestWithUser, token: str, tunnelId: str) -> HttpResponse:
if not TunnelToken.validateToken(token):
logger.error('Invalid token %s from %s', token, request.ip)
return HttpResponse(ERROR, content_type=CONTENT_TYPE)
# TODO: Check the authId validity
logger.debug('Received credentials request for tunnel id %s', tunnelId)
try:
@ -103,10 +106,3 @@ def guacamole(request: ExtendedHttpRequestWithUser, tunnelId: str) -> HttpRespon
return HttpResponse(ERROR, content_type=CONTENT_TYPE)
return HttpResponse(response, content_type=CONTENT_TYPE)
def guacamole_authenticated(request: ExtendedHttpRequestWithUser, token: str, tunnelId: str) -> HttpResponse:
if not TunnelToken.validateToken(token):
logger.error('Invalid token %s from %s', token, request.ip)
return HttpResponse(ERROR, content_type=CONTENT_TYPE)
# TODO: Check the authId validity
return guacamole(request, tunnelId)

View File

@ -55,6 +55,13 @@ class IPServiceBase(services.Service):
except Exception:
return ''
@staticmethod
def getOrder(ipData: str) -> str:
try:
return ipData.split('~')[0]
except Exception:
return ''
def parent(self) -> 'provider.PhysicalMachinesProvider':
return typing.cast('provider.PhysicalMachinesProvider', super().parent())

View File

@ -158,21 +158,6 @@ class Proxy:
prettyDest = ''
logger.info('CONNECT FROM %s', prettySource)
try:
# First, ensure handshake (simple handshake) and command
data: bytes = await source.recv(len(consts.HANDSHAKE_V1))
if data != consts.HANDSHAKE_V1:
logger.error('INVALID HANDSHAKE %s', data)
raise Exception()
except Exception:
if consts.DEBUG:
logger.exception('HANDSHAKE')
logger.error('HANDSHAKE from %s', address)
await source.sendall(b'ERROR_HANDSHAKE')
# Closes connection now
return
try:
# Handshake correct, get the command (4 bytes)
command: bytes = await source.recv(consts.COMMAND_LENGTH)

View File

@ -40,7 +40,7 @@ ssl_dhparam = /etc/certs/dhparam.pem
# http://www.example.com/uds/rest/tunnel/ticket
# https://www.example.com:14333/uds/rest/tunnel/ticket
uds_server = http://172.27.0.1:8000/uds/rest/tunnel/ticket
uds_token = 123456789012345678901234567890123456789012345678
uds_token = eBCeFxTBw1IKXCqq-RlncshwWIfrrqxc8y5nehqiqMtRztwD
# Secret to get access to admin commands (Currently only stats commands). No default for this.
# Admin commands and only allowed from "allow" ips

View File

@ -105,7 +105,25 @@ async def tunnel_proc_async(
while True:
msg: message.Message = pipe.recv()
if msg.command == message.Command.TUNNEL and msg.connection:
# Connection done, check for handshake
source, address = msg.connection
try:
# First, ensure handshake (simple handshake) and command
data: bytes = source.recv(len(consts.HANDSHAKE_V1))
if data != consts.HANDSHAKE_V1:
raise Exception() # Invalid handshake
except Exception:
if consts.DEBUG:
logger.exception('HANDSHAKE')
logger.error('HANDSHAKE from %s', address)
# Close Source and continue
source.close()
continue
return msg.connection
# Process other messages, and retry
except Exception:
logger.exception('Receiving data from parent process')