Help with SNI
Hi Team , i have a Multiple MQTT Broker hosted in K8's , these MQTT Brokers are behind the ingress controller ingress controller routes the traffic to appropriate broker based on SNI for non TLS i am able to verify the connection using below command openssl s_client -showcerts -connect istio-test.westus2.cloudapp.azure.com:8883 -servername example1.test.com openssl s_client -showcerts -connect istio-test.westus2.cloudapp.azure.com:8883 -servername example2.test.com
with TLS traffic i am not able to set specific SNI (servername). By default SNI going as istio-test.westus2.cloudapp.azure.com
please fine the sample i am trying
import paho.mqtt.client as paho
from paho.mqtt import client as mqtt
import ssl
import time
import socket
#path_to_root_cert = "/home/challal/Downloads/cacert.pem"
#path_to_root_cert = "/home/challal/Downloads/certs/azure-iot-test-only.root.ca.cert.pem"
path_to_root_cert = "/home/challal/Downloads/test.pem"
device_id = "pub_cert"
cert_file = "/Users/l0c0gvk/Workspace/Testing/mqtttest/example_certs/device_cert_filename.pem"
key_file = "/Users/l0c0gvk/Workspace/Testing/mqtttest/example_certs/device_cert_key_filename.key"
ca_cert='/Users/l0c0gvk/Workspace/Testing/mqtttest/example_certs/root_CA_cert_filename.pem'
def on_connect(client, userdata, flags, rc):
print("Device connected with result code: " + str(rc))
def on_disconnect(client, userdata, rc):
print("Device disconnected with result code: " + str(rc))
def on_publish(client, userdata, mid):
print("Device sent message")
def sni_callback(sock, req_hostname, cb_context, as_callback=True):
print('sni_callback')
# context1 = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
# context1.load_cert_chain(certfile=cert_file,keyfile=key_file)
# context1.wrap_socket(socket.socket(socket.AF_INET),server_hostname="example1.test.com")
print('Loading certs for {}'.format(req_hostname))
# print(type(cb_context))
client = paho.Client(client_id=device_id,clean_session=True,userdata=None,protocol=mqtt.MQTTv311)
client.on_connect = on_connect
client.on_disconnect = on_disconnect
client.on_publish = on_publish
# Set the certificate and key paths on your client
#client.tls_set(ca_certs=path_to_root_cert, certfile=cert_file, keyfile=key_file,
# cert_reqs=ssl.CERT_REQUIRED, tls_version=ssl.PROTOCOL_TLSv1_2, ciphers=None)
ssl_ctx = ssl.create_default_context(cafile=ca_cert)
ssl_ctx.check_hostname = False
# ssl_ctx.load_cert_chain(certfile=cert_file, keyfile=key_file)
# ssl_ctx.verify_mode = ssl.CERT_NONE
# client.tls_set_context(ssl_ctx)
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
context.load_verify_locations(ca_cert)
context.load_cert_chain(cert_file, key_file)
#context.wrap_socket(server_hostname="example1.test.com")
context.sni_callback=sni_callback
client.tls_set_context(context)
client.tls_insecure_set(True)
client.connect("istio-test.westus2.cloudapp.azure.com", 8883,60)
print(type(client.socket))
client.loop_start()
while True:
client.publish("test_topic", "{id=123}", qos=1)
time.sleep(0.1)
Can some one help me here
I believe you wish to change server_hostname within wrap_socket; unfortunately this is currently fixed in the code (reconnect function):
# Try with server_hostname, even it's not supported in certain scenarios
sock = self._ssl_context.wrap_socket(
sock,
server_hostname=self._host,
do_handshake_on_connect=False,
)
One option would be to do this via DNS (e.g. a CNAME for example1.test.com, using a domain you own!, pointing to istio-test.westus2.cloudapp.azure.com); that should work as-is.
Alternatively see the subclass example. Using this technique you can override reconnect() and configure the server_hostname as you require.
As this would seem to be a fairly rare requirement I'm going to leave it there; please let us know if that is useful or you believe modifications to the library are needed (given this was logged sometime ago I'd guess you may already have a solution).
As this would seem to be a fairly rare requirement I'm going to leave it there; please let us know if that is useful or you believe modifications to the library are needed (given this was logged sometime ago I'd guess you may already have a solution).
Just adding my 2 cents to this issue: I was looking for the same option (specify the server_hostname of the SSL context): our MQTT broker has several servers all of which are behind a single record (i.e. DNS returns multiple A records for the same name) and we needed to verify that all IPs are working as expected [in our case it was mainly to verify the firewall in front of our clients].
The work-around we applied:
import paho.mqtt.client as mqtt
import ssl
import socket as _socket
ip = "127.2.3.4"
port = 8883
host = "foo.example.com"
client_id = "foo"
class ServerNameClient(mqtt.Client):
def _ssl_wrap_socket(self, tcp_sock: _socket.socket) -> ssl.SSLSocket:
orig_host = self._host
self._host = host
res = super()._ssl_wrap_socket(tcp_sock)
self._host = orig_host
return res
mc = ServerNameClient(mqtt.CallbackAPIVersion.VERSION2, client_id=client_id)
...
mc.connect(ip, port=port)
mc.loop_forever()
In other threads regarding SNI (i.e. https://github.com/eclipse/paho.mqtt.python/issues/133#issuecomment-269967646) there was some fear that adding it may confuse users but that might be avoidable if the server_hostanme was an option of Client.tls_set, i.e. that one could do something like:
mc = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=client_id)
mc.tls_set(server_hostname="foo.example.com")
mc.connect(ip, port=port)
mc.loop_forever()
and that _ssl_wrap_socket would then prefer it (foo.example.com) over the host param of connect