Skip to content

Commit f5a1e41

Browse files
author
Brendan Jackman
committed
Initial commit
0 parents  commit f5a1e41

15 files changed

+1937
-0
lines changed

.gitignore

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.eggs/
2+
bwt_uapi.h
3+
*.egg-info
4+
.tox/
5+
build/
6+
**/__pycache__/
7+
*.pyc

LICENSE.txt

+339
Large diffs are not rendered by default.

README.md

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Python library for Generic Netlink
2+
3+
At [Blu Wireless](https://bluwireless.com/) we needed a tool for doing NL80211
4+
interactions in Python test code. We couldn't find any libraries that facilitate
5+
this in a way that leverages the strengths of Python, so we wrote
6+
something. Most of the original code was specific to Blu Wireless' technology,
7+
but the generic stuff was pulled out to create this library.
8+
9+
Netlink and Generic Netlink themselves aren't very complicated, but the
10+
attribute system can result in reams of boilerplate code. The aim of this
11+
library is to reduce that boilerplate. The key observation is that for
12+
some/most/all genl families, given knowledge about which attributes appear in
13+
which context and what type their payload should have, attribute sets can be
14+
mapped to Python dictionaries. So to use this library you provide a _schema_
15+
expressing that knowledge about attribute semantics in the protocol you're
16+
using, and it gives you an ergonomic way to build and parse messages.
17+
18+
The best way to see what this really means is to take a look at nl80211.py,
19+
where we define an example schema for NL80211 commands, and
20+
examples/nl80211_dump.py, which uses that schema to query NL80211 for
21+
information about a given WiFi adapter.
22+
23+
Works on both Python 2 and 3.
24+
25+
## Known limitations
26+
27+
- It would make sense for this library to help you build and parse multi-part
28+
Netlink messages, but it doesn't.
29+
30+
- The library niftily maps between full-length attribute names and short Python
31+
names (e.g. `msg["NL80211_ATTR_IFINDEX"]` can be rewritten as
32+
`msg.ifindex`). But this doesn't work fully for nested attributes,
33+
e.g. `msg["NL80211_ATTR_KEY"]["NL80211_KEY_DEFAULT"]` cannot be rewritten as
34+
`msg.key.default`.
35+
36+
- No HTML documentation. There are docstrings in the code, though. The most
37+
interesting bit is `nlattr.NlAttrSchema`.

examples/Pipfile

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[[source]]
2+
url = "https://pypi.org/simple"
3+
verify_ssl = true
4+
name = "pypi"
5+
6+
[dev-packages]
7+
8+
[packages]
9+
"a571e24" = {path = "./py-genl"}
10+
11+
[requires]
12+
python_version = "3.6"

examples/Pipfile.lock

+24
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/nl80211_dump.py

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/usr/bin/env python3
2+
3+
# SPDX-License-Identifier: GPL-2.0
4+
# Copyright (c) 2019 Blu Wireless Technology
5+
6+
"""
7+
Example program to dump a wireless interface info from nl80211
8+
9+
Pass interface name as sole argument
10+
"""
11+
12+
from sys import argv
13+
import socket
14+
15+
from genl.nl80211 import nl80211_schema, NL80211_CMD_GET_INTERFACE
16+
from genl import lookup_genl_family, if_nametoindex
17+
from genl.netlink import (get_genl_message, parse_genl_message,
18+
NETLINK_GENERIC, NLM_F_REQUEST)
19+
20+
21+
def main():
22+
# We need to look up the family ID which will go in the nl header's type
23+
# field. This also gives us the IDs we'd need to subscribe the socket to any
24+
# broadcast groups exposed by the family.
25+
family = lookup_genl_family("nl80211")
26+
27+
sock = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, NETLINK_GENERIC)
28+
sock.bind((0, 0))
29+
30+
# We use get_genl_message to build a Generic Netlink message. The nl80211
31+
# command goes in the cmd field of the genl header.
32+
# The payload is built using the canned nl0211 schema, using kwargs to
33+
# specify the attribute values used to express command params.
34+
# We could instead pass a dict, in which case we'd use the full attribute
35+
# names instead of shortened Pythonic names, e.g. instead of ifindex=foo
36+
# we could pass {"NL80211_ATTR_IFINDEX": foo}.
37+
msg = get_genl_message(
38+
mtype=family.id,
39+
flags=NLM_F_REQUEST,
40+
cmd=NL80211_CMD_GET_INTERFACE,
41+
payload=nl80211_schema.build(ifindex=if_nametoindex(argv[1])))
42+
sock.send(msg)
43+
44+
# Note the fixed size receive. This library lacks support for multi-part
45+
# messages.
46+
msg = sock.recv(8192)
47+
# Parse out the nl and genl header
48+
nl_header, genl_header, payload = parse_genl_message(msg)
49+
# Now we use the same canned schema to parse then attributes in the reply
50+
# payload
51+
info = nl80211_schema.parse(payload)
52+
53+
# The attributes can be accessed like a dict; in this case the keys are the
54+
# full attribute names. For exmple if your WiFi is connected to a network ,
55+
# these prints will include a line something like:
56+
# NL80211_ATTR_SSID = Darude-LANStorm
57+
print("Raw data:")
58+
for key in info:
59+
print(" {} = {}".format(key, info[key]))
60+
61+
# Then for convenience we can also access the attributes as Python
62+
# attributes using nicer names. The nl80211 schema (see nl80211.py) doesn't
63+
# specify the names for the Python attributes, so we get the default
64+
# behaviour, which is that the Python names are determined by finding the
65+
# full name's unique suffix amongst its siblings and lower-casing it.
66+
# So instead of info["NL80211_ATTR_SSID"] we can just access info.ssid.
67+
print("\nname: '{}' | MAC: {} | Current SSID: '{}'".format(
68+
info.ifname,
69+
":".join("{:02x}".format(b) for b in info.mac),
70+
info.ssid))
71+
72+
if __name__ == "__main__":
73+
main()

