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

Hi.
I, like many others, faced the same problem. There are enough discussions/solutions, but they do not completely solve the problem.
I was able to connect to the DLNA server from another subnet, but I have to wait up to 60 seconds until the client sees the SSDP NOTIFY messages from the server. At the same time, the server does not respond to M-SEARCH messages in any way. I would like to see the server instantly, just like when I am on the same subnet with it. Out of curiosity, I wonder if this is related to mDNS?

Is there a solution to the problem using ssdp_helper using nftables?

I updated "recently" to a new firmware, which finally switched to firewall4 and thus broke my original solution. So, here's my port of the ideas to the fw4 ecosystem. The gist stays the same:

  • unchanged: use smcroute to enable routing of multicast traffic across subnets
  • updated: increase TTL by a firewall4 rule
  • updated: accept required traffic in the firewall4
  • updated: create dynamics fw4 rules to handle random ports

In /etc/config/firewall add the following rules:

config include
        option type 'nftables'
        option path '/etc/nftables.d/00-flush-ruleset.fw4'
        option position 'ruleset-pre'

config include
        option type 'nftables'
        option path '/etc/nftables.d/20-ssdp-chains.fw4'
        option position 'table-pre'

This will include two new rulesets:

  • /etc/nftables.d/00-flush-ruleset.fw4 allows to reload the firewall from the user interface. It simply flushes the entire ruleset explicitly. If you don't do this, the nftables will complain on resources being in use whenever you restart the firewall.
  • /etc/nftables.d/20-ssdp-chains.fw4 will do all the magic:
    • It will create a new chain to increase TTL.
    • It will create a chain to accept M_SEARCH messages.
    • It will create a chain to accept NOTIFY messages from random ports or in broadcasts.
    • It will use a dynamic set to accomplish this. So, no custom scripts involved anymore.
  • These rulesets need to be loaded before regular rules, because they need to be prefixed to the regular chains.

/etc/nftables.d/00-flush-ruleset.fw4 (no custom changes needed):

flush ruleset

/etc/nftables.d/20-ssdp-chains.fw4 (changes needed to adapt to your zones):

# chain mangle_forward_increase_ssdp_ttl
# create a new base chain, as not to conflict with default mangle_forward
# use same seetings as default chain
# set ttl to 2 (because increasing it by 1 is not implemented)
chain mangle_forward_increase_ssdp_ttl {
    type filter hook forward priority mangle; policy accept;
    ip daddr 239.255.255.250 udp dport 1900 counter ip ttl set 2 comment "SSDP packets: increase TTL to pass routing"
}

# chain ssdp-notify-allow
# no hook, no default policy
# jump from each server zone_forward
# return, if length is not correct
# allow traffic to triggered client.port
# return, if port is not correct
# return, if destination is not correct
# allow multicast by jumping to accept_to_zone for client zones
chain accept_ssdp_notify {
    meta length    0-250           return comment "NOTIFY packets must be longer than 250"
    ip daddr . udp dport @ssdp_expectations counter accept comment "Allow expected NOTIFY responses"
    ip daddr    != 239.255.255.250 return comment "NOTIFY packets must be broadcast to 239.255.255.250"
    udp dport   != 1900            return comment "NOTIFY packets must be broadcast to port 1900"

    # CHANGES NEEDED:
    # add a rule here for each zone, which includes SSDP clients
    # example:
    counter jump accept_to_lan
}

# chain ssdp-msearch-allow
# no hook, no default policy
# jump from each client zone_forward
# return, if length is not correct
# return, if port is not correct
# return, if destination is not correct
# update vmap ssdp-expectations
# allow multicast by jumping to accept_to_zone for server zones
chain accept_ssdp_msearch {
    meta length != 0-250           return comment "MSEARCH packets must be shorter than 250"
    ip daddr    != 239.255.255.250 return comment "MSEARCH packets must be broadcast to 239.255.255.250"
    udp dport   != 1900            return comment "MSEARCH packets must be broadcast to port 1900"
    update @ssdp_expectations { ip saddr . udp sport} counter comment "Add expected NOTIFY response destination"

    # CHANGES NEEDED:
    # add a rule here for each zone, which includes SSDP servers
    # example:
    counter jump accept_to_dmz
}

# set ssdp-expectations
# client.port
# timeout 5s
set ssdp_expectations {
    typeof ip daddr . udp dport
    size 123
    timeout 10s
}

# CHANGES NEEDED:
# add a rule here for each zone, which includes SSDP clients
# example:
chain forward_lan       { jump accept_ssdp_msearch; }

# CHANGES NEEDED:
# add a rule here for each zone, which includes SSDP clients
# example:
chain forward_dmz       { jump accept_ssdp_notify; }

This is working fine for me. I can see servers instantly. I'll leave the original post, in case someone sticks to the old firmware versions.

2 Likes

@ImTest: I have tried to setup ssdp_helper, but I did not succeed. I'd say, the problem is related to mDNS. Both share the challenge of forwarding broadcasts accross subnets. But they are two different protocols, with mDNS using fixed ports, which makes many things easier. I have mDNS working through avahi and uci firewall rules. This seems to be pretty standard. I will hint at what I did, but I cannot offer support in porting this to your setup.

Add this to /etc/avahi/avahi-daemon.conf:

[reflector]
enable-reflector=yes

Add this to /etc/config/firewall:

config rule
        list proto 'udp'
        option src '*'
        option src_port '5353'
        option dest_port '5353'
        option target 'ACCEPT'
        list dest_ip '224.0.0.251'
        list dest_ip 'ff02::fb'

This should allow mDNS between all networks. So, it is not as selective as the rules for SSPD.

