Accept SSDP/DLNA responses through routed networks

Hey there,

I really had a hard time to make Simple Service Discovery Protocol (SSDP) work more or less seamlessly through my firewall. One might think, that is pretty common use case. But unfortunately none of the tipps I found on the internet really worked all the way. So, I thought, I'd try and make it easier for those who follow...

The use case is very simple: I have a network drive living in one subnet. And I want to "auto-magically" discover it from another network. Access is already allowed by suitable firewall zones. So, it's really just the convenient DLNA/UPnP/SSDP discovery features that are missing.

The hurdles are as follows:

  • SSDP uses multicast M-SEARCH and NOTIFY messages, which are not routed by default: smcroute comes to the rescue.
  • Those messages have a time to live (TTL) of 1, which means they will not survive the router: A mangle firewall rule will increase the TTL.
  • Obviously, the firewall needs to accept those messages: Custom rules in the firewall will be required.
  • Multicast M-SEARCH messages are sent from a random port. The unicast NOTIFY responses are sent to that same random port; from the server's IP with another random port. They thus have no static way to recognize them as a related response: conntrackd should provide a user-space helper for this. But I could not get it to work. So, I adapted a script from this archived forum topic.

Using smcroute is quite straight forward. Install it via opkg and edit your /etc/smcroute.conf:

  • Enable each interface that should be used, e.g. br-lan:
    phyint br-lan enable ttl-threshold 1
  • Setup a forwarding for each route you require (M-SEARCH or NOTIFY):
    mroute from br-lan group 239.255.255.250 to br-other

The mangle rule to increase TTL is quite simple. The script below will also insert it for you:
iptables -w -t mangle -A PREROUTING -d 239.255.255.250 -p udp --dport 1900 -j TTL --ttl-inc 1

You will know best, what firewall rules you require. But read on to have them set by the script.

Unfortunately, the unicast responses require a separate firewall rule for each M-SEARCH with its specific source IP and source port. For this you can use the following script as /etc/init.d/ssdphelper:

Script
#!/bin/sh /etc/rc.common

# Helper for SSDP M-Search multicast
# inspired by https://forum.archive.openwrt.org/viewtopic.php?id=67102

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

START=90

restart() {
  # Avoid stopping ssdphelper twice
  start
}

start() {
  # First stop ssdphelper
  stop

  # Create ssdphelper chains
  iptables -w -t filter -N ssdphelper_trigger      # &> /dev/null
  iptables -w -t filter -N ssdphelper_expectations # &> /dev/null

  # Create rules for increasing TTL
  # The TTL is usually set to 1, meaning it would not be routed.
  iptables -w -t mangle -A PREROUTING -d 239.255.255.250 -p udp --dport 1900 -j TTL --ttl-inc 1 -m comment --comment "SSDP: Increase TTL" # &> /dev/null

  # Create rules for logging and accepting traffic from zones with SSDP clients into zones with SSDP services
  # M-Search packets are smaller then Notify packets, so that's why there's filtering on packet length.
  iptables -w -t filter -A forwarding_lan_rule -j ssdphelper_trigger -m comment --comment "SSDP: Check triggers" # &> /dev/null
  iptables -w -t filter -A ssdphelper_trigger -d 239.255.255.250 -p udp --dport 1900 -m length --length 0:250 -j LOG --log-prefix "SSDP M-SEARCH: " --log-level 6 -m comment --comment "SSDP: Log M-SEARCH packets" # &> /dev/null
  iptables -w -t filter -A ssdphelper_trigger -d 239.255.255.250 -p udp --dport 1900 -m length --length 0:250 -j zone_other_dest_ACCEPT -m comment --comment "SSDP: Allow M-SEARCH" # &> /dev/null

  # Create rules for accepting traffic into from zones with SSDP services into zones with SSDP clients
  # M-Search packets are smaller then Notify packets, so that's why there's filtering on packet length.
  iptables -w -t filter -A forwarding_other_rule -j ssdphelper_expectations -m comment --comment "SSDP: Check expectations" # &> /dev/null
  iptables -w -t filter -A ssdphelper_expectations -d 239.255.255.250 -p udp --dport 1900 -m length ! --length 0:250 -j LOG --log-prefix "SSDP NOTIFY: " --log-level 6 -m comment --comment "SSDP: Log NOTIFY packets" # &> /dev/null
  iptables -w -t filter -A ssdphelper_expectations -d 239.255.255.250 -p udp --dport 1900 -m length ! --length 0:250 -j zone_lan_dest_ACCEPT -m comment --comment "SSDP: Allow NOTIFY" # &> /dev/null

  # Loop to check the log for triggers and add expectation rules
  check_log &
}