examples/py-genl

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
..

genl/__init__.py

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# SPDX-License-Identifier: GPL-2.0
2+
# Copyright (c) 2019 Blu Wireless Technology
3+
4+
import ctypes
5+
import ctypes.util
6+
from collections import namedtuple
7+
import socket
8+
import os
9+
import errno
10+
11+
from .nlattr import NlAttrSchema
12+
from .netlink import (NLM_F_REQUEST,
13+
GENL_ID_CTRL, GNL_FAMILY_VERSION,
14+
NETLINK_GENERIC,
15+
NetlinkError,
16+
get_genl_message,
17+
parse_genl_message)
18+
19+
20+
# From linux/genetlink.h
21+
CTRL_CMD_GETFAMILY = None # To relax the linter
22+
genl_ctrl_constants = {
23+
"CTRL_CMD_GETFAMILY": 3,
24+
"CTRL_ATTR_FAMILY_ID": 1,
25+
"CTRL_ATTR_FAMILY_NAME": 2,
26+
"CTRL_ATTR_MCAST_GROUPS": 7,
27+
"CTRL_ATTR_MCAST_GRP_NAME": 1,
28+
"CTRL_ATTR_VERSION": 3,
29+
"CTRL_ATTR_HDRSIZE": 4,
30+
"CTRL_ATTR_MAXATTR": 5,
31+
"CTRL_ATTR_OPS": 6,
32+
"CTRL_ATTR_MCAST_GRP_ID": 2,
33+
}
34+
globals().update(genl_ctrl_constants)
35+
36+
37+
# We'll need to use the GETFAMILY command, which is part of the core generic
38+
# netlink system, to query the system's IDs for nl80211. Define a schema for
39+
# that.
40+
getfamily_spec = [
41+
{
42+
"name": "CTRL_ATTR_FAMILY_ID",
43+
"type": "u16",
44+
},
45+
{
46+
"name": "CTRL_ATTR_FAMILY_NAME",
47+
"type": "str"
48+
},
49+
{
50+
"name": "CTRL_ATTR_VERSION",
51+
"type": "u32",
52+
},
53+
{
54+
"name": "CTRL_ATTR_HDRSIZE",
55+
"type": "u32",
56+
},
57+
{
58+
"name": "CTRL_ATTR_MAXATTR",
59+
"type": "u32",
60+
},
61+
{
62+
"name": "CTRL_ATTR_OPS",
63+
"type": "bytes", # Actually a nested thing, don't care about it.
64+
},
65+
{
66+
"name": "CTRL_ATTR_MCAST_GROUPS",
67+
"type": "list",
68+
"subelem_type": [
69+
{
70+
"name": "CTRL_ATTR_MCAST_GRP_NAME",
71+
"type": "str"
72+
},
73+
{
74+
"name": "CTRL_ATTR_MCAST_GRP_ID",
75+
"type": "u32"
76+
}
77+
],
78+
}
79+
]
80+
getfamily_schema = NlAttrSchema.from_spec(getfamily_spec, genl_ctrl_constants)
81+
82+
83+
GenlFamilyInfo = namedtuple("GenlFamilyInfo", ["id", "mcast_groups"])
84+
85+
86+
def lookup_genl_family(family_name):
87+
"""
88+
Look up a generic netlink family by name
89+
90+
Returns a GenlFamilyInfo with the numerical family ID and the IDs of the
91+
multicast groups it exposes
92+
"""
93+
cmd = get_genl_message(
94+
mtype=GENL_ID_CTRL,
95+
flags=NLM_F_REQUEST,
96+
cmd=CTRL_CMD_GETFAMILY,
97+
version=GNL_FAMILY_VERSION,
98+
payload=getfamily_schema.build(family_name=family_name))
99+
100+
sock = socket.socket(socket.AF_NETLINK, socket.SOCK_RAW, NETLINK_GENERIC)
101+
try:
102+
sock.bind((0, 0))
103+
sock.sendall(cmd)
104+
data = sock.recv(4096)
105+
nl_header, genl_header, genl_body = parse_genl_message(data)
106+
response = getfamily_schema.parse(genl_body)
107+
108+
mcast_groups = {}
109+
for entry in response["CTRL_ATTR_MCAST_GROUPS"]:
110+
group_name = entry["CTRL_ATTR_MCAST_GRP_NAME"].strip()
111+
mcast_groups[group_name] = entry["CTRL_ATTR_MCAST_GRP_ID"]
112+
113+
return GenlFamilyInfo(response["CTRL_ATTR_FAMILY_ID"], mcast_groups)
114+
finally:
115+
sock.close()
116+
117+
118+
# Make if_nametoindex(3) and if_indextoname(3) from libc available (for newer
119+
# Pythons this is in the standard library anyway).
120+
_libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
121+
_if_nametoindex = _libc.if_nametoindex
122+
_if_nametoindex.argtypes = [ctypes.c_char_p]
123+
_if_nametoindex.restype = ctypes.c_uint
124+
125+
_if_indextoname = _libc.if_indextoname
126+
_if_indextoname.argtypes = [ctypes.c_uint, ctypes.c_char_p]
127+
_if_indextoname.restype = ctypes.c_char_p
128+
129+
IF_NAMESIZE = 16
130+
131+
132+
def if_nametoindex(name):
133+
index = _if_nametoindex(name.encode("ascii"))
134+
135+
if index == 0:
136+
op_errno = ctypes.get_errno()
137+
if op_errno == errno.ENODEV:
138+
raise NetlinkError("no interface called '%s'" % name)
139+
else:
140+
raise OSError(op_errno, os.strerror(op_errno))
141+
142+
return index
143+
144+
145+
def if_indextoname(index):
146+
name = _if_indextoname(index, b" " * IF_NAMESIZE)
147+
148+
if not name:
149+
op_errno = ctypes.get_errno()
150+
if op_errno == errno.ENXIO:
151+
raise NetlinkError("no interface with index %d" % index)
152+
else:
153+
raise OSError(op_errno, os.strerror(op_errno))
154+
155+
return name.decode("ascii")

0 commit comments

Comments
 (0)