Coverage for nova/console/securityproxy/rfb.py: 95%
80 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-24 11:16 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-24 11:16 +0000
1# Copyright (c) 2014-2016 Red Hat, Inc
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
16import struct
18from oslo_log import log as logging
20from nova.console.rfb import auth
21from nova.console.rfb import auths
22from nova.console.securityproxy import base
23from nova import exception
24from nova.i18n import _
26LOG = logging.getLogger(__name__)
29class RFBSecurityProxy(base.SecurityProxy):
30 """RFB Security Proxy Negotiation Helper.
32 This class proxies the initial setup of the RFB connection between the
33 client and the server. Then, when the RFB security negotiation step
34 arrives, it intercepts the communication, posing as a server with the
35 "None" authentication type to the client, and acting as a client (via
36 the methods below) to the server. After security negotiation, normal
37 proxying can be used.
39 Note: this code mandates RFB version 3.8, since this is supported by any
40 client and server impl written in the past 10+ years.
42 See the general RFB specification at:
44 https://tools.ietf.org/html/rfc6143
46 See an updated, maintained RDB specification at:
48 https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst
49 """
51 def __init__(self):
52 self.auth_schemes = auths.RFBAuthSchemeList()
54 def _make_var_str(self, message):
55 message_str = str(message)
56 message_bytes = message_str.encode('utf-8')
57 message_len = struct.pack("!I", len(message_bytes))
58 return message_len + message_bytes
60 def _fail(self, tenant_sock, compute_sock, message):
61 # Tell the client there's been a problem
62 result_code = struct.pack("!I", 1)
63 tenant_sock.sendall(result_code + self._make_var_str(message))
65 if compute_sock is not None:
66 # Tell the server that there's been a problem
67 # by sending the "Invalid" security type
68 compute_sock.sendall(auth.AUTH_STATUS_FAIL)
70 @staticmethod
71 def _parse_version(version_str):
72 r"""Convert a version string to a float.
74 >>> RFBSecurityProxy._parse_version('RFB 003.008\n')
75 0.2
76 """
77 maj_str = version_str[4:7]
78 min_str = version_str[8:11]
80 return float("%d.%d" % (int(maj_str), int(min_str)))
82 def connect(self, tenant_sock, compute_sock):
83 """Initiate the RFB connection process.
85 This method performs the initial ProtocolVersion
86 and Security messaging, and returns the socket-like
87 object to use to communicate with the server securely.
88 If an error occurs SecurityProxyNegotiationFailed
89 will be raised.
90 """
92 def recv(sock, num):
93 b = sock.recv(num)
94 if len(b) != num: 94 ↛ 95line 94 didn't jump to line 95 because the condition on line 94 was never true
95 reason = _("Incorrect read from socket, wanted %(wanted)d "
96 "bytes but got %(got)d. Socket returned "
97 "%(result)r") % {'wanted': num, 'got': len(b),
98 'result': b}
99 raise exception.RFBAuthHandshakeFailed(reason=reason)
100 return b
102 # Negotiate version with compute server
103 compute_version = recv(compute_sock, auth.VERSION_LENGTH)
104 LOG.debug(
105 "Got version string '%s' from compute node",
106 compute_version[:-1].decode('utf-8'))
108 if self._parse_version(compute_version) != 3.8:
109 reason = _(
110 "Security proxying requires RFB protocol version 3.8, "
111 "but server sent %s")
112 raise exception.SecurityProxyNegotiationFailed(
113 reason=reason % compute_version[:-1].decode('utf-8'))
114 compute_sock.sendall(compute_version)
116 # Negotiate version with tenant
117 tenant_sock.sendall(compute_version)
118 tenant_version = recv(tenant_sock, auth.VERSION_LENGTH)
119 LOG.debug(
120 "Got version string '%s' from tenant",
121 tenant_version[:-1].decode('utf-8'))
123 if self._parse_version(tenant_version) != 3.8:
124 reason = _(
125 "Security proxying requires RFB protocol version 3.8, "
126 "but tenant asked for %s")
127 raise exception.SecurityProxyNegotiationFailed(
128 reason=reason % tenant_version[:-1].decode('utf-8'))
130 # Negotiate security with server
131 permitted_auth_types_cnt = recv(compute_sock, 1)[0]
133 if permitted_auth_types_cnt == 0:
134 # Decode the reason why the request failed
135 reason_len_raw = recv(compute_sock, 4)
136 reason_len = struct.unpack('!I', reason_len_raw)[0]
137 reason = recv(compute_sock, reason_len)
139 tenant_sock.sendall(auth.AUTH_STATUS_FAIL +
140 reason_len_raw + reason)
142 raise exception.SecurityProxyNegotiationFailed(reason=reason)
144 f = recv(compute_sock, permitted_auth_types_cnt)
145 permitted_auth_types = []
146 for auth_type in f:
147 if isinstance(auth_type, str): 147 ↛ 148line 147 didn't jump to line 148 because the condition on line 147 was never true
148 auth_type = ord(auth_type)
149 permitted_auth_types.append(auth_type)
151 LOG.debug(
152 "Server sent security types: %s",
153 ", ".join(
154 '%d (%s)' % (auth.AuthType(t).value, auth.AuthType(t).name)
155 for t in permitted_auth_types
156 ))
158 # Negotiate security with client before we say "ok" to the server
159 # send 1:[None]
160 tenant_sock.sendall(auth.AUTH_STATUS_PASS +
161 bytes((auth.AuthType.NONE,)))
162 client_auth = recv(tenant_sock, 1)[0]
164 if client_auth != auth.AuthType.NONE:
165 self._fail(
166 tenant_sock, compute_sock,
167 _("Only the security type {value} ({name}) "
168 "is supported").format(value=auth.AuthType.NONE.value,
169 name=auth.AuthType.NONE.name))
171 reason = _(
172 "Client requested a security type other than "
173 "{value} ({name}): {client_value} ({client_name})"
174 ).format(value=auth.AuthType.NONE.value,
175 name=auth.AuthType.NONE.name,
176 client_value=auth.AuthType(client_auth).value,
177 client_name=auth.AuthType(client_auth).name)
178 raise exception.SecurityProxyNegotiationFailed(reason=reason)
180 try:
181 scheme = self.auth_schemes.find_scheme(permitted_auth_types)
182 except exception.RFBAuthNoAvailableScheme as e:
183 # Intentionally don't tell client what really failed
184 # as that's information leakage
185 self._fail(tenant_sock, compute_sock,
186 _("Unable to negotiate security with server"))
187 raise exception.SecurityProxyNegotiationFailed(
188 reason=_("No compute auth available: %s") % str(e))
190 compute_sock.sendall(bytes((scheme.security_type(),)))
192 LOG.debug(
193 "Using security type %d (%s) with server, %d (%s) with client",
194 scheme.security_type().value, scheme.security_type().name,
195 auth.AuthType.NONE.value, auth.AuthType.NONE.name)
197 try:
198 compute_sock = scheme.security_handshake(compute_sock)
199 except exception.RFBAuthHandshakeFailed as e:
200 # Intentionally don't tell client what really failed
201 # as that's information leakage
202 self._fail(tenant_sock, None,
203 _("Unable to negotiate security with server"))
204 LOG.debug("Auth failed %s", str(e))
205 raise exception.SecurityProxyNegotiationFailed(
206 reason=_("Auth handshake failed"))
208 LOG.info("Finished security handshake, resuming normal proxy "
209 "mode using secured socket")
211 # we can just proxy the security result -- if the server security
212 # negotiation fails, we want the client to think it has failed
214 return compute_sock