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/docs/index.rst b/docs/index.rst index 10de825..08937f9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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: diff --git a/vncauthproxy/proxy.py b/vncauthproxy/proxy.py index a1fef6e..c590aca 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) @@ -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 @@ -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. @@ -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" @@ -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) @@ -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 @@ -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) @@ -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