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

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. 

15 

16import struct 

17 

18from oslo_log import log as logging 

19 

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 _ 

25 

26LOG = logging.getLogger(__name__) 

27 

28 

29class RFBSecurityProxy(base.SecurityProxy): 

30 """RFB Security Proxy Negotiation Helper. 

31 

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. 

38 

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. 

41 

42 See the general RFB specification at: 

43 

44 https://tools.ietf.org/html/rfc6143 

45 

46 See an updated, maintained RDB specification at: 

47 

48 https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst 

49 """ 

50 

51 def __init__(self): 

52 self.auth_schemes = auths.RFBAuthSchemeList() 

53 

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 

59 

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)) 

64 

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) 

69 

70 @staticmethod 

71 def _parse_version(version_str): 

72 r"""Convert a version string to a float. 

73 

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] 

79 

80 return float("%d.%d" % (int(maj_str), int(min_str))) 

81 

82 def connect(self, tenant_sock, compute_sock): 

83 """Initiate the RFB connection process. 

84 

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 """ 

91 

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 

101 

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')) 

107 

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) 

115 

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')) 

122 

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')) 

129 

130 # Negotiate security with server 

131 permitted_auth_types_cnt = recv(compute_sock, 1)[0] 

132 

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) 

138 

139 tenant_sock.sendall(auth.AUTH_STATUS_FAIL + 

140 reason_len_raw + reason) 

141 

142 raise exception.SecurityProxyNegotiationFailed(reason=reason) 

143 

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) 

150 

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 )) 

157 

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] 

163 

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)) 

170 

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) 

179 

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)) 

189 

190 compute_sock.sendall(bytes((scheme.security_type(),))) 

191 

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) 

196 

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")) 

207 

208 LOG.info("Finished security handshake, resuming normal proxy " 

209 "mode using secured socket") 

210 

211 # we can just proxy the security result -- if the server security 

212 # negotiation fails, we want the client to think it has failed 

213 

214 return compute_sock