Revision 3b98303f

b/Changelog
13 13
	* Add Changelog, README and docs
14 14
	* Replace Unix domain control sockets with TCP control sockets, with
15 15
	  basic authentication and SSL support.
16
	* Add the vncauthproxy-passwd tool to manage the users file.
b/MANIFEST.in
3 3

  
4 4
recursive-include vncauthproxy *
5 5
recursive-include docs *
6
recursive-include examples *
b/docs/index.rst
18 18
  * IPv4 and IPv6 support
19 19
  * Configurable timeout for client connections
20 20

  
21
Its main use is to enable VNC clients to connect to firwalled VNC servers.
21
Its main use is to enable VNC clients to connect to firewalled VNC servers.
22 22

  
23 23
It is used by `Synnefo <https://code.grnet.gr/projects/synnefo>`_ to provide
24 24
users with (VNC) console access to their VMs.
......
26 26
Installation
27 27
^^^^^^^^^^^^
28 28

  
29
snf-vncauthproxy is currently packaged only for Debian (stable / oldstable).
29
snf-vncauthproxy is currently packaged only for Debian (stable).
30 30

  
31 31
You can find and install the latest version snf-vncauthproxy at Synnefo's apt
32 32
repository:
......
37 37

  
38 38
| ``curl https://dev.grnet.gr/files/apt-grnetdev.pub | apt-key add -``
39 39

  
40
In case you're upgrading from an older snf-vncauthproxy version or it's the
41
first time you're installing snf-vncauthproxy, you will prompted to configure
42
a vncauthproxy user (see below for more information on user management).
43

  
40 44
Overview
41 45
^^^^^^^^
42 46

  
43
snf-vncauthproxy listens on a TCP socket for control (JSON) messages from clients.
44
The format of the control messages is:
47
snf-vncauthproxy listens on a TCP socket for control (JSON) messages from
48
clients. The format of the control messages is:
45 49

  
46 50
.. code-block:: console
47 51

  
......
89 93
The snf-vncauthproxy daemon can be either run manually or managed via its init
90 94
script.
91 95

  
92
If you're using the init script, snf-vncauthproxy reads its paramater from its
96
If you're using the init script, snf-vncauthproxy reads its options from its
93 97
default file (``DAEMON_OPTS`` parameter in ``/etc/default/vncauthproxy``).
98
Refer to the vncauthproxy help output for a detailed listing and information
99
on all available options:
100

  
101
.. code-block:: console
94 102

  
95
By default snf-vncauthproxy will listen to ``127.0.0.1:24999`` TCP, for incoming
96
control connections and uses the ``25000-30000`` range for the listening / data
97
sockets.
103
    # vncauthproxy --help
98 104

  
99
Version 1.5 introduced replaced Unix domain control sockets with TCP
100
control sockets. This change made it necessary to also introduce an
101
authentication file to replace the Unix file permissions, which protected the
102
domain sockets.
105
By default snf-vncauthproxy will listen to ``127.0.0.1:24999`` TCP, for
106
incoming control connections and uses the ``25000-30000`` range for the
107
listening / data sockets.
108

  
109
Version 1.5 replaced Unix domain control sockets with TCP control sockets. This
110
change made it necessary to introduce an authentication file to replace the
111
POSIX file permissions, which protected the domain sockets.
103 112

  
104 113
The default path for the auth file is ``/var/lib/vncauthproxy/users``
105 114
(configurable by the ``--auth-file`` option). Each line in the file represents
......
108 117

  
109 118
.. code-block:: console
110 119

  
111
    user password
112
    user1 {cleartext}password
113
    user2 {HA1}md5hash
120
    username:$6$salt$hash
121

  
122
The password part of the line (after the colon) is the output of crypt(), using
123
a random 16-char salt with SHA-512.
124

  
125
To manage the authentication file, you can use the vncauthproxy-passwd tool,
126
to easily add, update and delete users:
127

  
128
To add a user:
129

  
130
.. code-block:: console
131

  
132
    # vncauthproxy-passwd /var/lib/vncauthproxy/users user
