From aa825517600bda984df68451062532306e6e18e9 Mon Sep 17 00:00:00 2001 From: Dimitris Aragiorgis Date: Thu, 28 May 2015 18:45:43 +0300 Subject: [PATCH 1/3] proxy: Support RFB_AUTHTYPE_VNC Until now we could proxy VNC servers only if no authentication was required by the server (RFB_AUTHTYPE_NONE). This patch adds an extra configuration option --vnc-password, the value of which will be used as the password when RFB_AUTHTYPE_VNC authentication is requested by the server. Add missing CONTRIBUTORS file and adjust Copyright messages accordingly. Signed-off-by: Dimitris Aragiorgis --- CONTRIBUTORS | 1 + README.md | 2 +- vncauthproxy/proxy.py | 30 ++++++++++++++++++++++++++---- 3 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 CONTRIBUTORS diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 0000000..b0ec1f6 --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1 @@ +Dimitris Aragiorgis diff --git a/README.md b/README.md index 7f250dd..b0e0129 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/vncauthproxy/proxy.py b/vncauthproxy/proxy.py index a1fef6e..d8d2f51 100644 --- a/vncauthproxy/proxy.py +++ b/vncauthproxy/proxy.py @@ -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 @@ -85,6 +85,7 @@ import crypt from vncauthproxy import rfb +from vncauthproxy import d3des from vncauthproxy.websockets import (LoggedStream, LoggedStderr, VNCWS, VNCWebSocketWSGIApplication, VNCWSGIServer) @@ -301,10 +302,25 @@ 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("RFB_AUTHTYPE_NONE requested by the server") + server.send(rfb.to_u8(rfb.RFB_AUTHTYPE_NONE)) + elif rfb.RFB_AUTHTYPE_VNC in types: + self.debug("RFB_AUTHTYPE_VNC requested by the server") + + if not self.vnc_password: + raise InternalError("Authentication requested but no VNC" + " password is set." + " Use the '--vnc-password' option.") + + # Read challenge + server.send(rfb.to_u8(rfb.RFB_AUTHTYPE_VNC)) + challenge = server.recv(16) + response = d3des.generate_response(self.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) @@ -839,6 +855,11 @@ def parse_arguments(args): help=("Address to listen for client connections" "(default: *)")) + parser.add_option('--vnc-password', dest="vnc_password", + default=None, + metavar='VNCPASSWORD', + help=("Global VNC password to use (default: None)")) + (opts, args) = parser.parse_args(args) if args: @@ -874,6 +895,7 @@ def main(): ports = range(opts.min_port, opts.max_port + 1) # Init VncAuthProxy class attributes + VncAuthProxy.vnc_password = opts.vnc_password VncAuthProxy.server_timeout = opts.server_timeout VncAuthProxy.connect_retries = opts.connect_retries VncAuthProxy.retry_wait = opts.retry_wait From ce0af0edc86b2e4769552b5a1fd3a3abb67ea6f0 Mon Sep 17 00:00:00 2001 From: Dimitris Aragiorgis Date: Thu, 28 May 2015 19:13:51 +0300 Subject: [PATCH 2/3] proxy: Use file instead of string for vnc password For security reasons, the password should not show up in the command line. We replace the '--vnc-password' option with the '--vnc-password-file' option that expects a file path as its value. This file should contain the password in plain text in the first line. The remaining lines will be ignored. Update the docs accordingly. Signed-off-by: Dimitris Aragiorgis --- docs/index.rst | 9 +++++++++ vncauthproxy/proxy.py | 28 ++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 10de825..a3add8e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -180,6 +180,15 @@ 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. Note that vncauthproxy can only use the same +password across all VNC servers that require authentication. The expected +password should exist in a file which can be specified with the +``--vnc-password-file`` option. The password file should contain the password in +a single line. Of course this file will be ignored for servers that do not +require authentication at all. + For detailed help on its configuration parameters, either consult its man page or run: diff --git a/vncauthproxy/proxy.py b/vncauthproxy/proxy.py index d8d2f51..3ad55a0 100644 --- a/vncauthproxy/proxy.py +++ b/vncauthproxy/proxy.py @@ -311,7 +311,7 @@ def _perform_server_handshake(self): if not self.vnc_password: raise InternalError("Authentication requested but no VNC" " password is set." - " Use the '--vnc-password' option.") + " Use the '--vnc-password-file' option.") # Read challenge server.send(rfb.to_u8(rfb.RFB_AUTHTYPE_VNC)) @@ -771,6 +771,21 @@ def parse_auth_file(auth_file): return users +def parse_vnc_password_file(vnc_password_file): + password = None + 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: + # Expect the password in the first line (ignoring next ones) + password = f.readline().strip() + else: + raise InternalError("Invalid VNC password file: %s." + " File does not exist." % vnc_password_file) + + return password + + def parse_arguments(args): from optparse import OptionParser @@ -854,11 +869,11 @@ def parse_arguments(args): metavar="PROXY_LISTEN_ADDRESS", help=("Address to listen for client connections" "(default: *)")) - - parser.add_option('--vnc-password', dest="vnc_password", + parser.add_option('--vnc-password-file', dest="vnc_password_file", default=None, - metavar='VNCPASSWORD', - help=("Global VNC password to use (default: None)")) + metavar='VNC_PASSWORD_FILE', + help=("File containing the global VNC password to use" + " (default: None)")) (opts, args) = parser.parse_args(args) @@ -895,7 +910,8 @@ def main(): ports = range(opts.min_port, opts.max_port + 1) # Init VncAuthProxy class attributes - VncAuthProxy.vnc_password = opts.vnc_password + VncAuthProxy.vnc_password = \ + 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 From 9629cb7027e4d5ddf8d8a100c3318447845b44f4 Mon Sep 17 00:00:00 2001 From: Dimitris Aragiorgis Date: Tue, 16 Jun 2015 17:03:46 +0300 Subject: [PATCH 3/3] proxy: Support multiple VNC passwords Add support for authenticating against VNC servers that require different passwords. The VNC password file should now include one password per line, where lines starting with "#" are ignored. If the server requests for password authentication, then the proxy will try to connect using available passwords in the order found in the password file. If a server changes its password then this password should be added in the VNC password file and vncauthproxy should be restarted. This patch improves logging with regard to the VNC authentication progress. Signed-off-by: Dimitris Aragiorgis --- docs/index.rst | 25 ++++++++++++----- vncauthproxy/proxy.py | 65 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 70 insertions(+), 20 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index a3add8e..08937f9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -181,13 +181,24 @@ 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. Note that vncauthproxy can only use the same -password across all VNC servers that require authentication. The expected -password should exist in a file which can be specified with the -``--vnc-password-file`` option. The password file should contain the password in -a single line. Of course this file will be ignored for servers that do not -require authentication at all. +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: diff --git a/vncauthproxy/proxy.py b/vncauthproxy/proxy.py index 3ad55a0..c590aca 100644 --- a/vncauthproxy/proxy.py +++ b/vncauthproxy/proxy.py @@ -116,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 @@ -238,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. @@ -286,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" @@ -303,20 +325,25 @@ def _perform_server_handshake(self): " ".join([str(x) for x in types])) if rfb.RFB_AUTHTYPE_NONE in types: - self.debug("RFB_AUTHTYPE_NONE requested by the server") + 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("RFB_AUTHTYPE_VNC requested by the server") + self.debug("Password authentication requested by the server") - if not self.vnc_password: - raise InternalError("Authentication requested but no VNC" - " password is set." - " Use the '--vnc-password-file' option.") + 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(self.vnc_password, challenge) + response = d3des.generate_response(vnc_password, challenge) server.send(response) else: raise InternalError("Unsupported authentication method: %s", types) @@ -327,7 +354,10 @@ def _perform_server_handshake(self): 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) @@ -772,18 +802,26 @@ def parse_auth_file(auth_file): def parse_vnc_password_file(vnc_password_file): - password = None + """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: - # Expect the password in the first line (ignoring next ones) - password = f.readline().strip() + 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) - return password + 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): @@ -910,7 +948,8 @@ def main(): ports = range(opts.min_port, opts.max_port + 1) # Init VncAuthProxy class attributes - VncAuthProxy.vnc_password = \ + 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