Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTORS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Dimitris Aragiorgis <dimitris.aragiorgis@gmail.com>
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ for more information.
Copyright and license
=====================

Copyright (C) 2010-2014 GRNET S.A.
Copyright (C) 2010-2015 GRNET S.A. and individual contributors

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
Expand Down
20 changes: 20 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,26 @@ With version 1.6, the private and public certificates are necessary for the
secure WebSocket (``vnc-wss``) console type. Otherwise, any ``vnc-wss`` request
will fail.

By default the vncauthproxy daemon expects VNC servers at the backend to require
no authentication. Alternatively, it can authenticate using simple VNC
password-based authentication. If the server requires password authentication,
vncauthproxy will try to connect using available passwords in the order
found in the password file, until a valid handshake is achieved.

The expected passwords should exist in a file which is specified with the
``--vnc-password-file`` option. The password file can contain multiple
passwords, each in a separate line. It can also include comments as lines
starting with '#'.

Mutliple passwords are useful in case you want to migrate an existing deployment
to a new VNC password gradually. In this case you must prepend the new password
in the VNC password file so it contains both the old and the new password, and
restart vncauthproxy. This will allow vncauthproxy to authenticate against
servers which still use the old password. You can remove the old password when
all servers require the same password. Note that passwords are tried in
the order found in the password file and thus it makes sense to list passwords
from newest to oldest.

For detailed help on its configuration parameters, either consult its man page
or run:

Expand Down
87 changes: 82 additions & 5 deletions vncauthproxy/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
vncauthproxy - a VNC authentication proxy
"""
#
# Copyright (c) 2010-2014 Greek Research and Technology Network S.A.
# Copyright (c) 2010-2015 GRNET S.A. and individual contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
Expand Down Expand Up @@ -85,6 +85,7 @@
import crypt

from vncauthproxy import rfb
from vncauthproxy import d3des
from vncauthproxy.websockets import (LoggedStream, LoggedStderr, VNCWS,
VNCWebSocketWSGIApplication,
VNCWSGIServer)
Expand Down Expand Up @@ -115,6 +116,11 @@ class InternalError(Exception):
pass


class AuthenticationError(InternalError):
"""Exception for VNC authentication error"""
pass


# Currently, gevent uses libevent-dns for asynchronous DNS resolution,
# which opens a socket upon initialization time. Since we can't get the fd
# reliably, We have to maintain all file descriptors open (which won't harm
Expand Down Expand Up @@ -237,6 +243,21 @@ def _forward(self, source, dest):
# They are owned by and will be closed by the original greenlet.

def _perform_server_handshake(self):
"""
Retry server handshake for all possible VNC passwords

"""
# First try for all given passwords
for idx, password in enumerate(self.vnc_passwords):
try:
return self._try_server_handshake(password, idx)
except AuthenticationError:
pass
# ..and if no passwords are given, try once without password
else:
return self._try_server_handshake()

def _try_server_handshake(self, vnc_password=None, vnc_password_idx=None):
"""
Initiate a connection with the backend server and perform basic
RFB 3.8 handshake with it.
Expand Down Expand Up @@ -285,6 +306,8 @@ def _perform_server_handshake(self):
if server is None:
raise InternalError("Failed to connect to server")

self.debug("Trying authentication with the VNC server")

version = server.recv(1024)
if not rfb.check_version(version):
raise InternalError("Unsupported RFB version: %s"
Expand All @@ -301,17 +324,40 @@ def _perform_server_handshake(self):
self.debug("Supported authentication types: %s",
" ".join([str(x) for x in types]))

if rfb.RFB_AUTHTYPE_NONE not in types:
raise InternalError("Error, server demands authentication")
if rfb.RFB_AUTHTYPE_NONE in types:
self.debug("No authentication requested by the server")
server.send(rfb.to_u8(rfb.RFB_AUTHTYPE_NONE))
elif rfb.RFB_AUTHTYPE_VNC in types:
self.debug("Password authentication requested by the server")

if not self.vnc_password_file:
raise InternalError("Password authentication requested but"
" no passwords are available"
" (check '--vnc-password-file' option)")

if not vnc_password:
raise InternalError("Password authentication requested but"
" no valid VNC password found")

self.debug("Using password no. %s", vnc_password_idx)
# Read challenge
server.send(rfb.to_u8(rfb.RFB_AUTHTYPE_VNC))
challenge = server.recv(16)
response = d3des.generate_response(vnc_password, challenge)
server.send(response)
else:
raise InternalError("Unsupported authentication method: %s", types)

server.send(rfb.to_u8(rfb.RFB_AUTHTYPE_NONE))

# Check authentication response
res = server.recv(4)
res = rfb.from_u32(res)

if res != 0:
raise InternalError("Authentication error")
self.debug("Authentication failed")
raise AuthenticationError("Authentication failed")

self.debug("Authentication succeeded")

# Reset the timeout for the rest of the session
server.settimeout(None)
Expand Down Expand Up @@ -755,6 +801,29 @@ def parse_auth_file(auth_file):
return users


def parse_vnc_password_file(vnc_password_file):
"""Parse multiline password file with comments"""
passwords = []
if vnc_password_file:
if os.path.isfile(vnc_password_file):
logger.debug("Using %s for VNC password file", vnc_password_file)
with open(vnc_password_file, "r") as f:
for line in f.readlines():
# Ignore comments and empty lines
if line.startswith("#") or not line.strip():
continue
passwords.append(line.strip())
else:
raise InternalError("Invalid VNC password file: %s."
" File does not exist." % vnc_password_file)

logger.debug("Found %s passwords in %s", len(passwords), vnc_password_file)
else:
logger.debug("Not using a VNC password file")

return passwords


def parse_arguments(args):
from optparse import OptionParser

Expand Down Expand Up @@ -838,6 +907,11 @@ def parse_arguments(args):
metavar="PROXY_LISTEN_ADDRESS",
help=("Address to listen for client connections"
"(default: *)"))
parser.add_option('--vnc-password-file', dest="vnc_password_file",
default=None,
metavar='VNC_PASSWORD_FILE',
help=("File containing the global VNC password to use"
" (default: None)"))

(opts, args) = parser.parse_args(args)

Expand Down Expand Up @@ -874,6 +948,9 @@ def main():
ports = range(opts.min_port, opts.max_port + 1)

# Init VncAuthProxy class attributes
VncAuthProxy.vnc_password_file = opts.vnc_password_file
VncAuthProxy.vnc_passwords = \
parse_vnc_password_file(opts.vnc_password_file)
VncAuthProxy.server_timeout = opts.server_timeout
VncAuthProxy.connect_retries = opts.connect_retries
VncAuthProxy.retry_wait = opts.retry_wait
Expand Down