A Linux command-line utility to discover and list WSD-enabled computers and printers on a home network
July 8, 2020 5 Comments
In an earlier post I covered the installation and use of wsdd
, a WS-Discovery (WSD) daemon that can run on Linux machines and enable machines running Microsoft Windows 10 to discover Linux machines in File Explorer now that Windows 10 has dropped Computer Browser, NetBIOS and SMBv1. All my Linux machines in my home network have wsdd
running alongside NetBIOS broadcast name resolution, SMBv2 (used by my Android phone) and SMBv3 (used by my Linux machines). If any visitors to my house happen to bring a laptop running Windows 10, they will be able to discover my SMB shares in File Explorer, which I have always been able to do in Linux and in earlier Windows releases that supported NetBIOS and Computer Browser.
As I pointed out in a comment to another of my earlier posts, a downside of not using the (insecure) SMBv1 protocol is that the Samba utility smbtree
incorrectly returns nothing if you enter the command smbtree
when using SMBv2 or SMBv3. As all the Linux machines in my home network are running the wsdd
daemon in addition to NetBIOS, SMBv2 and SMBv3 — and any visitors’ laptops could be running Windows 10 — it would be nice to have a command-line utility that would discover all machines. Well, here is a stab at such a utility, written by a close relative of mine as a learning exercise in WSD and Python, and is provided here as-is without any warranty or support. It consists of the following five files:
wsd-discover.sh
#!/bin/bash function del-tmp-files() { if ls /tmp/wsd-*.txt 1> /dev/null 2>&1; then rm /tmp/wsd-* fi return 0 } # Delete pre-existing temporary work files. del-tmp-files # Get the V5 UUID of this machine UUID=$(python3 $HOME/discover/wsd-gen-uuid.py) # Send a multicast probe to all WSD capable devices and store the XML output in wsd-probe1.txt echo echo "Please wait.....sending multicast discovery probe and waiting 2 seconds for responses" echo python3 $HOME/discover/wsd-mcast-probe.py > /tmp/wsd-probe1.txt # Iterate through the XML until the UUID to IPv4 mappings are obtained in wsd-probe9.txt more /tmp/wsd-probe1.txt | grep Computer | awk -F "<wsa:Address>" '{print $2}' > /tmp/wsd-probe2.txt sort -u /tmp/wsd-probe2.txt > /tmp/wsd-probe3.txt more /tmp/wsd-probe3.txt | awk -F "uuid:" '{print $2}' > /tmp/wsd-probe4.txt more /tmp/wsd-probe4.txt | awk -F "</wsa:Address>" '{print $1,"******",$2}' > /tmp/wsd-probe5.txt more /tmp/wsd-probe5.txt | awk -F "from" '{print $1,"******",$2}' > /tmp/wsd-probe6.txt more /tmp/wsd-probe6.txt | awk -F "******" '{print $1 $3}' > /tmp/wsd-probe7.txt more /tmp/wsd-probe7.txt | awk -F "\\\('" '{print $1 $2}' > /tmp/wsd-probe8.txt more /tmp/wsd-probe8.txt | awk -F "'" '{print $1}' > /tmp/wsd-probe9.txt # Read the UUID to IPv4 mappings until end of file and send XML requests to each WSD host while read RECORD; do URN=$(echo $RECORD | cut -d" " -f1) IPA=$(echo $RECORD | cut -d" " -f2) # Generate the HTTP/XML request file from the template cat $HOME/discover/wsd-template.xml | sed 's/XXXXXXXXXX/'$URN'/g' > /tmp/wsd-request.txt cat /tmp/wsd-request.txt | sed -i 's/YYYYYYYYYY/'$UUID'/g' /tmp/wsd-request.txt # Send the XML/SOAP request to the target machine curl -s -A wsd --header "Accept-Encoding: identity" --header "Connection: Close" \ --header "Content-Type: application/soap+xml" --header "User-Agent: wsd" \ --data @/tmp/wsd-request.txt http://$IPA:5357/$URN > /tmp/wsd-response-$IPA.txt # Extract, format and display the information returned echo echo "Device IP : $IPA" echo "===========================" echo -n "Name :";cat /tmp/wsd-response-$IPA.txt | awk -F "FriendlyName" '{print $2}' | awk -F "<" '{print $1}' | cut -d">" -f2 echo -n "Manufacturer :";cat /tmp/wsd-response-$IPA.txt | awk -F "Manufacturer" '{print $2}' | awk -F "<" '{print $1}' | cut -d">" -f2 echo -n "Model :";cat /tmp/wsd-response-$IPA.txt | awk -F "ModelName" '{print $2}' | awk -F "<" '{print $1}' | cut -d">" -f2 echo -n "Category :";cat /tmp/wsd-response-$IPA.txt | awk -F "DeviceCategory" '{print $2}' | awk -F "<" '{print $1}' | cut -d">" -f2 echo -n "URN :";cat /tmp/wsd-response-$IPA.txt | awk -F "Address" '{print $2}' | awk -F "<" '{print $1}' | cut -d">" -f2 echo -n "Type :";cat /tmp/wsd-response-$IPA.txt | awk -F "Types" '{print $2}' | awk -F "<" '{print $1}' | cut -d">" -f2 echo -n "Workgroup :";cat /tmp/wsd-response-$IPA.txt | awk -F "<pub:Computer" '{print $2}' | awk -F "<" '{print $1}' | cut -d">" -f2 echo done < /tmp/wsd-probe9.txt # This next bit is just a bit of fluff to display printers. The formatting is inconsistent because every printer # has a different web page. Printer manufacturers are listed in the file $HOME/discover/printers.txt. If the printer # is not in this file it won't be found in the HTTP information # Check whether the original multicast response contains any printer information cat /tmp/wsd-probe1.txt | grep -q -A2 Print if [[ $? -eq 0 ]]; then # A printer of some sort has been found # Get the line that contains 'Print' and the two lines after it (one of which contains the printer IP and URL) more /tmp/wsd-probe1.txt | grep -A2 Print > /tmp/wsd-probe10.txt # Remove any duplicate entries sort -u /tmp/wsd-probe10.txt > /tmp/wsd-probe11.txt # Isolate the printer IP and URL information cat /tmp/wsd-probe11.txt | awk -F"XAddrs>" '{print $2}' | awk -F"/wsd" '{print $1}' > /tmp/wsd-probe12.txt # Remove blank lines to clean up the file sed '/^$/d' /tmp/wsd-probe12.txt > /tmp/wsd-probe13.txt # Read each line of the file containing the printer URLs and contact the printers in turn while read RECORD; do echo "Printers" echo "===========================" URL=$RECORD # Try to get the printer's HTML page curl -s $URL/index.html > /tmp/wsd-printer.txt if [[ $? -ne 0 ]]; then echo "Couldn't get HTML info from $URL" else # Read each line of the printers.txt file and try to get the Make and Model from the HTML while read PRT; do grep -q $PRT /tmp/wsd-printer.txt if [[ $? -eq 0 ]]; then # Printer in the list is contained in the returned HTML # Extract the Make and the following word hoping it's the Model TYP=$(grep $PRT /tmp/wsd-printer.txt | awk -v a=$PRT '{for(i=1;i<=NF;i++) if ($i==a) print $i,$(i+1)}') echo "URL : $URL" echo "Make : $TYP" fi done < $HOME/discover/printers.txt fi done < /tmp/wsd-probe13.txt fi echo # # Delete the latest temporary work files. # del-tmp-files
wsd-gen-uuid.py
import uuid import socket hostName = (socket.gethostname()) # nameSpaces = [uuid.NAMESPACE_DNS, uuid.NAMESPACE_URL, uuid.NAMESPACE_OID, uuid.NAMESPACE_X500] nameSpaces = [uuid.NAMESPACE_DNS] for namespace in nameSpaces: print (uuid.uuid5(namespace, hostName))
wsd-mcast-probe.py
import socket import struct import sys import uuid # Create a V1 UUID for the MessageID based on the host address and current time # The MessageID must be unique but it isn't necessary to have anything other than a V1 UUID uuid1 = uuid.uuid1() myuuid = str(uuid1) print ("Generating UUID for MessageID") print(myuuid) # The string 'message' is a template WSD probe that is multicast to group 239.255.255.250 port 3702 # The template should not change unless there is a major change to the WSD specifications # Escape double quotation marks within the message string (but not the outer double quotation marks) message = "<?xml version=\"1.0\" encoding=\"utf-8\"?><soap:Envelope xmlns:pnpx=\"http://schemas.microsoft.com/windows/pnpx/2005/10\" xmlns:pub=\"http://schemas.microsoft.com/windows/pub/2005/07\" xmlns:soap=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:wsa=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\" xmlns:wsd=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\" xmlns:wsdp=\"http://schemas.xmlsoap.org/ws/2006/02/devprof\" xmlns:wsx=\"http://schemas.xmlsoap.org/ws/2004/09/mex\"><soap:Header><wsa:To>urn:schemas-xmlsoap-org:ws:2005:04:discovery</wsa:To><wsa:Action>http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</wsa:Action><wsa:MessageID>urn:uuid:" + myuuid + "</wsa:MessageID></soap:Header><soap:Body><wsd:Probe><wsd:Types>wsdp:Device</wsd:Types></wsd:Probe></soap:Body></soap:Envelope>" # Convert the message to a UTF-8 byte string bytstr = message.encode('utf-8') # Define a variable for the multicast group and multicast destination port multicast_group = ('239.255.255.250', 3702) multicast_address = '239.255.255.250' # Cheeky way to get the Internet facing Ethernet IP address for use further down # Create a socket, pretend to use it to connect to an Internet service. Nothing is actually sent # but the IP address of the Internet facing interface is returned def get_ip_address(): sock1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock1.connect(("8.8.8.8", 80)) return sock1.getsockname()[0] # Create datagram socket 1 for multicasts and allow the IP address and port to be reused in case something # else is using them e.g. the WSD service sock1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) IPADDR = (get_ip_address()) # Set the multicasts TTL to 1 so they stay on the local segment sock1.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 1) # Set a timeout so the socket stops listening if no data is received within the timeout # This prevents it from locking up sock1.settimeout(2.0) # Bind the socket to the IP and port that we wish to use as the source IP and port of datagrams we transmit # AND the destination IP and port of datagrams that we receive sock1.bind ((IPADDR, 3702)) # Join the 239.255.255.250 multicast group. This isn't necessary if this script is being run on a machine # that is also running the wsdd daemon. Joining the multicast group allows the script to be run on any # machine regardless mreq = struct.pack("4sl", socket.inet_aton(multicast_address), socket.INADDR_ANY) sock1.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) try: # Send the WSD probe (bytstr) to the multicast group and port # print ('\nsending "%s"' % bytstr) sent = sock1.sendto(bytstr, multicast_group) # Listen for up to 4096 byte responses from all responders to the multicast message while True: print ('\nwaiting to receive responses') try: data, addr = sock1.recvfrom(4096) # We could use the format below to split 'addr' into its component IP and port fields but is isn't necessary # data, (ip, port) = sock1.recvfrom (4096) except: # This exception only occurs if no data is received on socket for the timeout period print ('\ntimed out, no more responses socket1') break else: # This is the response data that the bash script writes out to the wsd-probe1.txt file print ('\nreceived %s from %s' % (data.decode('utf-8'), addr)) finally: print ('\nsocket closed\n')
wsd-template.xml
<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:pnpx="http://schemas.microsoft.com/windows/pnpx/2005/10" xmlns:pub="http://schemas.microsoft.com/windows/pub/2005/07" xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:wsd="http://schemas.xmlsoap.org/ws/2005/04/discovery" xmlns:wsdp="http://schemas.xmlsoap.org/ws/2006/02/devprof" xmlns:wsx="http://schemas.xmlsoap.org/ws/2004/09/mex"> <soap:Header> <wsa:To>urn:uuid:XXXXXXXXXX</wsa:To> <wsa:Action>http://schemas.xmlsoap.org/ws/2004/09/transfer/Get</wsa:Action> <wsa:MessageID>urn:uuid:fe11d044-bc13-11ea-b98c-2c56dc778d37</wsa:MessageID> <wsa:ReplyTo> <wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address> </wsa:ReplyTo> <wsa:From> <wsa:Address>urn:uuid:YYYYYYYYYY</wsa:Address> </wsa:From> </soap:Header> <soap:Body /> </soap:Envelope>
printers.txt
Brother Canon Epson HP Kodak Lexmark
How to install
user $ mkdir $HOME/discover
Use a text editor to create the five files listed above in the directory $HOME/discover/
.
Make the Bash script and the Python scripts executable:
user $ chmod u+x $HOME/discover/*.sh $HOME/discover/*.py
How to use
user $ $HOME/discover/wsd-discover.sh
The script will list discovered devices (see the caveat in the Description section below). For example:
user $ $HOME/discover/wsd-discover.sh
Please wait.....sending multicast discovery probe and waiting 2 seconds for responses
Device IP : 192.168.1.121
===========================
Name :WSD Device tutankhamun
Manufacturer :wsdd
Model :wsdd
Category :Computers
URN :urn:uuid:ff03f853-8a45-5ad9-b75b-fe4f632c8c5b
Type :pub:Computer
Workgroup :TUTANKHAMUN/Workgroup:HOME
Device IP : 192.168.1.10
===========================
Name :WSD Device akhanaten
Manufacturer :wsdd
Model :wsdd
Category :Computers
URN :urn:uuid:ad8fedfb-a22c-5551-92b4-653aae69f379
Type :pub:Computer
Workgroup :AKHANATEN/Workgroup:HOME
Device IP : 192.168.1.74
===========================
Name :WSD Device thutmoseiii
Manufacturer :wsdd
Model :wsdd
Category :Computers
URN :urn:uuid:9bf49ac3-e58d-57a4-87ea-7c0d5ef02234
Type :pub:Computer
Workgroup :THUTMOSEIII/Workgroup:HOME
Printers
===========================
URL : http://192.168.1.78:80
Make : Canon MP560
The example output above was for a network of three Linux machines running the wsdd
daemon, connected via Ethernet, plus a printer connected via Wi-Fi.
Description
The scripts are non-intrusive and discover WSD-enabled devices in multicast group 239.255.255.250 port 3702, namely a) Windows 10 and b) other Linux machines running the WSD daemon wsdd
or other WSD software. It runs over Ethernet and Wi-Fi. The script joins the multicast group (with a reusable socket) and sends out a WSD Probe. The responses contain the UUID-to-IP address mappings of the devices it discovers. Each discovered device is then contacted individually on its IP address TCP port 5357 to retrieve basic information.
If you run the script on Linux with the WSD Daemon (wsdd
) also running (see earlier post), the script discovers itself as well as other devices. If you run the script on a machine that is not running the WSD Daemon it still discovers other devices, but not itself.
The script also discovers any WSD-enabled printers that listen for multicasts on UPnP / SSDP group 239.255.255.250 but don’t care about what UDP port is being used. If a WSD-enabled printer is detected, the script attempts to retrieve the make and model of the printer using HTTP. To detect different printer makes, add the manufacturer e.g. Canon, Epson, Lexmark etc. to the file ‘printers.txt
‘. The script reports on the printer make and tries to extract the model type. It may not always format the output 100% accurately.
The main thing to bear in mind is that the scripts do not maintain state i.e. a single discovery probe is transmitted. Multicast is fundamentally unreliable and only devices that respond are reported. If the probe is lost or an end device doesn’t respond, for whatever reason, it doesn’t get reported. You can run the script a few times to ensure that it picks up as many of the devices as it possibly can.
To launch
wsd-discover.sh
easily from the GUI, I have an icon on the Desktop in my Lubuntu 18.04 (LXDE) and Gentoo Linux (KDE) installations. The Desktop Configuration filesDiscover_WSD_devices.desktop
for the two distributions/DEs are listed below:[Desktop Entry]
Comment[en_GB]=Discover WSD devices
Comment=Discover WSD devices
Exec=lxterminal -e "/home/fitzcarraldo/discover/wsd-discover.sh; echo -n 'Press any key to close window.'; read -n 1 -s"
GenericName[en_GB]=Discover_WSD_devices
GenericName=Discover_WSD_devices
Icon=/home/fitzcarraldo/Pictures/Icons/Crystal_Clear/png/actions/find.png
MimeType=
Name[en_GB]=Discover_WSD_devices
Name=Discover_WSD_devices
Path=/home/fitzcarraldo
StartupNotify=true
Terminal=true
TerminalOptions=
Type=Application
X-DBUS-ServiceName=
X-DBUS-StartupType=none
X-LXDE-SubstituteUID=false
X-LXDE-Username=fitzcarraldo
X-KeepTerminal=true
[Desktop Entry]
Comment[en_GB]=Discover WSD devices
Comment=Discover WSD devices
Exec=bash /home/fitzcarraldo/discover/wsd-discover.sh; echo -n 'Press any key to close window.'; read -n 1 -s
GenericName[en_GB]=Discover_WSD_devices
GenericName=Discover_WSD_devices
Icon=/home/fitzcarraldo/Pictures/Icons/Crystal_Clear/png/actions/find.png
MimeType=
Name[en_GB]=Discover_WSD_devices
Name=Discover_WSD_devices
Path=/home/fitzcarraldo
StartupNotify=true
Terminal=true
TerminalOptions=
Type=Application
X-DBUS-ServiceName=
X-DBUS-StartupType=none
X-KDE-SubstituteUID=false
X-KDE-Username=fitzcarraldo
X-KeepTerminal=true
Pingback: Moving from Lubuntu 18.04 to 20.10 | Fitzcarraldo's Blog
If you also use the wsdd daemon (see my June 2020 post ‘Using WS-Discovery to enable Windows 10 to browse SMB shares in my home network of Linux computers’) and you find that running the script wsd-discover.sh results in wsd-mcast-probe.py crashing at Line 50, this is due to a bug in wsdd 0.6.4 (and possibly 0.6.2 and 0.6.3 too, as I know 0.6.1 worked fine) which was fixed by the wsdd developer on 19 March 2021 (https://github.com/christgau/wsdd/issues/95). If you’re running *buntu then you can update wsdd from the Master branch to benefit from the bug fix:
$ wget https://github.com/christgau/wsdd/archive/refs/heads/master.zip
$ unzip master.zip
$ sudo cp ~/wsdd-master/src/wsdd.py /usr/bin/wsdd
$ sudo systemctl restart wsdd
If you’re using Gentoo Linux then either wait for net-misc/wsdd-0.6.5 to be added to the wsdd developer’s overlay (guru) or install net-misc/wsdd-9999 (https://github.com/christgau/wsdd-gentoo/tree/master/net-misc/wsdd) which will install the latest wsdd code from the Master branch.
If you use Gentoo Linux and want to install the live (‘Master’) version of wsdd, you can download wsdd-9999.ebuild from the wsdd GitHub repository and install wsdd-9999 via a local overlay. See https://wiki.gentoo.org/wiki/Handbook:AMD64/Portage/CustomTree#Defining_a_custom_ebuild_repository if you don’t know how to create a local overlay. My local overlay is named ‘local_overlay’ and is in the directory /usr/local/portage/. This is what I did to install wsdd-9999 in my laptop that runs Gentoo Testing (~amd64):
# cd /usr/local/portage/net-misc/wsdd/
# wget https://raw.githubusercontent.com/christgau/wsdd-gentoo/master/net-misc/wsdd/wsdd-9999.ebuild
# ebuild wsdd-9999.ebuild manifest
# echo "=net-misc/wsdd-9999 **" >> /etc/portage/package.accept_keywords/wsdd # (or whatever your keywords mask file is called)
# emerge wsdd
# rc-service wsdd restart
Any chance you can get this working for a user using sudo on cmd line? I tried it and it runs but the multicast python file doesn’t print anything (literally anything) after it searches and I can’t readily see where the .txt file it generates is if I try it from “sudo -i” (Switching to root in pseudo terminal.)
Why are you trying to run it using sudo? It works fine run as a normal user from the command line:
fitzcarraldo@aspirexc600:~$ cd discover/
fitzcarraldo@aspirexc600:~/discover$ ls
printers.txt wsd-discover.sh wsd-gen-uuid.py wsd-mcast-probe.py wsd-template.xml
fitzcarraldo@aspirexc600:~/discover$ ./wsd-discover.sh
Please wait... sending multicast discovery probe and waiting 2 seconds for responses
Device IP : 192.168.0.145
===========================
Name :WSD Device meshedgedx
Manufacturer :wsdd
Model :wsdd
Category :Computers
URN :urn:uuid:b628bf33-9a86-5da6-b8c1-998e7dbf0abd
Type :pub:Computer
Workgroup :MESHEDGEDX/Workgroup:HOME
Device IP : 192.168.0.55
===========================
Name :WSD Device aspirexc600
Manufacturer :wsdd
Model :wsdd
Category :Computers
URN :urn:uuid:fb46b5fb-4623-577c-a6d0-ce3e62ab7949
Type :pub:Computer
Workgroup :ASPIREXC600/Workgroup:HOME
Press any key to close window.fitzcarraldo@aspirexc600:~/discover$