├── .gitignore └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.bak 2 | *.swp 3 | *.tmp 4 | *~ 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reverse Engineered UniFi Protocol 2 | 3 | This document describes the reverse-engineered protocol Ubiquity Unifi APs use to communicate with their controller. 4 | 5 | It's based on what I've observed; don't rely on it. 6 | 7 | 8 | ## Protocol 9 | 10 | 11 | ### Discover 12 | 13 | A factory-reset AP will send discovery packets: 14 | 15 | - as broadcast to 255.255.255.255 16 | - as multicast to 233.89.188.1 17 | 18 | The packets consist of a fixed header and some fields in Type-Length-Value (TLV) format. 19 | All numbers are in network byte order (big endian). 20 | 21 | Header: 22 | 23 | - 2 Byte: todo 24 | - 2 Byte: Payload length 25 | 26 | Fields: 27 | 28 | - 2 Byte: Type 29 | - 2 Byte: Length 30 | - n Byte: Value 31 | 32 | Available fields: 33 | 34 | - todo 35 | 36 | 37 | ### Adopt 38 | 39 | Via SSH. todo. 40 | 41 | 42 | ### Inform 43 | 44 | Every 10s, the AP sends an HTTP POST containing it's current status to the "inform URL". 45 | The controller will either respond with a no-op message or with a command message. 46 | Upon receiving a command message, an AP will execute a command and then send another inform immediately. 47 | 48 | For Layer 3 adoption purposes, that's `http://unifi:8080/inform` by default. 49 | It can be overridden by DHCP option 43 (Vendor Specific Information) code 1 (type IP-Address). 50 | 51 | Both the requests (AP to controller) and the responses (controller to AP) share the same format. 52 | All numbers are in network byte order (big endian). 53 | 54 | - 4 byte: magic bytes `TNBU` 55 | - 4 byte (int): packet version. Assuming 1. 56 | - 6 byte: APs MAC address (even in response sent by controller) 57 | - 2 byte (short): flags 58 | - bit 0x1: payload is encrypted 59 | - bit 0x2: payload is compressed 60 | - 16 byte: initialization vector 61 | - 4 byte (int): payload version 62 | - 4 byte (int): payload length in byte 63 | - remaining bytes: payload, maybe compressed, maybe encrypted 64 | 65 | Compression is done before encryption (as should be obvious). 66 | By default, the controller will not accept unencrypted packets. 67 | 68 | The compression algorithm is ZLIB. 69 | The encryption algorithm is AES in CBC mode with PKCS#7-style padding. 70 | 71 | Version 1 payloads are JSON documents. 72 | 73 | A request might look like this. 74 | 75 | A Unifi UAPs request might look a follows. 76 | Field values have been changed; but their capitalization and lengths haven't. 77 | The controller is `192.168.1.1`, the AP is `192.168.1.2`. 78 | 79 | {u'bootrom_version': u'unifi-v1.5.2.206-g44e4c8bc', 80 | u'cfgversion': u'0123456789abcdef', 81 | u'connect_request_ip': u'192.168.1.2', 82 | u'connect_request_port': u'56288', 83 | u'country_code': 0, 84 | u'default': False, 85 | u'guest_token': u'0123456789ABCDEF01234567890ABCDE', 86 | u'has_eth1': False, 87 | u'has_poe_passthrough': False, 88 | u'hostname': u'UBNT', 89 | u'if_table': [{u'full_duplex': True, 90 | u'ip': u'0.0.0.0', 91 | u'mac': u'01:23:45:67:89:ab', 92 | u'name': u'eth0', 93 | u'num_port': 1, 94 | u'rx_bytes': 1234567, 95 | u'rx_dropped': 0, 96 | u'rx_errors': 0, 97 | u'rx_multicast': 1234, 98 | u'rx_packets': 12345, 99 | u'speed': 100, 100 | u'tx_bytes': 1234567, 101 | u'tx_dropped': 0, 102 | u'tx_errors': 0, 103 | u'tx_packets': 12345, 104 | u'up': True}], 105 | u'inform_url': u'http://192.168.1.1:8080/inform', 106 | u'ip': u'192.168.1.2', 107 | u'isolated': False, 108 | u'locating': False, 109 | u'mac': u'01:23:45:67:89:ab', 110 | u'model': u'BZ2', 111 | u'model_display': u'UAP', 112 | u'radio_table': [{u'athstats': {u'ast_ath_reset': 0, 113 | u'ast_be_xmit': 101550, 114 | u'ast_cst': 53, 115 | u'ast_deadqueue_reset': 0, 116 | u'ast_fullqueue_stop': 0, 117 | u'ast_txto': 3, 118 | u'n_rx_aggr': 0, 119 | u'n_rx_pkts': 512003, 120 | u'n_tx_bawadv': 0, 121 | u'n_tx_bawretries': 0, 122 | u'n_tx_pkts': 13036, 123 | u'n_tx_queue': 550, 124 | u'n_tx_retries': 0, 125 | u'n_tx_xretries': 0, 126 | u'n_txaggr_compgood': 0, 127 | u'n_txaggr_compretries': 0, 128 | u'n_txaggr_compxretry': 0, 129 | u'n_txaggr_prepends': 0, 130 | u'name': u'wifi0'}, 131 | u'builtin_ant_gain': 0, 132 | u'builtin_antenna': True, 133 | u'max_txpower': 23, 134 | u'min_txpower': 5, 135 | u'name': u'wifi0', 136 | u'radio': u'ng', 137 | u'scan_table': []}], 138 | u'required_version': u'2.4.4', 139 | u'serial': u'0123456789AB', 140 | u'state': 2, 141 | u'time': 1424915907, 142 | u'uplink': u'eth0', 143 | u'uptime': 10442, 144 | u'vap_table': [{u'bssid': u'00:00:00:00:00:00', 145 | u'ccq': 4772488, 146 | u'channel': 1, 147 | u'essid': u'vport-0123456789AB', 148 | u'id': u'user', 149 | u'name': u'ath0', 150 | u'num_sta': 0, 151 | u'radio': u'ng', 152 | u'rx_bytes': 0, 153 | u'rx_crypts': 0, 154 | u'rx_dropped': 0, 155 | u'rx_errors': 0, 156 | u'rx_frags': 0, 157 | u'rx_nwids': 0, 158 | u'rx_packets': 0, 159 | u'sta_table': [], 160 | u'state': u'INIT', 161 | u'tx_bytes': 0, 162 | u'tx_dropped': 0, 163 | u'tx_errors': 0, 164 | u'tx_packets': 0, 165 | u'tx_power': 23, 166 | u'tx_retries': 0, 167 | u'up': False, 168 | u'usage': u'uplink'}, 169 | {u'bssid': u'ba:09:87:65:43:21', 170 | u'ccq': 4772480, 171 | u'channel': 1, 172 | u'essid': u'test', 173 | u'id': u'0123456789abcdef01234567', 174 | u'name': u'ath1', 175 | u'num_sta': 0, 176 | u'radio': u'ng', 177 | u'rx_bytes': 0, 178 | u'rx_crypts': 0, 179 | u'rx_dropped': 0, 180 | u'rx_errors': 0, 181 | u'rx_frags': 0, 182 | u'rx_nwids': 4097, 183 | u'rx_packets': 0, 184 | u'sta_table': [], 185 | u'state': u'RUN', 186 | u'tx_bytes': 301713, 187 | u'tx_dropped': 29169, 188 | u'tx_errors': 0, 189 | u'tx_packets': 1051, 190 | u'tx_power': 23, 191 | u'tx_retries': 0, 192 | u'up': True, 193 | u'usage': u'user'}], 194 | u'version': u'3.2.10.2886'} 195 | 196 | 197 | ## Dependencies 198 | 199 | My decoder has a few Python libraries as dependencies: 200 | 201 | - Padding 202 | - PyCrypto 203 | - pymongo 204 | 205 | 206 | ## How to capture 207 | 208 | ### With mitmproxy 209 | 210 | You can use mitmproxy decode and/or tamper with the packets. 211 | 212 | mitmdump --port 18080 --reverse http://localhost:8080/ -s mitmproxy-extension.py 213 | 214 | You can use iptables to redirect incoming packets to mitmproxy. 215 | 216 | -m addrtype -p tcp --dport 8080 --dst-type LOCAL ! --src-type LOCAL -j REDIRECT --to-ports 18080 217 | 218 | If you're using firewalld, you can configure this as a direct rule: 219 | 220 | firewall-cmd [--permanent] --direct --add-rule ipv4 nat PREROUTING ... 221 | 222 | You shouldn't, but you could also add it to `/etc/firewalld/direct.xml` (before starting firewalld): 223 | 224 | ... 225 | 226 | You can't see the adoption process yet, since the key for the AP will only 227 | be added to the database *after* we've forwarded the packet. 228 | It'd be possible to fix this by delaying decoding of the packet if no key is available. 229 | One would have to take care to prevent displaying packets in the wrong order. 230 | 231 | --------------------------------------------------------------------------------