OpenWrt Forum Archive

Topic: Forwarding SSDP multicast with NAT

The content of this topic has been archived on 1 May 2018. There are no obvious gaps in this topic, but there may still be some posts missing at the end.

Hi,

I have a TP-Link TL-WR1043N/ND v1 router running OpenWrt Chaos Calmer 15.05.1.

I have separate subnets for my wired LAN (192.168.123.0/24) and WiFi (192.168.125.0/24).
There is a NAS connected to the wired LAN (192.168.123.10) running twonky media server. On wifi there are some mobile devices (phone, tablet) with uPnP media player software.

The mobile devices need to see the Twonky server on the NAS. This is done by SSDP multicast messages (239.255.255.250 on port 1900). The multicasts are forwarded from LAN to Wifi and from Wifi to LAN (and this works).

There are 2 ways for client devices to see the Twonky server:

1) The Twonky server sends out a SSDP multicast Notify message. This message is received by the client and the server shows up in the library list. The disadvantage is that the server is sending this Notify message only once every minute. So you have to wait for max a minute before the server shows in the library.

2) The client software sends out a SSDP multicast M-Search message from a random UDP port to UDP port 1900. The twonky server receives this message, and sends back a UDP message from a random UDP port to the UDP port the M-Search message was received from. For example: 192.168.123.10 is the server, 192.168.123.50 is the client (I use the same subnet for this example). 192.168.123.50 is sending a M-Search message to 239.255.255.250:1900 from port 46359 (or another, because it's random). 192.168.123.10 is receiving this message, and sends a unicast message back to 192.168.123.50:46359 from port 39099 (also random). This way the client is instantly aware of the server and it shows up in the library.

The first method is working both in single and multiple subnet (with multicast routing). The second is only working when server and client are in the same subnet. When in different subnets, the server is receiving the M-Search message, but isn't sending a response (probably because it's programmed to not send a response when the request is from another subnet).

I was thinking I could fool the server by using NAT.
When adding the following rule to iptables the same M-Search message is received by the server, with the difference that the source address is now the router, which is in the same subnet:

iptables -t nat -I POSTROUTING -o eth0.1 -d 239.255.255.250 -p udp --dport 1900 -j SNAT --to 192.168.123.1

This works, the server is now sending back an answer to 192.168.123.1. But then I have a problem: the answer is send to the router, and not arriving at the client. This is probably because the answer is non-related to the question (UDP and a random source port instead of 1900 that was used in the question) and the router probably has no clue this packet is related to the SNAT rule.
I can make a manual rule like this:

iptables -t nat -I PREROUTING -i eth0.1 -s 192.168.123.10 -d 192.168.123.1 -p udp -j DNAT --to 192.168.125.42

192.168.125.42 is the address of the client (my phone). This works. The answer is now arriving at the client, and the server shows in the library. The drawback is that this rule is not very specific: there might be other UDP packets from 192.168.123.10 to 192.168.123.1, but because I don't know the port numbers of the answer, I can't be more specific. The other drawback is that you can only make a rule this way for a single client. If you have multiple clients in your wifi, you can't define a rule for all of them (because you can't make the rule more specific).

My question: What can I do to make this work? Can I configure iptables in a way that it recognizes the answer as an answer on a snatted question so it can dnat it automatically? Or can I dynamically add a rule to iptables the moment a client sends a M-Search message, use the port number the message came from in that new rule, and automatically delete it a few seconds later? Or port triggering maybe?

Best regards,
Marc

With some more searching I found something about installing userspace conntrack helpers, and I found one for the SSDP protocol.

The following commands should install the helper:

nfct helper add ssdp inet udp
iptables --verbose -I OUTPUT 1 -t raw -p udp --dport 1900 -j CT --helper ssdp

The nfct command should be part of the conntrack-tools. I installed the conntrack-tools package on my OpenWRT router, which gave me conntrack and conntrackd, but still no nfct.

How can I get nfct on the router and install the userspace conntrack helper?

I found a solution. I think there must be better ones, but this works.

I created an init script that creates some rule in iptables.
All SSDP multicast packets are source natted when forwarded by the router. When a m-search packet is forwarded, a script is triggered. This script creates a temporary dnat rule in iptables, that is automatically removed after 5 seconds.
The triggering of the script is done by writing a line to the log, and checking the log for this line.

I have very little experience wih scripting in linux, so comments are welcome!

#!/bin/sh /etc/rc.common
# Copyright (C) 2016-08-25 by MGK
# Natting of SSDP M-Search multicast

EXTRA_COMMANDS="status"
EXTRA_HELP="        status  Show status and current rules"

START=90