133

  
134
You will be prompted for a password.
114 135

  
115
The Debian package provides an example users file.
136
To delete a user:
137

  
138
.. code-block:: console
139

  
140
    # vncauthproxy-passwd -D /var/lib/vncauthproxy/users user
141

  
142
See the help output of the tool for more options:
143

  
144
.. code-block:: console
145

  
146
    # vncauthproxy-passwd -h
147

  
148
.. warning:: The vncauthproxy daemon requires a restart for the changes in the
149
 authentication file to take effect.
150

  
151
.. warning:: After installing snf-vncauthproxy for the fist time, make sure
152
 that you create a valid authentication file and define any users needed. The
153
 vncauthproxy daemon will start but will not be usable if no users are defined
154
 or if no authentication file is present.
116 155

  
117 156
Version 1.5 introduced also support for SSL for the control socket. If you
118 157
enable SSL support (``--enable-ssl`` parameter, disabled by default) you wil
......
161 200
snf-cyclades-app can connect to the snf-vncauthproxy on the listening address /
162 201
port. It's also recommended to enable SSL on the control socket in that case.
163 202

  
164
.. include:: changelog.rst
203
Changelog
204
^^^^^^^^^
205

  
206
* v1.5 :ref:`Changelog <Changelog-1.5>`
207

  
208
Upgrade notes
209
^^^^^^^^^^^^^
210

  
211
.. toctree::
212
   :maxdepth: 1
165 213

  
166
.. include:: upgrade.rst
214
    v1.4 -> v1.5 <upgrade/upgrade-1.5.rst>
167 215

  
168 216
Contact
169 217
^^^^^^^
/dev/null
1
Upgrade notes
2
^^^^^^^^^^^^^
3

  
4
v1.5
5
====
6
Version 1.5 replaced Unix domain control sockets with TCP
7
control sockets. This change made it necessary to also introduce an
8
authentication file to replace the POSIX file permissions, which protected the
9
domain sockets.
10

  
11
The default path for the auth file is ``/var/lib/vncauthproxy/users``
12
(configurable by the ``--auth-file`` option). Each line in the file represents
13
one user which is allowed to use the control socket and should be in the
14
following format:
15

  
16
.. code-block:: console
17

  
18
    user password
19
    user1 {cleartext}password
20
    user2 {HA1}md5hash
21

  
22
If you want to use a hash instead of a password, you should provide the MD5
23
digest of the string ``user:vncauthproxy:password``. It can be generated with
24
the following command:
25

  
26
.. code-block:: console
27

  
28
    $ echo -n 'user:vncauthproxy:password' | openssl md5
29

  
30
The Debian package provides an example users file.
31

  
32
Version 1.5 also introduced support for SSL for the control socket. If you
33
enable SSL support (``--enable-ssl`` parameter, disabled by default) you will
34
have to provide a certficate and key file (``--cert-file`` and ``--key-file``
35
parameters). The default values for certificate and key files are
36
``/var/lib/vncauthrpoxy/{cert,key}.pem`` respectively.
37

  
38
If you're using snf-vncauthproxy with Synnefo, you should make sure to edit the
39
``CYCLADES_VNCAUTHPROXY_OPTS`` setting in
40
``/etc/synnefo/20-snf-cyclades-app-api.conf``.  The
41
``CYCLADES_VNCAUTHPROXY_OPTS`` dict in
42
``/etc/synnefo/20-snf-cyclades-app-api.conf`` should be edited to match
43
snf-vncauthproxy configuration (user, password, SSL support, certificate file).
44
You should also make sure that the node running snf-cyclades-app can connect to
45
the snf-vncauthproxy's control socket address / port (the suggested deployment to
46
run snf-vncauthproxy on the same host as snf-cyclades-app should work with
47
the defaults of snf-vncauthproxy, with the exception of the authentication
48
file).
b/docs/upgrade/upgrade-1.5.rst
1
Upgrade notes
2
^^^^^^^^^^^^^
3

  
4
v1.5
5
====
6
Version 1.5 replaced Unix domain control sockets with TCP control sockets. This
7
change made it necessary to introduce an authentication file to replace the
8
POSIX file permissions, which protected the domain sockets.
9

  
10
You can configure vncauthproxy daemon by modifying the Debian default file
11
(``/etc/default/vncauthproxy``) and more specifically the ``DAEMON_OPTS``
12
variable. This option (along with the modified ``CHUID`` option) has been added
13
to the v1.5 default file (which you'll need to 'merge' if you're upgrading from
14
an older version of snf-vncauthproxy).
15

  
16
The ``DAEMON_OPTS`` variable accepts any valid option you can pass to the
17
vncauthproxy daemon on the command line. For a detailed listing and information
18
about the avaialble options plese check vncauthproxy help output:
19

  
20
.. code-block:: console
21

  
22
    # vncauthproxy --help