Thank you very much for your option, it is very interesting. A few days ago I also managed to implement the reception/sending of M-SЕARCH and NOTIFY messages using nftables rules, but I had to do manual address substitution (NAT), because the masquerade did not work, and the server responded only to packets from its local network.

# creating a map to record the port:address of the DLNA servers requesting traffic
nft add map inet fw4 ssdp_client {type inet_service : ipv4_addr\; timeout 5s \;}
# creating a set to record the DLNA server address
nft add set inet fw4 ssdp_server {type ipv4_addr\; timeout 130s \;}

# incrementing ttl multicast packets to go to another subnet, except for source *.*.*.1 (from SNAT, see below)
nft add rule inet fw4 mangle_prerouting iifname "br-lan.30" ip daddr 239.255.255.250 udp dport 1900 ip saddr 192.168.30.0/24 ip saddr != 192.168.30.1 ip ttl set 2 counter
nft add rule inet fw4 mangle_prerouting iifname "br-lan.20" ip daddr 239.255.255.250 udp dport 1900 ip saddr 192.168.20.0/24 ip saddr != 192.168.20.1 ip ttl set 2 counter 
nft add rule inet fw4 mangle_prerouting iifname "ztosicjnqf" ip daddr 239.255.255.250 udp dport 1900 ip saddr 192.168.191.0/24 ip saddr != 192.168.191.2 ip ttl set 2 counter

#when redirecting multicast traffic (ip length != 0-255 corresponds to NOTIFY), write the DLNA server address in set 
nft add rule inet fw4 mangle_forward oifname "br-lan.30" ip daddr 239.255.255.250 udp dport 1900 ip saddr != 192.168.30.0/24 ip length != 0-255 update @ssdp_server {ip saddr} counter
nft add rule inet fw4 mangle_forward oifname "br-lan.20" ip daddr 239.255.255.250 udp dport 1900 ip saddr != 192.168.20.0/24 ip length != 0-255 update @ssdp_server {ip saddr} counter
nft add rule inet fw4 mangle_forward oifname "ztosicjnqf" ip daddr 239.255.255.250 udp dport 1900 ip saddr != 192.168.191.0/24 ip length != 0-255 update @ssdp_server {ip saddr} counter

#when redirecting multicast traffic (ip length 0-255 corresponds to M-SEARCH) to another subnet, we write it to the map and replace the source address with the address of the router interfaces (SNAT)
nft add rule inet fw4 mangle_forward oifname "br-lan.30" ip daddr 239.255.255.250 udp dport 1900 ip saddr != 192.168.30.0/24 ip length 0-255 add @ssdp_client {udp sport : ip saddr} ip saddr set 192.168.30.1 counter 
nft add rule inet fw4 mangle_forward oifname "br-lan.20" ip daddr 239.255.255.250 udp dport 1900 ip saddr != 192.168.20.0/24 ip length 0-255 add @ssdp_client {udp sport : ip saddr} ip saddr set 192.168.20.1 counter 
nft add rule inet fw4 mangle_forward oifname "ztosicjnqf" ip daddr 239.255.255.250 udp dport 1900 ip saddr != 192.168.191.0/24 ip length 0-255 add @ssdp_client {udp sport : ip saddr} ip saddr set 192.168.191.2 counter

#reverse address substitution (DNAT) + writing the DLNA server address to set
nft add rule inet fw4 mangle_prerouting iifname "br-lan.30" ip saddr 192.168.30.0/24 udp sport 1900 ip daddr 192.168.30.1 ip daddr set udp dport map @ssdp_client update @ssdp_server {ip saddr} counter
nft add rule inet fw4 mangle_prerouting iifname "br-lan.20" ip saddr 192.168.20.0/24 udp sport 1900 ip daddr 192.168.20.1 ip daddr set udp dport map @ssdp_client update @ssdp_server {ip saddr} counter
nft add rule inet fw4 mangle_prerouting iifname "ztosicjnqf" ip saddr 192.168.191.0/24 udp sport 1900 ip daddr 192.168.191.2 ip daddr set udp dport map @ssdp_client update @ssdp_server {ip saddr} counter

#allowing multicast traffic from IOT zone "br-lan.20" to other zones
nft insert rule inet fw4 forward_IOTZone oifname {"br-lan.30", "ztosicjnqf", "br-lan.20"} ip daddr 239.255.255.250 udp dport 1900 counter accept

#allowing traffic for communication with DLNA server from IOT zone "br-lan.20" to other zones
nft insert rule inet fw4 forward_IOTZone oifname {"br-lan.30", "ztosicjnqf", "br-lan.20"} ip daddr @ssdp_server udp dport 1900 counter accept
nft insert rule inet fw4 forward_IOTZone oifname {"br-lan.30", "ztosicjnqf", "br-lan.20"} ip daddr @ssdp_server tcp dport 8200 counter accept

Now the server is instantly found, but there are cases when ssdp_server is not updated for some reason, although the server should send NOTIFY packets every 1-2 minutes. This does not affect the viewing of the movie, but if I want to rewind/fast forward or select another movie, the server is unavailable, because there is no entry in ssdp_server. A possible solution is to increase the time from 130 sec to several hours. Is it correct?

This also seems quite elegant! I must admit, I haven't yet fully grasped it. :see_no_evil:

As to your issue: Maybe it's enough to simply allow access from LAN zone to IOT zone on port 8200, regardless of the destination IP? That way the actual communication wouldn't have to rely on SSDP to work. Or even (very simply) allow all access from LAN to IOT, because you trust everything that is on LAN?

PS: I don't think there is s standard interval for notify announcement. I have seen values of up to 20 minutes...

1 Like

IOT devices are not allowed to communicate with each other and with devices from the LAN network, so there is a need for known SSDP servers. I will still think about how to transform this code better! Thank you very much for the answers)