start() {
  # First stop ssdpnat
  stop
  
  # Create ssdpnat chains
  iptables -w -t raw -N ssdpnat_trigger &> /dev/null
  iptables -w -t nat -N ssdpnat_dnat &> /dev/null
  iptables -w -t nat -N ssdpnat_snat &> /dev/null
  
  # Attach ssdpnat chains to standard chains
  iptables -w -t raw -C PREROUTING -j ssdpnat_trigger &> /dev/null
  if [ $? = 1 ]
  then
    iptables -w -t raw -I PREROUTING -j ssdpnat_trigger &> /dev/null
  fi

  iptables -w -t nat -C POSTROUTING -j ssdpnat_snat &> /dev/null
  if [ $? = 1 ]
  then
    iptables -w -t nat -I POSTROUTING -j ssdpnat_snat &> /dev/null
  fi

  iptables -w -t nat -C PREROUTING -j ssdpnat_dnat &> /dev/null 
  if [ $? = 1 ]
  then
    iptables -w -t nat -I PREROUTING -j ssdpnat_dnat &> /dev/null 
  fi
  
    
  # Create rules for logging. Only M-Search packets are logged (M-Search packets are smaller then Notify packets, so that's why there's filtering on packet length)
  iptables -w -t raw -I ssdpnat_trigger -i wlan0 ! -s 192.168.125.1 -d 239.255.255.250 -p udp --dport 1900 -m length --length 0:250 -j LOG --log-prefix "SSDP Packet: " --log-level 6 &> /dev/null
  iptables -w -t raw -I ssdpnat_trigger -i eth0.1 ! -s 192.168.123.1 -d 239.255.255.250 -p udp --dport 1900 -m length --length 0:250 -j LOG --log-prefix "SSDP Packet: " --log-level 6 &> /dev/null

  # Create rules for Source Natting. All SSDP packages are NATted.
  iptables -w -t nat -I ssdpnat_snat -o wlan0 -d 239.255.255.250 -p udp --dport 1900 -j SNAT --to 192.168.125.1 &> /dev/null
  iptables -w -t nat -I ssdpnat_snat -o eth0.1 -d 239.255.255.250 -p udp --dport 1900 -j SNAT --to 192.168.123.1 &> /dev/null
  
  # loop which checks the log for triggers and adds DNAT rules
  check_log & 
}

stop() {
  # Kill check_log loop
  pid1=$(ps -w | grep "logread -P ssdpnat -f" |grep -v grep | awk '{print $1}')
  kill $pid1 &> /dev/nul

  # Detach ssdpnat chains
  iptables -w -t raw -D PREROUTING -j ssdpnat_trigger &> /dev/null
  iptables -w -t nat -D POSTROUTING -j ssdpnat_snat &> /dev/null
  iptables -w -t nat -D PREROUTING -j ssdpnat_dnat &> /dev/null 

  # Clear and remove ssdpnat chains
  iptables -w -t raw -F ssdpnat_trigger &> /dev/null
  iptables -w -t raw -X ssdpnat_trigger &> /dev/null

  iptables -w -t nat -F ssdpnat_dnat &> /dev/null
  iptables -w -t nat -X ssdpnat_dnat &> /dev/null

  iptables -w -t nat -F ssdpnat_snat &> /dev/null
  iptables -w -t nat -X ssdpnat_snat &> /dev/null
}

check_log() {
  # read the system logfile
  logread -P ssdpnat -f | while read line ;
  do
    # search for lines with text generated from the iptables log rule
    echo "$line" | grep -q "SSDP Packet"
    if [ $? = 0 ]
    then
      # get the needed information from the log line
      interface=$(echo $line | grep -o IN=[^,]* | cut -d " " -f 1 | cut -d = -f 2)
      sourceaddress=$(echo $line | grep -o SRC=[^,]* | cut -d " " -f 1 | cut -d = -f 2)
      sourceport=$(echo $line | grep -o SPT=[^,]* | cut -d " " -f 1 | cut -d = -f 2)
                
      # create DNAT rule in iptables
      if [ $interface == 'wlan0' ];
      then
        iptablesrule="ssdpnat_dnat -t nat -i eth0.1 -d 192.168.123.1 -p udp --dport $sourceport -j DNAT --to $sourceaddress"
        # check if rule doesn't allready exists
        iptables -w -C $iptablesrule &> /dev/null
        if [ $? = 1 ]
        then
          # if not, add it
          iptables -w -I $iptablesrule &> /dev/null
          # create a process that will delete the rule after 5 seconds
          (sleep 5; iptables -w -D $iptablesrule &> /dev/null)&
        fi
      fi
      
      if [ $interface == 'eth0.1' ];
      then
        iptablesrule="ssdpnat_dnat -t nat -i wlan0 -d 192.168.125.1 -p udp --dport $sourceport -j DNAT --to $sourceaddress"
        iptables -w -C $iptablesrule &> /dev/null
        if [ $? = 1 ]
        then
          iptables -w -I $iptablesrule &> /dev/null
          (sleep 5; iptables -w -D $iptablesrule &> /dev/null)&
        fi
      fi
    fi
  done
}

status() {
  # Check if main loop is running
  pid1=$(ps -w | grep "logread -P ssdpnat -f" |grep -v grep | awk '{print $1}')
  [[ -z  $pid1  ]] && echo "Main loop not found. Daemon not running."
  if [ ! -z  $pid1  ];
  then
    echo "Main loop found, PID=$pid1. Daemon running."
    echo "" 

    # shows the rules in the 3 used chains of iptables 
    echo "Current TRIGGER Rules:"
    iptables -w -v -t raw -L ssdpnat_trigger
  
    echo ""
    echo ""

    echo "Current SNAT Rules:"
    iptables -w -v -t nat -L ssdpnat_snat
  
    echo ""
    echo ""

    echo "Current DNAT Rules:"
    iptables -w -v -t nat -L ssdpnat_dnat

    echo ""
    echo ""
  fi
  
}

The discussion might have continued from here.