Limited broadcast forwarding

Hello.

I'm struggling with the following problem. I'd appreciate any suggestions/pointers to the right solution.

I have an "iot" wifi camera (Tapo, Tp-Link). With the camera comes a mobile app (quite decent one, 3rd party apps can't match it even remotely, unfortunately).

In my network all iot devices live in a separate VLAN (192.168.40.0/24). So, the camera has a 192.168.40.x address while my mobile device that I have the app installed on lives in the main lan has a 192.168.99.x address.

Now, the app uses limited broadcast (a UDP packet sent to 255.255.255.255) for camera discovery. Needless to say that the app and the camera being in two separate VLANs is in the way of app seeing/discovering the camera.

With the help of Wireshark, I captured the app <-> camera initial handshake. It's not very complicated. Technically, I could try writing a python script that would act as a proxy - listen to the broadcasts in the app ('source') network and relay them to the camera ('destination') network. And vice versa - listen to the 'replies' in the camera network and send them back to the app network. My firewall rules allow that.

But I'm lazy and plus, don't particularly like the idea of proxying: overcomplicated, quite brittle plus all the housekeeping around hosting/keeping it running. It feels like there should be an easier solution by which the packets would simply get forwarded (vs relayed/proxied) from the app network to the camera network. That way the 'reverse' forwarding would not even be needed - the camera could simply directly reply to the (app) address I had gotten the broadcast from - my firewall rules allow that.

What would be the correct way to approach/implement that in openwrt?

Okay :slight_smile: below is the script that relays limited broadcasts from a "main vlan" to a "camera vlan" as well as unicast replies from the "camera vlan" back to the "main vlan".

According to Wireshark the full roundtrip is going through just fine.

import socket

PROXY_MAIN_VLAN_IP = '192.168.1.148' # proxy's IP in the 'source' vlan
PROXY_CAMERA_VLAN_IP = "192.168.40.121" # proxy's IP in the camera vlan
BROADCAST_PORT = 20002

BUFFER_SIZE = 1024 * 2

main_vlan_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
main_vlan_socket.bind((PROXY_MAIN_VLAN_IP, BROADCAST_PORT))

camera_vlan_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
camera_vlan_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
camera_vlan_socket.bind((PROXY_CAMERA_VLAN_IP, 0))

counter = 0

while True:
    counter += 1
    print (f"Awaiting handshake {counter}...")
    data, addr = main_vlan_socket.recvfrom(BUFFER_SIZE)
    ip, port = addr
    print(f"Handshake initiated from the main vlan {ip}:{port}: {data}\n")

    # re-broadcasting data to the camera vlan
    camera_vlan_socket.sendto(data, ('255.255.255.255', BROADCAST_PORT))

    # awaiting the response from the camera
    handshake_response, _ = camera_vlan_socket.recvfrom(BUFFER_SIZE)
    print(f'Received response from the camera: {handshake_response}\n')

    main_vlan_socket.sendto(handshake_response, addr)
    print (f'Handshake response relayed to the main vlan {ip}:{port}\n')
    print ('--------------------------------------------------------------')

In my case though, the app still doesn't connect. I'm assuming it does some kind of validation of e.g. the sender address and if it's not what the handshake reply has in the payload - the discovered camera's IP - it just ignores this reply. In my case, obviously the sender's IP is the IP of the proxy..

Proxying has obvious inherent flaws for this job.

Ok. Another attempt. This time I'm trying to see if I can modify the 'proxied' UDP packet's source IP address such that the app would see that the camera IP it gets in the datagram payload matches the IP address of the sender (the camera). Essentially, it's an attempt to hide the fact that the calls are being proxied.

For that I'm using this iptables rule:

iptables -t nat -A POSTROUTING -s 192.168.40.121 -p udp -d 192.168.1.223 -o br-lan.40 -j SNAT --to-source 192.168.40.142

Essentially it's supposed to say: whenever you see an outgoing packet from the proxy socket (in the camera vlan) destined to the app in the main vlan (192.168.1.223), substitute the source IP with 192.168.40.142 - which is the real IP of the camera.

For that rule to have a chance to kick in, I'm assuming the UDP traffic should cross the subnetworks, otherwise it won't even go through the router. For the traffic to cross the subnetworks, I modified the original python script so that the handshake response is initiated from the camera vlan (and so it crosses camera vlan -> app vlan) - see below.

Question: the traffic is flowing as expected according to the console output and wireshark. But when I do iptables -t nat -L POSTROUTING -v, I get 0 hits for the rule..

 pkts bytes target     prot opt in     out     source               destination
    0     0 SNAT       udp  --  any    br-lan.40  192.168.40.121    192.168.1.223        to:192.168.40.142

Any ideas why the rule is not getting hit?

import socket

PROXY_MAIN_VLAN_IP = '192.168.1.148' # proxy's IP in the 'source' vlan
PROXY_CAMERA_VLAN_IP = "192.168.40.121" # proxy's IP in the camera vlan
BROADCAST_PORT = 20002

BUFFER_SIZE = 1024 * 2

main_vlan_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
main_vlan_socket.bind((PROXY_MAIN_VLAN_IP, BROADCAST_PORT))

camera_vlan_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
camera_vlan_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
camera_vlan_socket.bind((PROXY_CAMERA_VLAN_IP, 0))

camera_vlan_bounce_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
camera_vlan_bounce_socket.bind((PROXY_CAMERA_VLAN_IP, BROADCAST_PORT))

counter = 0

while True:
    counter += 1
    print (f"Awaiting handshake {counter}...")
    data, addr = main_vlan_socket.recvfrom(BUFFER_SIZE)
    ip, port = addr
    print(f"Handshake initiated from the main vlan {ip}:{port}: {data}\n")

    # re-broadcasting data to the camera vlan
    camera_vlan_socket.sendto(data, ('255.255.255.255', BROADCAST_PORT))

    # awaiting the response from the camera
    handshake_response, _ = camera_vlan_socket.recvfrom(BUFFER_SIZE)
    print(f'Received response from the camera: {handshake_response}\n')

    # TODO: clear the receive buffer - the socket is bound to BROADCAST PORT so it will receive the broadcast datagram
    camera_vlan_bounce_socket.sendto(handshake_response, addr)
    source_ip, source_port = camera_vlan_bounce_socket.getsockname()
    print (f'Handshake response relayed to the main vlan {ip}:{port} from {source_ip}:{source_port}\n')
    print ('--------------------------------------------------------------')

Success! :slight_smile:

It appears that I messed up a bit how my iot device and the app were connected to the network. And that's why my initial script didn't work. In a "correct" setup, where the app and iot devices are on two different vlans defined on the same router (yup, that's what I overlooked in my initial setup), proxying works (for Tapo devices).

And since the router has its one leg in the iot vlan and another leg in the app vlan, I use router's interfaces to bind proxy sockets to. Below is the corrected/brushed up script:

import socket
import select

PROXY_APP_VLAN_IP = '0.0.0.0' # proxy's IP in the 'source'/app vlan
PROXY_IOT_VLAN_IP = '192.168.40.121' # proxy's IP in the iot vlan
BROADCAST_PORT = 20002

BUFFER_SIZE = 1024 * 2
RESPONSE_TIMEOUT = 1 # devices are expected to reply to the handshare in 1 sec; otherwise handshake propagation will be delayed

app_vlan_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
app_vlan_socket.bind((PROXY_APP_VLAN_IP, BROADCAST_PORT))

iot_vlan_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
iot_vlan_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
iot_vlan_socket.bind((PROXY_IOT_VLAN_IP, 0))

app_proxy_ip, app_proxy_port = app_vlan_socket.getsockname()
iot_proxy_ip, iot_proxy_port = iot_vlan_socket.getsockname()

iteration = 1

try:
    while True:
 
        print (f'Iteration: {iteration}. Awaiting handshake on {app_proxy_ip}:{app_proxy_port}...')

        handshake_request, initiator_addr = app_vlan_socket.recvfrom(BUFFER_SIZE)
        initiator_ip, initiator_port = initiator_addr
        
        is_rebroadcast = initiator_addr == (iot_proxy_ip, iot_proxy_port)
        if is_rebroadcast:
            print (f'Skipping re-broadcast from the proxy (self) {initiator_ip}:{initiator_port}\n')
            continue
        
        print (f'Handshake initiated from the app vlan {initiator_ip}:{initiator_port}:\n {handshake_request}\n')

        # re-broadcasting data to the iot vlan
        iot_vlan_socket.sendto(handshake_request, ('255.255.255.255', BROADCAST_PORT))
        print (f'Handshake re-broadcast to the iot vlan from {iot_proxy_ip}:{iot_proxy_port}')

        # awaiting the response from devices in the iot vlan - potentially more than 1
        while True:

            responses_available, _, _ = select.select([iot_vlan_socket], [], [], RESPONSE_TIMEOUT)

            if not responses_available:
                print ('All responses collected, finishing current iteration')
                break

            handshake_response, (device_ip, device_port) = iot_vlan_socket.recvfrom(BUFFER_SIZE)

            print (f'Received response from a device {device_ip}:{device_port}:\n {handshake_response}\n')

            app_vlan_socket.sendto(handshake_response, initiator_addr)
            print (f'Handshake response relayed to the app vlan to {initiator_ip}:{initiator_port} from {app_proxy_ip}:{app_proxy_port}\n')

        iteration += 1
        print ('--------------------------------------------------------------')

except KeyboardInterrupt:
    app_vlan_socket.close()
    iot_vlan_socket.close()

Please note that I haven't tested it thoroughly, so if you wish so, use it at your own risk. E.g. to eliminate certain edge cases which could cause broadcast storms, it may be a good idea to white-list networks/apps that can be the source of broadcasts vs just blanket-listening on 0.0.0.0. Plus, ability to tweak verbosity of logging would also be nice..

For my home network, though, and a router running OpenWRT 23.05.0 and Python 3 it seems to work okay so far :slight_smile:

I'm also planning to use coreutils-nohup to launch the script on boot.

1 Like

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.