23

  
24
The default path for the auth file is ``/var/lib/vncauthproxy/users``
25
(configurable by the ``--auth-file`` option). Each line in the file represents
26
one user which is allowed to use the control socket and should be in the
27
following format:
28

  
29
.. code-block:: console
30

  
31
    username:$6$salt$hash
32

  
33
The password part of the line (after the colon) is the output of crypt(), using
34
a random 16-char salt with SHA-512.
35

  
36
To manage the authentication file, you can use the vncauthproxy-passwd tool,
37
to easily add, update and delete users:
38

  
39
To add a user:
40

  
41
.. code-block:: console
42

  
43
    # vncauthproxy-passwd /var/lib/vncauthproxy/users user
44

  
45
You will be prompted for a password.
46

  
47
To delete a user:
48

  
49
.. code-block:: console
50

  
51
    # vncauthproxy-passwd -D /var/lib/vncauthproxy/users user
52

  
53
See the help output of the tool for more options:
54

  
55
.. code-block:: console
56

  
57
    # vncauthproxy-passwd -h
58

  
59
.. warning:: The vncauthproxy daemon requires a restart for the changes in the
60
 authentication file to take effect.
61

  
62
.. warning:: After installing snf-vncauthproxy for the fist time, make sure
63
 that you create a valid authentication file and define any users needed. The
64
 vncauthproxy daemon will start but will not be usable if no users are defined
65
 or if no authentication file is present.
66

  
67
Version 1.5 also introduced support for SSL for the control socket. If you
68
enable SSL support (``--enable-ssl`` parameter, disabled by default) you will
69
have to provide a certficate and key file (``--cert-file`` and ``--key-file``
70
parameters). The default values for certificate and key files are
71
``/var/lib/vncauthrpoxy/{cert,key}.pem`` respectively.
72

  
73
If you're using snf-vncauthproxy with Synnefo, you should make sure to edit the
74
``CYCLADES_VNCAUTHPROXY_OPTS`` setting in
75
``/etc/synnefo/20-snf-cyclades-app-api.conf``.  The
76
``CYCLADES_VNCAUTHPROXY_OPTS`` dict in
77
``/etc/synnefo/20-snf-cyclades-app-api.conf`` should be edited to match
78
snf-vncauthproxy configuration (user, password, SSL support, certificate file).
79
You should also make sure that the node running snf-cyclades-app can connect to
80
the snf-vncauthproxy's control socket address / port (the suggested deployment to
81
run snf-vncauthproxy on the same host as snf-cyclades-app should work with
82
the defaults of snf-vncauthproxy, with the exception of the authentication
83
file).
84

  
85
Finally, snf-vncauthproxy now adds a user and group (``vncauthproxy``) to be
86
used by the vncauthproxy daemon. As a result the ``CHUID`` option in the Debian
87
default file (``/etc/default/vncauthproxy``) has changed accordingly. Although
88
it is recommended to run vncauhtproxy with the predfined user and group, it's
89
not mandatory.
/dev/null
1
user password
2
user1 {cleartext}password
3
user2 {HA1}md5hash
b/setup.py
40 40
    ],
41 41
    entry_points={
42 42
        'console_scripts': [
43
            'vncauthproxy = vncauthproxy.proxy:main'
43
            'vncauthproxy = vncauthproxy.proxy:main',
44
            'vncauthproxy-passwd = vncauthproxy.passwd:main'
44 45
        ]
45 46
    }
46 47
)
b/vncauthproxy/client.py
59 59
                      help=("Use source port PORT for incoming connections "
60 60
                            "(default: allocate a port automatically)"))
