From 0ec14dadd499002a9071d64252190e283aa5274f Mon Sep 17 00:00:00 2001 From: Jocelyn Delalande Date: Mon, 19 Jun 2017 16:49:48 +0200 Subject: [PATCH 1/6] Add json output mode --- ubnt_discovery.py | 49 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/ubnt_discovery.py b/ubnt_discovery.py index 80e2a1b..dd6eb54 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -6,6 +6,8 @@ # www.bvnetworks.it # ########################################## +import argparse +import json from random import randint from scapy.all import * @@ -35,6 +37,16 @@ DISCOVERY_TIMEOUT = 5 +def parse_args(): + parser = argparse.ArgumentParser( + description="Discovers ubiquiti devices on network using ubnt device discovery protocol") + parser.add_argument( + '--output-format', type=str, default='text', choices=('text', 'json'), + help="output format") + + return parser.parse_args() + + def ubntDiscovery(): # Prepare and send our discovery packet @@ -109,24 +121,31 @@ def ubntDiscovery(): Radio['model'] = RadioModel Radio['essid'] = RadioEssid Radio['firmware'] = RadioFirmware + Radio['uptime'] = RadioUptime Radio['model_short'] = RadioModelShort + Radio['wlan_mode'] = RadioWlanMode RadioList.append(Radio) return RadioList -print("\nDiscovery in progress...") -RadioList = ubntDiscovery() -found_radios = len(RadioList) -if found_radios: - print("\nDiscovered " + str(found_radios) + " radio(s):") - for Radio in RadioList: - print("\n--- [" + Radio['model'] + "] ---") - print(" IP Address : " + Radio['ip']) - print(" Name : " + Radio['name']) - print(" Model : " + Radio['model_short']) - print(" Firmware : " + Radio['firmware']) - print(" ESSID : " + Radio['essid']) - print(" MAC Address : " + Radio['mac']) -else: - print("\nNo radios discovered\n") +if __name__ == '__main__': + args = parse_args() + print("\nDiscovery in progress...") + RadioList = ubntDiscovery(args.interface) + found_radios = len(RadioList) + if args.output_format == 'text': + if found_radios: + print("\nDiscovered " + str(found_radios) + " radio(s):") + for Radio in RadioList: + print("\n--- [" + Radio['model'] + "] ---") + print(" IP Address : " + Radio['ip']) + print(" Name : " + Radio['name']) + print(" Model : " + Radio['model_short']) + print(" Firmware : " + Radio['firmware']) + print(" ESSID : " + Radio['essid']) + print(" MAC Address : " + Radio['mac']) + else: + print("\nNo radios discovered\n") + elif args.output_format == 'json': + print(json.dumps(RadioList, indent=2)) From 73116330c40acc6e5a2515a023f54cfef9714601 Mon Sep 17 00:00:00 2001 From: Jocelyn Delalande Date: Mon, 19 Jun 2017 16:51:09 +0200 Subject: [PATCH 2/6] Allow interface selection --- ubnt_discovery.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/ubnt_discovery.py b/ubnt_discovery.py index dd6eb54..848e67a 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -9,7 +9,11 @@ import argparse import json from random import randint -from scapy.all import * + +from scapy.all import ( + Ether, IP, UDP, Raw, + get_if_hwaddr, get_if_list, conf, srp) + # UBNT field types UBNT_MAC = '01' @@ -40,6 +44,8 @@ def parse_args(): parser = argparse.ArgumentParser( description="Discovers ubiquiti devices on network using ubnt device discovery protocol") + parser.add_argument( + 'interface', help="the interface you want to use for discovery") parser.add_argument( '--output-format', type=str, default='text', choices=('text', 'json'), help="output format") @@ -47,14 +53,20 @@ def parse_args(): return parser.parse_args() -def ubntDiscovery(): +def ubntDiscovery(iface): + + if not iface in get_if_list(): + raise ValueError('{} is not a valid network interface'.format(iface)) + + src_mac = get_if_hwaddr(iface) # Prepare and send our discovery packet conf.checkIPaddr = False # we're broadcasting our discovery packet from a local IP (local->255.255.255.255) # but we'll expect a reply on the broadcast IP as well (radioIP->255.255.255.255), # not on our local IP. # Therefore we must disable destination IP checking in scapy - ubnt_discovery_packet = Ether(dst="ff:ff:ff:ff:ff:ff")/\ + conf.iface = iface + ubnt_discovery_packet = Ether(dst="ff:ff:ff:ff:ff:ff", src=src_mac)/\ IP(dst="255.255.255.255")/\ UDP(sport=randint(1024,65535),dport=10001)/\ Raw(UBNT_REQUEST_PAYLOAD.decode('hex')) From 3ea202459abc5fdb90f4b9eef8163d9e668322c5 Mon Sep 17 00:00:00 2001 From: Jocelyn Delalande Date: Mon, 19 Jun 2017 16:51:28 +0200 Subject: [PATCH 3/6] Do not enter promiscuous mode. That is not required, so better avoid messing with ethernet iface. --- ubnt_discovery.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ubnt_discovery.py b/ubnt_discovery.py index 848e67a..f68dab9 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -65,6 +65,7 @@ def ubntDiscovery(iface): # but we'll expect a reply on the broadcast IP as well (radioIP->255.255.255.255), # not on our local IP. # Therefore we must disable destination IP checking in scapy + conf.sniff_promisc=False conf.iface = iface ubnt_discovery_packet = Ether(dst="ff:ff:ff:ff:ff:ff", src=src_mac)/\ IP(dst="255.255.255.255")/\ From b32435c791036dc8552d6c6a23002035903bdbfc Mon Sep 17 00:00:00 2001 From: Jocelyn Delalande Date: Mon, 19 Jun 2017 16:52:25 +0200 Subject: [PATCH 4/6] Add uptime and wlan mode fields --- ubnt_discovery.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/ubnt_discovery.py b/ubnt_discovery.py index f68dab9..54d3ab8 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -19,11 +19,11 @@ UBNT_MAC = '01' UBNT_MAC_AND_IP = '02' UBNT_FIRMWARE = '03' -UBNT_UNKNOWN_2 = '0a' +UBNT_UPTIME = '0a' UBNT_RADIONAME = '0b' UBNT_MODEL_SHORT = '0c' UBNT_ESSID = '0d' -UBNT_UNKNOWN_3 = '0e' +UBNT_WLAN_MODE = '0e' UBNT_UNKNOWN_1 = '10' UBNT_MODEL_FULL = '14' @@ -31,6 +31,18 @@ UBNT_REQUEST_PAYLOAD = '01000000' UBNT_REPLY_SIGNATURE = '010000' + +# Wirelss modes +UBNT_WIRELESS_MODES ={ + '\x00': "Auto", + '\x01': "adhoc", + '\x02': "Station", + '\x03': "AP", + '\x04': "Repeater", + '\x05': "Secondary", + '\x06': "Monitor", +}; + # Offset within the payload that contains the amount of bytes remaining offset_PayloadRemainingBytes = 3 @@ -121,8 +133,12 @@ def ubntDiscovery(iface): RadioModelShort = fieldData elif fieldType == UBNT_FIRMWARE: RadioFirmware = fieldData + elif fieldType == UBNT_UPTIME: + RadioUptime = int(fieldData.encode('hex'), 16) elif fieldType == UBNT_ESSID: RadioEssid = fieldData + elif fieldType == UBNT_WLAN_MODE: + RadioWlanMode = UBNT_WIRELESS_MODES[fieldData] # We don't know or care about other field types. Continue walking the payload. pointer += fieldLen remaining_bytes -= fieldLen From 72d413345f25d978655546e33d710e83c6f6baca Mon Sep 17 00:00:00 2001 From: Jocelyn Delalande Date: Mon, 19 Jun 2017 18:28:17 +0200 Subject: [PATCH 5/6] Write error/debug to stderr to avoid putting garbage into json --- ubnt_discovery.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ubnt_discovery.py b/ubnt_discovery.py index 54d3ab8..60d264f 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -9,6 +9,7 @@ import argparse import json from random import randint +import sys from scapy.all import ( Ether, IP, UDP, Raw, @@ -160,7 +161,7 @@ def ubntDiscovery(iface): if __name__ == '__main__': args = parse_args() - print("\nDiscovery in progress...") + sys.stderr.write("\nDiscovery in progress...\n") RadioList = ubntDiscovery(args.interface) found_radios = len(RadioList) if args.output_format == 'text': @@ -175,6 +176,6 @@ def ubntDiscovery(iface): print(" ESSID : " + Radio['essid']) print(" MAC Address : " + Radio['mac']) else: - print("\nNo radios discovered\n") + sys.stderr.write("\n\nNo radios discovered\n") elif args.output_format == 'json': print(json.dumps(RadioList, indent=2)) From 0fdeb10783f72987d5489d28579c8cdf8e9b6cf9 Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Wed, 27 Nov 2019 17:29:07 +0300 Subject: [PATCH 6/6] python3, mac-ip pairs and refactor This commit: 1. switches to python 3. 2. refactors code such that it is more flexible to add field data parsers and has less duplicate code. 3. parses mac-ip pairs data. 4. reports all known fields in text mode. --- ubnt_discovery.py | 175 +++++++++++++++++++++++----------------------- 1 file changed, 88 insertions(+), 87 deletions(-) diff --git a/ubnt_discovery.py b/ubnt_discovery.py index 60d264f..4834988 100644 --- a/ubnt_discovery.py +++ b/ubnt_discovery.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 ########################################## # UBNT command line discovery tool # @@ -10,45 +10,64 @@ import json from random import randint import sys +from struct import unpack from scapy.all import ( Ether, IP, UDP, Raw, get_if_hwaddr, get_if_list, conf, srp) - -# UBNT field types -UBNT_MAC = '01' -UBNT_MAC_AND_IP = '02' -UBNT_FIRMWARE = '03' -UBNT_UPTIME = '0a' -UBNT_RADIONAME = '0b' -UBNT_MODEL_SHORT = '0c' -UBNT_ESSID = '0d' -UBNT_WLAN_MODE = '0e' -UBNT_UNKNOWN_1 = '10' -UBNT_MODEL_FULL = '14' - -# UBNT discovery packet payload and reply signature -UBNT_REQUEST_PAYLOAD = '01000000' -UBNT_REPLY_SIGNATURE = '010000' - +def mac_repr(data): + return ':'.join(('%02x' % b) for b in data) +def ip_repr(data): + return '.'.join(('%d' % b) for b in data) # Wirelss modes UBNT_WIRELESS_MODES ={ - '\x00': "Auto", - '\x01': "adhoc", - '\x02': "Station", - '\x03': "AP", - '\x04': "Repeater", - '\x05': "Secondary", - '\x06': "Monitor", + 0x00: "Auto", + 0x01: "adhoc", + 0x02: "Station", + 0x03: "AP", + 0x04: "Repeater", + 0x05: "Secondary", + 0x06: "Monitor", }; -# Offset within the payload that contains the amount of bytes remaining -offset_PayloadRemainingBytes = 3 +# field type -> (field name; parsing function (bytes->str); \ +# is it expected to be seen multiple times?) +FIELD_PARSERS = { + 0x01: ('mac2', mac_repr, False), + 0x02: ('mac_ip', lambda data: '%s;%s' % (mac_repr(data[0:6]), + ip_repr(data[6:10])), True), + 0x03: ('firmware', bytes.decode, False), + 0x0a: ('uptime', lambda data: int.from_bytes(data, 'big'), False), + 0x0b: ('name', bytes.decode, False), + 0x0c: ('model_short', bytes.decode, False), + 0x0d: ('essid', bytes.decode, False), + 0x0e: ('wlan_mode', lambda data: + UBNT_WIRELESS_MODES.get(data[0], 'unknown'), False), + 0x10: ('unknown1', str, False), + 0x14: ('model', bytes.decode, False), +} + +# Basic fields: src MAC and IP of reply message; not parsed +BASIC_FIELDS = { 'mac', 'ip' } + +# String representation of non-basic fields +FIELD_STR = { + 'mac2': 'MAC 2', + 'mac_ip': 'MAC-IP Pairs', + 'firmware': 'Firmware', + 'uptime': 'Uptime', + 'name': 'Name', + 'model_short': 'Model (short)', + 'essid': 'ESSID', + 'wlan_mode':'WLAN Mode', + 'model': 'Model', +} -# Offset within the payload where we'll find the first field -offset_FirstField = 4 +# UBNT discovery packet payload and reply signature +UBNT_REQUEST_PAYLOAD = b'\x01\x00\x00\x00' +UBNT_REPLY_SIGNATURE = b'\x01\x00\x00' # Discovery timeout. Change this for quicker discovery DISCOVERY_TIMEOUT = 5 @@ -65,6 +84,15 @@ def parse_args(): return parser.parse_args() +def iter_fields(data, _len): + pointer = 0 + while pointer < _len: + fieldType, fieldLen = unpack('>BH', data[pointer:pointer+3]) + pointer += 3 + fieldData = data[pointer:pointer+fieldLen] + pointer += fieldLen + yield fieldType, fieldData + def ubntDiscovery(iface): @@ -83,7 +111,7 @@ def ubntDiscovery(iface): ubnt_discovery_packet = Ether(dst="ff:ff:ff:ff:ff:ff", src=src_mac)/\ IP(dst="255.255.255.255")/\ UDP(sport=randint(1024,65535),dport=10001)/\ - Raw(UBNT_REQUEST_PAYLOAD.decode('hex')) + Raw(UBNT_REQUEST_PAYLOAD) ans, unans = srp(ubnt_discovery_packet, multi=True, # We want to allow multiple radios to reply to our discovery packet verbose=0, # Suppress scapy output @@ -97,85 +125,58 @@ def ubntDiscovery(iface): payload = rcv[IP].load # Check for a valid UBNT discovery reply (first 3 bytes of the payload should be \x01\x00\x00) - if payload[0:3].encode('hex') == UBNT_REPLY_SIGNATURE: + if payload[0:3] == UBNT_REPLY_SIGNATURE: Radio = {} # This should be a valid discovery reply packet sent by an Ubiquiti radio else: continue # Not a valid UBNT discovery reply, skip to next received packet - RadioIP = rcv[IP].src # We avoid going through the hassle of enumerating type '02' fields (MAC+IP). There may + Radio['ip'] = \ + rcv[IP].src # We avoid going through the hassle of enumerating type '02' fields (MAC+IP). There may # be multiple IPs on the radio, and therefore multiple type '02' fields in the # reply packet. We conveniently pick the address from which the radio # replied to our discovery request directly from the reply packet, and store it. - RadioMAC = rcv[Ether].src # Read comment above, this time regarding the MAC Address. - RadioMAC = RadioMAC.upper() - - # Retrieve payload size (excluding initial signature) - pointer = offset_PayloadRemainingBytes - remaining_bytes = int( payload[pointer].encode('hex'), 16 ) + Radio['mac'] = rcv[Ether].src.upper() # Read comment above, this time regarding the MAC Address. # Walk the reply payload, staring from offset 04 (just after reply signature and payload size). - pointer += 1 - remaining_bytes -= 1 - while remaining_bytes > 0: - fieldType = payload[pointer].encode('hex') - pointer += 1 - remaining_bytes -= 1 - fieldLen = payload[pointer:pointer+2].encode('hex') # Data length is stored as a 16-bit word - fieldLen = int( fieldLen, 16 ) - pointer += 2 - remaining_bytes -= 2 - fieldData = payload[pointer:pointer+fieldLen] - if fieldType == UBNT_RADIONAME: - RadioName = fieldData - elif fieldType == UBNT_MODEL_FULL: - RadioModel = fieldData - elif fieldType == UBNT_MODEL_SHORT: - RadioModelShort = fieldData - elif fieldType == UBNT_FIRMWARE: - RadioFirmware = fieldData - elif fieldType == UBNT_UPTIME: - RadioUptime = int(fieldData.encode('hex'), 16) - elif fieldType == UBNT_ESSID: - RadioEssid = fieldData - elif fieldType == UBNT_WLAN_MODE: - RadioWlanMode = UBNT_WIRELESS_MODES[fieldData] - # We don't know or care about other field types. Continue walking the payload. - pointer += fieldLen - remaining_bytes -= fieldLen + # Take into account the payload length in offset 3 + for fieldType, fieldData in iter_fields(payload[4:], payload[3]): + + if fieldType not in FIELD_PARSERS: + sys.stderr.write("notice: unknown field type 0x%x: data %s\n" % + (fieldType, fieldData)) + continue + + # Parse the field and store in Radio + fieldName, fieldParser, isMany = FIELD_PARSERS[fieldType] + if isMany: + if fieldName not in Radio: Radio[fieldName] = [] + Radio[fieldName].append(fieldParser(fieldData)) + else: + Radio[fieldName] = fieldParser(fieldData) # Store the data we gathered from the reply packet - Radio['ip'] = RadioIP - Radio['mac'] = RadioMAC - Radio['name'] = RadioName - Radio['model'] = RadioModel - Radio['essid'] = RadioEssid - Radio['firmware'] = RadioFirmware - Radio['uptime'] = RadioUptime - Radio['model_short'] = RadioModelShort - Radio['wlan_mode'] = RadioWlanMode RadioList.append(Radio) return RadioList - if __name__ == '__main__': args = parse_args() sys.stderr.write("\nDiscovery in progress...\n") RadioList = ubntDiscovery(args.interface) found_radios = len(RadioList) if args.output_format == 'text': - if found_radios: - print("\nDiscovered " + str(found_radios) + " radio(s):") - for Radio in RadioList: - print("\n--- [" + Radio['model'] + "] ---") - print(" IP Address : " + Radio['ip']) - print(" Name : " + Radio['name']) - print(" Model : " + Radio['model_short']) - print(" Firmware : " + Radio['firmware']) - print(" ESSID : " + Radio['essid']) - print(" MAC Address : " + Radio['mac']) - else: + if not found_radios: sys.stderr.write("\n\nNo radios discovered\n") + sys.exit() + print("\nDiscovered %d radio(s):" % found_radios) + fmt = " %-14s: %s" + for Radio in RadioList: + print("\n---[ %s ]---" % Radio['mac']) + print(fmt % ("IP Address", Radio['ip'])) + for field in Radio: + if field in BASIC_FIELDS: continue + print(fmt % (FIELD_STR.get(field, field), + Radio[field])) elif args.output_format == 'json': print(json.dumps(RadioList, indent=2))