blob: 4ca71fb850df1c7bb64cbe7b941d4583d656495c [file] [log] [blame]
vuko3cd087d2021-12-28 13:19:40 +01001from at.dhcp import DhcpdUpdater, DhcpLease
2from pathlib import Path
3import yaml
4import grpc
5import json
6import re
7import subprocess
8import logging
9from concurrent import futures
vuko43069942021-12-28 21:39:28 +010010from datetime import datetime, timezone
vuko3cd087d2021-12-28 13:19:40 +010011
12from .tracker_pb2 import DhcpClient, DhcpClients, HwAddrResponse
13from .tracker_pb2_grpc import DhcpTrackerServicer, add_DhcpTrackerServicer_to_server
14
15import argparse
16parser = argparse.ArgumentParser()
17parser.add_argument("--verbose", help="output more info", action="store_true")
18parser.add_argument("config", type=Path, help="input file")
19
20logging.basicConfig(level=logging.INFO)
21
22def lease_to_client(lease: DhcpLease) -> DhcpClient:
23 return DhcpClient(
24 hw_address = bytes.fromhex(lease.hwaddr.replace(':', '')),
vuko43069942021-12-28 21:39:28 +010025 last_seen = datetime.utcfromtimestamp(lease.atime).replace(
26 tzinfo=timezone.utc).isoformat(),
vuko3cd087d2021-12-28 13:19:40 +010027 client_hostname = lease.name,
28 ip_address = lease.ip
29 )
30
31class DhcpTrackerServicer(DhcpTrackerServicer):
32 def __init__(self, tracker: DhcpdUpdater, *args, **kwargs):
33 self._tracker = tracker
34 super().__init__(*args, **kwargs)
35
36 def _authorize(self, context):
37 auth = context.auth_context()
38 ctype = auth.get('transport_security_type', 'local')
39 print(ctype)
40 if ctype == [b'ssl']:
41 if b'at.hackerspace.pl' not in context.peer_identities():
42 context.abort(
43 grpc.StatusCode.PERMISSION_DENIED,
44 (
45 "Only at.hackespace.pl is allowed to access raw "
46 "clients addresses"
47 )
48 )
49 elif ctype == 'local':
50 # connection from local unix socket is trusted by default
51 pass
52 else:
53 context.abort(
54 grpc.StatusCode.PERMISSION_DENIED,
55 f"Unknown transport type: {ctype}"
56 )
57
58 def GetClients(self, request, context):
59 self._authorize(context)
60
61 clients = [
62 lease_to_client(c) for c in self._tracker.get_active_devices().values()]
63 return DhcpClients(clients = clients)
64
65 def GetHwAddr(self, request, context):
66 self._authorize(context)
67 ip_address = str(request.ip_address)
68 if not re.fullmatch('[0-9a-fA-F:.]*', ip_address):
69 raise ValueError(f'Invalid ip address: {ip_address!r}')
70 logging.info(f'running ip neigh on {ip_address}')
71 r = subprocess.run(['ip', '-json', 'neigh', 'show', ip_address], check=True, capture_output=True)
72 neighs = json.loads(r.stdout)
73 if neighs:
74 return HwAddrResponse(hw_address=bytes.fromhex(neighs[0]['lladdr'].replace(':', '')))
75 return HwAddrResponse(hw_address=None)
76
77def server():
78 args = parser.parse_args()
79
80 config = yaml.safe_load(args.config.read_text())
81 tracker = DhcpdUpdater(config['LEASE_FILE'], config['TIMEOUT'])
82 tracker.start()
83
84 server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
85 add_DhcpTrackerServicer_to_server(DhcpTrackerServicer(tracker), server)
86
87
88 tls_address = config.get("GRPC_TLS_ADDRESS", None)
89 if tls_address:
90 cert_dir = Path(config.get('GRPC_TLS_CERT_DIR', 'cert'))
91 ca_cert = Path(config.get('GRPC_TLS_CA_CERT', 'ca.pem')).read_bytes()
92
93 server_credentials = grpc.ssl_server_credentials(
94 private_key_certificate_chain_pairs = ((
95 cert_dir.joinpath('key.pem').read_bytes(),
96 cert_dir.joinpath('cert.pem').read_bytes()
97 ),),
98 root_certificates = ca_cert,
99 require_client_auth = True
100 )
101
102 server.add_secure_port(config.get('GRPC_TLS_ADDRESS', '[::]:2847'), server_credentials)
103
104 unix_socket = config.get('GRPC_UNIX_SOCKET', False)
105 if unix_socket:
106 server.add_insecure_port(f'unix://{unix_socket}')
107
108 if tls_address or unix_socket:
109 print('starting grpc server ...')
110 server.start()
111 server.wait_for_termination()