61 61
    parser.add_option("-d", "--dest",
62
                      default=None, dest="daddr",
62
                      dest="daddr",
63 63
                      metavar="HOST",
64 64
                      help="Proxy connection to destination host HOST")
65 65
    parser.add_option("-p", "--dport", dest="dport",
66
                      default=None, type="int",
66
                      type="int",
67 67
                      metavar="PORT",
68 68
                      help="Proxy connection to destination port PORT")
69 69
    parser.add_option("-P", "--password", dest="password",
70
                      default=None,
71 70
                      metavar="PASSWORD",
72 71
                      help=("Use password PASSWD to authenticate incoming "
73 72
                            "VNC connections"))
74 73
    parser.add_option("--auth-user", dest="auth_user",
75
                      default=None,
76 74
                      metavar="AUTH_USER",
77 75
                      help=("User to authenticate as, for the control "
78 76
                            "connection"))
79 77
    parser.add_option("--auth-password", dest="auth_password",
80
                      default=None,
81 78
                      metavar="AUTH_PASSWORD",
82 79
                      help=("User password for the control connection "
83 80
                            "authentication"))
......
105 102
                       server_address=DEFAULT_SERVER_ADDRESS,
106 103
                       server_port=DEFAULT_SERVER_PORT, enable_ssl=False,
107 104
                       ca_cert=None, strict=False):
108
    """Connect to vncauthproxy and request a VNC forwarding."""
109

  
110
    # Mandatory arguments
111
    if not password:
112
        raise Exception("The password argument is mandatory.")
113
    if not daddr:
114
        raise Exception("The daddr argument is mandatory.")
115
    if not dport:
116
        raise Exception("The dport argument is mandatory.")
117
    if not auth_user:
118
        raise Exception("The auth_user argument is mandatory.")
119
    if not auth_password:
120
        raise Exception("The auth_password argument is mandatory.")
105
    """ Connect to vncauthproxy and request a VNC forwarding.
106

  
107
        @type sport: int
108
        @param sport: Source port for incoming connections
109
                      (0 for automatic allocation)"
110
        @type daddr: str
111
        @param daddr: Destination address for the forwarding
112
        @type dport: int
113
        @param dport: Destination port for the forwarding
114
        @type password: str
115
        @param password: VNC server auth password
116
        @type auth_user: str
117
        @param auth_user: vncauthproxy user
118
        @type auth_password: str
119
        @param auth_password: vncauthproxy password
120
        @type server_address: str
121
        @param server_address: Listening address for the vncauthproxy daemon
122
                               (default: 127.0.0.1)
123
        @type server_port: int
124
        @param server_port: Listening port for the vncauthproxy daemon
125
                           (default: 24999)
126
        @type enable_ssl: bool
127
        @param enable_ssl: Enable / disable SSL on the control socket
128
        @type ca_cert: str
129
        @param ca_cert: Path to the CA cert file
130
        @type strict: bool
131
        @param strict: Enable strict cert checking for SSL
132
        @rtype: dict
133
        @return: Server response in dict / JSON format
134

  
135
        """
121 136

  
122 137
    # Sanity check
123 138
    if strict and not ca_cert:
124 139
        raise Exception("strict requires ca-cert to be set")
125 140
    if not enable_ssl and (strict or ca_cert):
126
        logger.warning("strict or ca_cert set, but ssl not enabled")
141
        logger.warning("strict or ca-cert set, but ssl not enabled")
