经过反复试验,我现在已经找到了下面的解决方案。为了帮助其他人,我不仅会展示代码,还会提供用于测试示例代码的参考设置。
首先,安装 minikube,然后启动 minikube 实例;我已经使用 minikube 1.0.0 进行了测试,然后运行 Kubernetes 1.14,在撰写本文时它是最新的。然后启动一个简单的 websocket 服务器,它只显示发送给它的内容,并将您所做的任何输入发送回连接的 websocket 客户端。
minikube start
kubectl run wsserver --generator=run-pod/v1 --rm -i --tty \
--image ubuntu:disco -- bash -c "\
apt-get update && apt-get install -y wget && \
wget https://github.com/vi/websocat/releases/download/v1.4.0/websocat_1.4.0_ssl1.1_amd64.deb && \
dpkg -i webso*.deb && \
websocat -vv -s 0.0.0.0:8000"
接下来是 Python 代码。它尝试通过 Kubernetes 的远程 API 从 minikube 连接到我们刚刚启动的 wsserver,使用远程 API 作为其反向代理。 minikube 设置通常使用客户端和服务器的相互 SSL/TLS 身份验证,因此这里是一个“硬”测试。请注意,还有其他方法,例如服务器证书和不记名令牌(而不是客户端证书)。
import kubernetes.client.configuration
from urllib.parse import urlparse
from twisted.internet import reactor
from twisted.internet import ssl
from twisted.python import log
from autobahn.twisted.websocket import WebSocketClientFactory, WebSocketClientProtocol, connectWS
import sys
if __name__ == '__main__':
log.startLogging(sys.stdout)
class EchoClientProto(WebSocketClientProtocol):
def onOpen(self):
print('onOpen')
self.sendMessage('testing...\n'.encode('utf8'))
def onMessage(self, payload, isBinary):
print('onMessage')
if not isBinary:
print('message %s' % payload.decode('utf8'))
def onClose(self, wasClean, code, reason):
print('onClose', wasClean, code, reason)
print('stopping reactor...')
reactor.stop()
# Select the Kubernetes cluster context of the minikube instance,
# and see what client and server certificates need to be used in
# order to talk to the minikube's remote API instance...
kubernetes.config.load_kube_config(context='minikube')
ccfg = kubernetes.client.configuration.Configuration._default
print('Kubernetes API server CA certificate at %s' % ccfg.ssl_ca_cert)
with open(ccfg.ssl_ca_cert) as ca_cert:
trust_root = ssl.Certificate.loadPEM(ca_cert.read())
print('Kubernetes client key at %s' % ccfg.key_file)
print('Kubernetes client certificate at %s' % ccfg.cert_file)
with open(ccfg.key_file) as cl_key:
with open(ccfg.cert_file) as cl_cert:
client_cert = ssl.PrivateCertificate.loadPEM(cl_key.read() + cl_cert.read())
# Now for the real meat: construct the secure websocket URL that connects
# us with the example wsserver inside the minikube cluster, via the
# remote API proxy verb.
ws_url = 'wss://%s/api/v1/namespaces/default/pods/wsserver:8000/proxy/test' % urlparse(ccfg.host).netloc
print('will contact: %s' % ws_url)
factory = WebSocketClientFactory(ws_url)
factory.protocol = EchoClientProto
# We need to attach the client and server certificates to our websocket
# factory so it can successfully connect to the remote API.
context = ssl.optionsForClientTLS(
trust_root.getSubject().commonName.decode('utf8'),
trustRoot=trust_root,
clientCertificate=client_cert
)
connectWS(factory, context)
print('starting reactor...')
reactor.run()
print('reactor stopped.')
使用optionsForClientTLS 附加客户端和服务器证书时,这里的棘手部分是 Twisted/SSL 希望被告知我们将要与之交谈的服务器名称。这也需要通知虚拟服务器他们需要提供多个服务器证书中的哪一个——在出现任何 HTTP 标头之前!
不幸的是,现在这是一个丑陋的领域——我很高兴在这里得到反馈!简单地使用 urlparse(ccfg.host).hostname 可以在一些 minikube 实例上工作,但不能在其他实例上工作。我还没有弄清楚为什么看似相似的实例表现不同。
我目前的解决方法是简单地使用服务器证书中主题的 CN(通用名称)。也许更稳健的方法可能是仅在远程 API 服务器的 URL 使用 IP 地址文字而不是 DNS 名称(或至少是标签)时才采用这种策略。
唉,运行python3 wssex.py 上面的 Python 3 代码。如果脚本正确连接,那么您应该会看到类似于 2019-05-03 12:34:56+9600 [-] {"peer": "tcp4:192.168.99.100:8443", "headers": {"sec-websocket-accept": ... 的日志消息
此外,您之前启动的 websocket 服务器应该会显示诸如[INFO websocat::net_peer] Incoming TCP connection from Some(V4(172.17.0.1:35222)) 等日志消息。
这证明了客户端脚本已经通过安全的 websocket 成功连接到 minikube 的远程 API,通过了身份验证和访问控制,并且现在连接到了 minikube 内的(不安全的)websocket 演示服务器。