stop() {
  # Check if main loop is running
  pid1=$(ps -w | grep "logread -P ssdphelper -f" |grep -v grep | awk '{print $1}')
  [[ -z  $pid1  ]] && echo "Main loop not found. Daemon not running. You might see errors now."

  # Kill check_log loop
  kill $pid1 &> /dev/nul

  # Remove TTL rule
  iptables -w -t mangle -D PREROUTING -d 239.255.255.250 -p udp --dport 1900 -j TTL --ttl-inc 1 -m comment --comment "SSDP: Increase TTL" # &> /dev/null

  # Detach ssdphelper chains
  iptables -w -t filter -D forwarding_lan_rule   -j ssdphelper_trigger      -m comment --comment "SSDP: Check triggers"     # &> /dev/null
  iptables -w -t filter -D forwarding_other_rule -j ssdphelper_expectations -m comment --comment "SSDP: Check expectations" # &> /dev/null

  # Clear and remove ssdphelper chains
  iptables -w -t filter -F ssdphelper_trigger # &> /dev/null
  iptables -w -t filter -X ssdphelper_trigger # &> /dev/null

  iptables -w -t filter -F ssdphelper_expectations # &> /dev/null
  iptables -w -t filter -X ssdphelper_expectations # &> /dev/null

  # Indicate it's over
  [[ -z  $pid1  ]] && echo "Force-stopped ssdphelper. Errors should not happen anymore."
}

check_log() {
  # Read the system logfile $line by $line
  logread -P ssdphelper -f | while read line ;
  do
    # Silently search for $line with text generated from the iptables log rule
    echo "$line" | grep -q "SSDP M-SEARCH"
    if [ $? = 0 ]
    then

      # get the needed information from the log line
      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 expectation rule
      iptablesrule="ssdphelper_expectations -t filter -d $sourceaddress -p udp --dport $sourceport -j ACCEPT"

      # Check if rule already exists
      iptables -w -C $iptablesrule &> /dev/null
      if [ $? = 1 ]
      then
        # Else: add it
        iptables -w -A $iptablesrule # &> /dev/null
        # create a process that will delete the rule after 5 seconds
        (sleep 5; iptables -w -D $iptablesrule &> /dev/null)&
      fi

    fi
  done
}

status() {
  # Check if main loop is running
  pid1=$(ps -w | grep "logread -P ssdphelper -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 ""

    echo "Current TRIGGER Rules:"
    iptables -w -v -t filter -L ssdphelper_trigger

    echo ""
    echo ""

    echo "Current EXPECTATIONS Rules:"
    iptables -w -v -t filter -L ssdphelper_expectations

    echo ""
    echo ""
  fi

}

Make sure to replace and duplicate lines containing forwarding_lan_rule and forwarding_other_rule as you require. They setup the dynamic rules for the unicast NOTIFY responses.

Also make sure to adapt the lines containing zone_lan_dest_ACCEPT and zone_other_dest_ACCEPT. They will open the firewall for the multicast M-SEARCH and NOTIFY messages.

The script also inserts the mangle rule.

Now, start and enable the service:

chmod a+x /etc/init.d/ssdphelper
/etc/init.d/ssdphelper enable
/etc/init.d/ssdphelper start

And that's it. I can access my network share now with automatic discovery. I have this set up and running for not too long, yet. So, if I still run into trouble, I'll fix the guide here.

I hope this helps someone. And please let me know, if there is an easier way to do this. I really was puzzled by the complexity of that stuff...

1 Like