127 142

  
128 143
    req = {
129 144
        "source_port": int(sport),
......
192 207

  
193 208
    (opts, args) = parse_arguments(sys.argv[1:])
194 209

  
210
    # Mandatory arguments
211
    if opts.password is None:
212
        sys.stderr.write("The password argument is mandatory.\n")
213
        sys.exit(1)
214
    if opts.daddr is None:
215
        sys.stderr.write("The daddr argument is mandatory.\n")
216
    if opts.dport is None:
217
        sys.stderr.write("The dport argument is mandatory.\n")
218
    if opts.auth_user is None:
219
        sys.stderr.write("The auth_user argument is mandatory.\n")
220
    if opts.auth_password is None:
221
        sys.stderr.write("The auth_password argument is mandatory.\n")
222

  
195 223
    res = request_forwarding(sport=opts.sport, daddr=opts.daddr,
196 224
                             dport=opts.dport, password=opts.password,
197 225
                             auth_user=opts.auth_user,
b/vncauthproxy/passwd.py
1
#!/usr/bin/env python
2
"""
3
vncauthproxy-passwd - vncauthproxy passwd file mgmt tool
4
"""
5
#
6
# Copyright (c) 2010-2013 Greek Research and Technology Network S.A.
7
#
8
# This program is free software; you can redistribute it and/or modify
9
# it under the terms of the GNU General Public License as published by
10
# the Free Software Foundation; either version 2 of the License, or
11
# (at your option) any later version.
12
#
13
# This program is distributed in the hope that it will be useful, but
14
# WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16
# General Public License for more details.
17
#
18
# You should have received a copy of the GNU General Public License
19
# along with this program; if not, write to the Free Software
20
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21
# 02110-1301, USA.
22

  
23
import argparse
24
import crypt
25
import getpass
26
import os
27
import random
28
import re
29
import string
30
import sys
31
import tempfile
32

  
33

  
34
def parse_arguments():
35
    """ Parse cli args. """
36
    parser = argparse.ArgumentParser()
37

  
38
    parser.add_argument("-n", "--dry-run", action="store_true", dest="dry_run",
39
                        help="Display the results on stdout without updating "
40
                        "the passwd file")
41
    parser.add_argument("-D", "--delete", action="store_true",
42
                        dest="delete_user", help="Delete user from file")
43
    parser.add_argument("passwdfile", metavar="file", type=str, nargs=1,
44
                        help="Path to the passwd file")
45
    parser.add_argument("user", metavar="user", type=str, nargs=1,
46
                        help="User to edit")
47

  
48
    args = parser.parse_args()
49

  
50
    return args
51

  
52

  
53
def gen_salt():
54
    """ Generate 16-char salt string. """
55
    chars = list(string.ascii_letters + string.digits + "./")
56
    random.shuffle(chars)
57

  
58
    return "".join(random.choice(chars) for x in range(16))
59

  
60

  
61
def gen_hash(username, password):
62
    """ Generate SHA-512 hash in crypt() format. """
63
    salt = "$6$%s$" % gen_salt()
64
    return "%s:%s\n" % (username, crypt.crypt(password, salt))
65

  
66

  
67
def fail(reason):
68
    """ Print reason for failure and exit. """
69
    sys.stderr.write("%s\n" % reason)
70
    sys.exit(1)
71

  
72

  
73
def find_user(lines, user):
74
    """ Return user info from passwd file, if the users exists. """
75
    for (idx, line) in enumerate(lines):
76
        (username, _) = line.split(":", 1)
77
        if user == username:
78
            return (idx, line)
79

  
80
    return None
81

  
82

  
83
def write_wrapper(passwdfile, lines, dry_run):
84
    """ Dry-run wrapper for write. """
85
    if not dry_run:
86
        (fd, name) = tempfile.mkstemp()
87
        with os.fdopen(fd, "w+") as f:
88
            f.write("".join(lines))
89
        os.rename(name, passwdfile)
90
    else:
91
        sys.stderr.write("".join(lines))
92

  
93

  
94
def delete_user(user, passwdfile):
95
    """ Delete user from passwdfile. """
96
    if not os.path.isfile(passwdfile):
97
        fail("Cannot delete user from non-existent file")
98

  
99
    lines = open(passwdfile).readlines()
100
    user_line = find_user(lines, user)
101
    if not user_line:
102
        fail("User not found!")
103

  
104
    (idx, line) = user_line
105
    lines.remove(line)
106
    return lines
107

  
108

  
109
def add_or_update_user(user, passwdfile):
110
    """ Add or update user from passwdfile. """
111
    password = getpass.getpass()
112
    if password == "":
113
        fail("Password cannot be empty")
114

  
115
    if password != getpass.getpass("Retype password: "):
116
        fail("Passwords don't match")
117

  
118
    newline = gen_hash(user, password)
119

  
120
    lines = [newline]
121
    if os.path.isfile(passwdfile):
122
        lines = open(passwdfile).readlines()
123
        user_line = find_user(lines, user)
124
        if not user_line:
125
            lines.append(newline)
126
        else:
127
            (idx, _) = user_line
128
            lines[idx] = newline
129

  
130
    return lines
131

  
132

  
133
def main():
134
    """ Run the tool from the command line. """
135
    try:
136
        args = parse_arguments()
137

  
138
        user = args.user[0]
139
        passwdfile = args.passwdfile[0]
140

  
141
        user_re = r'^[a-z_][a-z0-9_]{0,30}$'
142
        if re.match(user_re, user) is None:
143
            fail("Username must match the following regexp: %s" % user_re)
144

  
145
        if args.delete_user:
146
            lines = delete_user(user, passwdfile)
147
        else:
148
            lines = add_or_update_user(user, passwdfile)
149

  
150
        write_wrapper(passwdfile, lines, args.dry_run)
151
    except KeyboardInterrupt:
152
        pass
153

  
154
    sys.exit(0)
b/vncauthproxy/proxy.py
70 70
import daemon
71 71
import random
72 72
import daemon.runner
73
import hashlib
74
import re
73
import crypt
75 74

  
76 75
from vncauthproxy import rfb
77 76

  
......
345 344
            self.password = req['password']
346 345

  
347 346
            if auth_user not in VncAuthProxy.authdb:
348
                msg = "Authentication failure: user not found"
347
                msg = "vncauthproxy authentication failure: user not found"
349 348
                raise InternalError(msg)
350 349

  
351
            (cipher, authdb_password) = VncAuthProxy.authdb[auth_user]
352
            if cipher == 'HA1':
353
                message = auth_user + ':vncauthproxy:' + auth_password
354
                auth_password = hashlib.md5(message).hexdigest()
350
            (cipher, salt, authdb_hash) = VncAuthProxy.authdb[auth_user]
351
            crypt_result = crypt.crypt(auth_password, '$%s$%s$' %
352
                                                      (cipher, salt))
353
            passhash = crypt_result.lstrip('$').split('$', 2)[-1]
355 354

  
356
            if auth_password != authdb_password:
357
                msg = "Authentication failure: wrong password"
355
            if passhash != authdb_hash:
356
                msg = "vncauthproxy authentication failure: wrong password"
358 357
                raise InternalError(msg)
359 358
        except KeyError:
360 359
            msg = "Malformed request: %s" % buf
......
618 617

  
619 618

  
620 619
def parse_auth_file(auth_file):
621
    supported_ciphers = ('cleartext', 'HA1', None)
622
    regexp = re.compile(r'^\s*(?P<user>\S+)\s+({(?P<cipher>\S+)})?'
623
                        r'(?P<pass>\S+)\s*$')
624

  
625 620
    users = {}
626 621

  
627 622
    if os.path.isfile(auth_file) is False:
......
631 626

  
632 627
    try:
633 628
        with open(auth_file) as f:
634
            lines = [l.strip() for l in f.readlines()]
635

  
636
            for line in lines:
637
                if not line or line.startswith('#'):
638
                    continue
639

  
640
                m = regexp.match(line)
641
                if not m:
642
                    raise InternalError("Invaild entry in auth file: %s"
643
                                        % line)
644

  
645
                user = m.group('user')
646
                cipher = m.group('cipher')
647
                if cipher not in supported_ciphers:
648
                    raise InternalError("Unsupported cipher in auth file: "
649
                                        "%s" % line)
650

  
651
                password = (cipher, m.group('pass'))
652

  
653
                if user in users:
654
                    raise InternalError("Duplicate user entry in auth file")
655

  
656
                users[user] = password
657
    except IOError as err:
658
        logger.error("Error while reading the auth file:")
629
            lines = [l.strip().split(':', 1) for l in f.readlines()]
630
            for (user, passhash) in lines:
631
                users[user] = passhash.lstrip('$').split('$', 2)
632
    except ValueError as err:
659 633
        logger.exception(err)
634
        raise InternalError("Malformed auth file")
660 635

  
661 636
    if not users:
662 637
        logger.warning("No users defined")

Also available in: Unified diff