-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathwgdata.py
249 lines (212 loc) · 8.08 KB
/
wgdata.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# Copyright 2014 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# pylint:disable=invalid-name
"""Data format for waveguide packets."""
from collections import namedtuple
import struct
import zlib
PROTO_MAGIC = 'wave'
PROTO_VERSION = 2
class DecodeError(Exception):
pass
# pylint: disable=invalid-name
class ApFlags(object):
Can2G = 0x01 # device supports 2.4 GHz band
Can5G = 0x02 # device supports 5 GHz band
Can_Mask = 0x0f # mask of all bits referring to band capability
HighPower = 0x10 # high-power device takes precedence over low-power
TvBox = 0x20 # tv boxes shouldn't connect to tv boxes (yet)
PRE_FMT = '!4sB'
Header = namedtuple(
'Header',
'bss_len,chan_len,assoc_len,arp_len')
HEADER_FMT = '!IIII'
# struct representing an AP's identity.
#
# now: system local time.time().
# uptime_ms: uptime of the waveguide process, in milliseconds.
# consensus_key: an agreed-upon key between all waveguide instances, to be
# used for anonymizing MAC and IP addresses in log messages.
# mac: the local MAC address of this wifi AP.
# flags: see ApFlags.
Me = namedtuple('Me', 'now,uptime_ms,consensus_key,mac,flags')
Me_FMT = '!QQ16s6sI'
# struct representing observed information about other APs in the vicinity.
#
# is_ours: true if the given BSS is part of our waveguide group.
# mac: the MAC address of the given AP.
# freq: the channel frequency (MHz) of the given AP.
# rssi: the power level received from this AP.
# flags: see ApFlags.
# last_seen: the time of the last time this AP was seen in a scan.
# cap: capabilities bitmask.
# phy: the dot11PHYType.
# reg: regulatory domain, like 'US'.
BSS = namedtuple('BSS', 'is_ours,mac,freq,rssi,flags,last_seen,cap,phy,reg')
BSS_FMT = '!B6sHbIIHB2s'
# struct representing observed information about traffic on a channel.
#
# freq: the channel frequency (MHz) that we observed.
# noise_dbm: the noise level observed on the channel.
# observed_ms: length of time (ms) that we have observed the channel.
# busy_ms: length of time (ms) that the channel was seen to be busy, where
# the traffic was *not* related to our own BSSID.
Channel = namedtuple('Channel', 'freq,noise_dbm,observed_ms,busy_ms')
Channel_FMT = '!HbII'
# struct representing stations associated with an AP.
#
# mac: the MAC address of the station.
# rssi: a running average of the signal strength received from the station.
# last_seen: the time of the last packet received from the station.
# can5G: whether the station supports 5GHz.
Assoc = namedtuple('Assoc', 'mac,rssi,last_seen,can5G')
Assoc_FMT = '!6sbI?'
# struct representing kernel ARP table entries.
#
# ip: the IP address of the node.
# mac: the MAC address corresponding to that IP address.
# last_seen: the time a packet was last received from this node.
ARP = namedtuple('ARP', 'ip,mac,last_seen')
ARP_FMT = '!4s6sI'
# struct representing the complete visible state of a waveguide node.
# (combination of the above structs)
#
# me: a Me() object corresponding to the AP's identity.
# seen_bss: a list of BSS().
# channel_survey: a list of Channel()
# assoc: a list of Assoc()
# arp: a list of ARP().
State = namedtuple('State', 'me,seen_bss,channel_survey,assoc,arp')
def EncodePacket(state):
"""Generate a binary waveguide packet for sending via multicast."""
me = state.me
now = me.now
me_out = struct.pack(Me_FMT, me.now, me.uptime_ms,
me.consensus_key, me.mac, me.flags)
bss_out = ''
for bss in state.seen_bss:
bss_out += struct.pack(BSS_FMT,
bss.is_ours,
bss.mac,
bss.freq,
bss.rssi,
bss.flags,
now - bss.last_seen,
bss.cap,
bss.phy,
bss.reg)
chan_out = ''
for chan in state.channel_survey:
chan_out += struct.pack(Channel_FMT,
chan.freq,
chan.noise_dbm,
chan.observed_ms,
chan.busy_ms)
assoc_out = ''
for assoc in state.assoc:
assoc_out += struct.pack(Assoc_FMT,
assoc.mac,
assoc.rssi,
now - assoc.last_seen,
assoc.can5G)
arp_out = ''
for arp in state.arp:
arp_out += struct.pack(ARP_FMT,
arp.ip,
arp.mac,
now - arp.last_seen)
header_out = struct.pack(HEADER_FMT,
len(bss_out),
len(chan_out),
len(assoc_out),
len(arp_out))
data = header_out + me_out + bss_out + chan_out + assoc_out + arp_out
pre = struct.pack(PRE_FMT, PROTO_MAGIC, PROTO_VERSION)
return pre + zlib.compress(data)
class Eater(object):
"""A simple wrapper for consuming bytes from the front of a string."""
def __init__(self, data):
"""Create an Eater instance.
Args:
data: the byte array that will be consumed by this instance.
"""
assert isinstance(data, bytes)
self.data = data
self.ofs = 0
def Eat(self, n):
"""Consumes the next n bytes of the string and returns them.
Args:
n: the number of bytes to consume.
Returns:
n bytes
Raises:
DecodeError: if there are not enough bytes left.
"""
if len(self.data) < self.ofs + n:
raise DecodeError('short packet: ofs=%d len=%d wanted=%d'
% (self.ofs, len(self.data), n))
b = self.data[self.ofs:self.ofs+n]
self.ofs += n
return b
def Remainder(self):
"""Consumes and returns all the remaining bytes."""
return self.Eat(len(self.data) - self.ofs)
def Unpack(self, fmt):
"""Consumes exactly enough bytes to run struct.unpack(fmt) on them.
Args:
fmt: a format string compatible with struct.unpack.
Returns:
The result of struct.unpack on the bytes using that format string.
Raises:
DecodeError: if there are not enough bytes left.
...or anything else struct.unpack might raise.
"""
n = struct.calcsize(fmt)
result = struct.unpack(fmt, self.Eat(n))
return result
def Iter(self, fmt, nbytes):
"""Consume and unpack a series of structs of struct.unpack(fmt).
Args:
fmt: a format string compatible with struct.unpack.
nbytes: the total number of bytes in the array. Must be a multiple
of struct.calcsize(fmt).
Yields:
A series of struct.unpack(fmt) tuples.
"""
e = Eater(self.Eat(nbytes))
while e.ofs < len(e.data):
yield e.Unpack(fmt)
def DecodePacket(p):
"""Decode a received binary waveguide packet into a State() structure."""
e = Eater(p)
magic, ver = e.Unpack(PRE_FMT)
if magic != PROTO_MAGIC:
raise DecodeError('expected magic=%r, got %r' % (PROTO_MAGIC, magic))
if ver != PROTO_VERSION:
raise DecodeError('expected proto_ver=%r, got %r' % (PROTO_VERSION, ver))
compressed = e.Remainder()
e = Eater(zlib.decompress(compressed))
(bss_len, chan_len, assoc_len, arp_len) = e.Unpack(HEADER_FMT)
me = Me(*e.Unpack(Me_FMT))
bss_list = [BSS(*i) for i in e.Iter(BSS_FMT, bss_len)]
chan_list = [Channel(*i) for i in e.Iter(Channel_FMT, chan_len)]
assoc_list = [Assoc(*i) for i in e.Iter(Assoc_FMT, assoc_len)]
arp_list = [ARP(*i) for i in e.Iter(ARP_FMT, arp_len)]
state = State(me=me,
seen_bss=bss_list,
channel_survey=chan_list,
assoc=assoc_list,
arp=arp_list)
return state