blob: 3b84839dfdcc64fb53f572c4cd2fb755f5c5ee66 [file] [log] [blame]
# 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
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