eclipse / paho.mqtt.python

paho.mqtt.python
Other
2.19k stars 723 forks source link

Help with SNI #734

Open lakshmisivareddy opened 1 year ago

lakshmisivareddy commented 1 year ago

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

MattBrittan commented 9 months ago

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

bram-tv commented 2 months ago

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