wip
[hippotat] / server
CommitLineData
094ee3a2 1#!/usr/bin/python3
3fba9787 2
e2d41dc1
IJ
3import sys
4import os
5
5bae5ba3 6import twisted
e2d41dc1
IJ
7import twisted.internet
8import twisted.internet.endpoints
9from twisted.internet import reactor
10from twisted.web.server import NOT_DONE_YET
11from twisted.logger import LogLevel
12
13import ipaddress
14from ipaddress import AddressValueError
5bae5ba3 15
5da7763e
IJ
16#import twisted.web.server import Site
17#from twisted.web.resource import Resource
3fba9787 18
e75e9c17
IJ
19from optparse import OptionParser
20from configparser import ConfigParser
21from configparser import NoOptionError
3fba9787 22
0ac316c8
IJ
23import collections
24
c4b6d990
IJ
25import syslog
26
3fba9787
IJ
27clients = { }
28
e2d41dc1 29def ipaddr(input):
3fba9787 30 try:
ec88b1f1 31 r = ipaddress.IPv4Address(input)
3fba9787 32 except AddressValueError:
ec88b1f1 33 r = ipaddress.IPv6Address(input)
3fba9787
IJ
34 return r
35
36def ipnetwork(input):
37 try:
ec88b1f1 38 r = ipaddress.IPv4Network(input)
3fba9787 39 except NetworkValueError:
ec88b1f1 40 r = ipaddress.IPv6Network(input)
3fba9787
IJ
41 return r
42
e75e9c17 43defcfg = '''
094ee3a2
IJ
44[DEFAULT]
45max_batch_down = 65536
46max_queue_time = 10
47max_request_time = 54
48
e75e9c17
IJ
49[virtual]
50mtu = 1500
51# network
52# [host]
53# [relay]
54
55[server]
e2d41dc1 56ipif = userv root ipif %(host)s,%(relay)s,%(mtu)s,slip %(network)s
5da7763e
IJ
57addrs = 127.0.0.1 ::1
58port = 80
e75e9c17 59
094ee3a2
IJ
60[limits]
61max_batch_down = 262144
62max_queue_time = 121
63max_request_time = 121
ec88b1f1
IJ
64'''
65
5da7763e
IJ
66#---------- "router" ----------
67
e2d41dc1 68def route(packet, daddr):
5da7763e
IJ
69 try: client = clients[daddr]
70 except KeyError: dclient = None
71 if dclient is not None:
72 dclient.queue_outbound(packet)
e2d41dc1 73 elif daddr == host or daddr not in network:
5da7763e 74 queue_inbound(packet)
e2d41dc1 75 elif daddr == relay:
5da7763e
IJ
76 log_discard(packet, saddr, daddr, 'relay')
77 else:
78 log_discard(packet, saddr, daddr, 'no client')
79
80def log_discard(packet, saddr, daddr, why):
81 syslog.syslog(syslog.LOG_DEBUG,
82 'discarded packet %s -> %s (%s)' % (saddr, daddr, why))
83
84#---------- ipif (slip subprocess) ----------
85
5bae5ba3
IJ
86class IpifProcessProtocol(twisted.internet.protocol.ProcessProtocol):
87 def __init__(self):
88 self._buffer = b''
89 def connectionMade(self): pass
90 def outReceived(self, data):
91 buffer += data
92 packets = slip_decode(buffer)
93 buffer = packets.pop()
94 for packet in packets:
95 (saddr, daddr) = packet_addrs(packet)
96 route(packet, daddr)
5da7763e
IJ
97 def processEnded(self, status):
98 status.raiseException()
5bae5ba3
IJ
99
100def start_ipif():
5da7763e
IJ
101 global ipif
102 ipif = IpifProcessProtocol()
103 reactor.spawnProcess(ipif,
e2d41dc1 104 '/bin/sh',['sh','-c', ipif_command],
5bae5ba3
IJ
105 childFDs={0:'w', 1:'r', 2:2})
106
5da7763e
IJ
107def queue_inbound(packet):
108 ipif.transport.write(slip_delimiter)
109 ipif.transport.write(slip_encode(packet))
110 ipif.transport.write(slip_delimiter)
5bae5ba3 111
5da7763e 112#---------- client ----------
c4b6d990 113
ec88b1f1 114class Client():
c4b6d990 115 def __init__(self, ip, cs):
ec88b1f1
IJ
116 # instance data members
117 self._ip = ip
118 self._cs = cs
119 self.pw = cfg.get(cs, 'password')
0ac316c8
IJ
120 self._rq = collections.deque() # requests
121 self._pq = collections.deque() # packets
c4b6d990
IJ
122 # plus from config:
123 # .max_batch_down
124 # .max_queue_time
125 # .max_request_time
ec88b1f1
IJ
126 for k in ('max_batch_down','max_queue_time','max_request_time'):
127 req = cfg.getint(cs, k)
094ee3a2 128 limit = cfg.getint('limits',k)
c4b6d990
IJ
129 self.__dict__[k] = min(req, limit)
130
131 def process_arriving_data(self, d):
132 for packet in slip_decode(d):
5bae5ba3 133 (saddr, daddr) = packet_addrs(packet)
c4b6d990
IJ
134 if saddr != self._ip:
135 raise ValueError('wrong source address %s' % saddr)
136 route(packet, daddr)
ec88b1f1 137
c4b6d990
IJ
138 def _req_cancel(self, request):
139 request.finish()
140
141 def _req_error(self, err, request):
142 self._req_cancel(request)
143
0ac316c8 144 def queue_outbound(self, packet):
094ee3a2 145 self._pq.append((time.monotonic(), packet))
0ac316c8 146
c4b6d990
IJ
147 def http_request(self, request):
148 request.setHeader('Content-Type','application/octet-stream')
149 reactor.callLater(self.max_request_time, self._req_cancel, request)
150 request.notifyFinish().addErrback(self._req_error, request)
0ac316c8
IJ
151 self._rq.append(request)
152 self._check_outbound()
153
154 def _check_outbound(self):
155 while True:
156 try: request = self._rq[0]
157 except IndexError: request = None
158 if request and request.finished:
159 self._rq.popleft()
160 continue
161
162 # now request is an unfinished request, or None
163 try: (queuetime, packet) = self._pq[0]
e2d41dc1 164 except IndexError:
0ac316c8 165 # no packets, oh well
094ee3a2
IJ
166 break
167
168 age = time.monotonic() - queuetime
169 if age > self.max_queue_time:
170 self._pq.popleft()
0ac316c8
IJ
171 continue
172
094ee3a2
IJ
173 if request is None:
174 # no request
175 break
176
177 # request, and also some non-expired packets
178 while True:
179 try: (dummy, packet) = self._pq[0]
180 except IndexError: break
181
182 encoded = slip_encode(packet)
183
184 if request.sentLength > 0:
185 if (request.sentLength + len(slip_delimiter)
186 + len(encoded) > self.max_batch_down):
187 break
188 request.write(slip_delimiter)
189
190 request.write(encoded)
191 self._pq.popLeft()
192
193 assert(request.sentLength)
194 self._rq.popLeft()
195 request.finish()
196 # round again, looking for more to do
ec88b1f1 197
5da7763e
IJ
198class IphttpResource(twisted.web.resource.Resource):
199 def render_POST(self, request):
200 # find client, update config, etc.
e2d41dc1 201 ci = ipaddr(request.args['i'])
5da7763e
IJ
202 c = clients[ci]
203 pw = request.args['pw']
204 if pw != c.pw: raise ValueError('bad password')
205
206 # update config
207 for r, w in (('mbd', 'max_batch_down'),
208 ('mqt', 'max_queue_time'),
209 ('mrt', 'max_request_time')):
210 try: v = request.args[r]
211 except KeyError: continue
212 v = int(v)
213 c.__dict__[w] = v
214
215 try: d = request.args['d']
216 except KeyError: d = ''
217
218 c.process_arriving_data(d)
219 c.new_request(request)
220
221def start_http():
222 resource = IphttpResource()
223 sitefactory = twisted.web.server.Site(resource)
e2d41dc1 224 for addrspec in cfg.get('server','addrs').split():
5da7763e
IJ
225 try:
226 addr = ipaddress.IPv4Address(addrspec)
227 endpointfactory = twisted.internet.endpoints.TCP4ServerEndpoint
228 except AddressValueError:
229 addr = ipaddress.IPv6Address(addrspec)
230 endpointfactory = twisted.internet.endpoints.TCP6ServerEndpoint
231 ep = endpointfactory(reactor, cfg.getint('server','port'), addr)
232 ep.listen(sitefactory)
233
234#---------- config and setup ----------
235
3fba9787
IJ
236def process_cfg():
237 global network
e75e9c17
IJ
238 global host
239 global relay
5bae5ba3 240 global ipif_command
3fba9787 241
ec88b1f1 242 network = ipnetwork(cfg.get('virtual','network'))
e75e9c17
IJ
243 if network.num_addresses < 3 + 2:
244 raise ValueError('network needs at least 2^3 addresses')
245
3fba9787 246 try:
e75e9c17
IJ
247 host = cfg.get('virtual','host')
248 except NoOptionError:
e2d41dc1 249 host = next(network.hosts())
e75e9c17
IJ
250
251 try:
252 relay = cfg.get('virtual','relay')
e2d41dc1 253 except NoOptionError:
e75e9c17 254 for search in network.hosts():
e2d41dc1 255 if search == host: continue
e75e9c17
IJ
256 relay = search
257 break
3fba9787 258
ec88b1f1
IJ
259 for cs in cfg.sections():
260 if not (':' in cs or '.' in cs): continue
e2d41dc1 261 ci = ipaddr(cs)
ec88b1f1
IJ
262 if ci not in network:
263 raise ValueError('client %s not in network' % ci)
264 if ci in clients:
265 raise ValueError('multiple client cfg sections for %s' % ci)
266 clients[ci] = Client(ci, cs)
3fba9787 267
e2d41dc1
IJ
268 global mtu
269 mtu = cfg.get('virtual','mtu')
270
5bae5ba3
IJ
271 iic_vars = { }
272 for k in ('host','relay','mtu','network'):
273 iic_vars[k] = globals()[k]
274
275 ipif_command = cfg.get('server','ipif', vars=iic_vars)
276
e2d41dc1
IJ
277def crash_on_critical(event):
278 if event.get('log_level') >= LogLevel.critical:
279 print('crashing: ', twisted.logger.formatEvent(event), file=sys.stderr)
280 #print('crashing!', file=sys.stderr)
281 #os._exit(1)
282 try: reactor.stop()
283 except twisted.internet.error.ReactorNotRunning: pass
284
e75e9c17 285def startup():
e2d41dc1
IJ
286 global cfg
287
e75e9c17
IJ
288 op = OptionParser()
289 op.add_option('-c', '--config', dest='configfile',
290 default='/etc/hippottd/server.conf')
291 global opts
292 (opts, args) = op.parse_args()
293 if len(args): op.error('no non-option arguments please')
294
e2d41dc1
IJ
295 twisted.logger.globalLogPublisher.addObserver(crash_on_critical)
296
e75e9c17 297 cfg = ConfigParser()
5bae5ba3 298 cfg.read_string(defcfg)
e2d41dc1 299 cfg.read(opts.configfile)
5bae5ba3
IJ
300 process_cfg()
301
302 start_ipif()
303 start_http()
e2d41dc1
IJ
304
305startup()
306reactor.run()