ffwd-tunnel 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/bin/ffwd-tunnel-agent +516 -0
- data/lib/ffwd/plugin/tunnel/binary_protocol.rb +384 -0
- data/lib/ffwd/plugin/tunnel/connection_tcp.rb +62 -0
- data/lib/ffwd/plugin/tunnel/version.rb +22 -0
- data/lib/ffwd/plugin/tunnel.rb +57 -0
- metadata +92 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5df1c67ae1513904503ff2db9e7071ec1fff649a
|
4
|
+
data.tar.gz: 07ec0e4930a3f2b8af7c850dd799d73775f83c08
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e463d7728abf1f437ef965038f41267f4a93161617d9ed77ea4ebdee3b7018e374c12d29a9fb9d0c68624099b81c09487df74aafe2cbe199b0a82be15956e6ba
|
7
|
+
data.tar.gz: 21bfd3d35a7bbef9b6e979fe7b0ab9cbc7a2cc64b5990fe18f3d9a04b40f928a7d329ab454eaaed73fd77b7d62d4b764d6b34b55664c388e0e2c3b6aa7386bc1
|
@@ -0,0 +1,516 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
"""A reference tunneling proxy for FFWD."""
|
3
|
+
|
4
|
+
import time
|
5
|
+
import json
|
6
|
+
import asyncore
|
7
|
+
import sys
|
8
|
+
import socket
|
9
|
+
import struct
|
10
|
+
import logging
|
11
|
+
import errno
|
12
|
+
import argparse
|
13
|
+
|
14
|
+
log = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
TEXT = object()
|
17
|
+
BINARY = object()
|
18
|
+
UDP = 'udp'
|
19
|
+
TCP = 'tcp'
|
20
|
+
RECV_MAX = 8192
|
21
|
+
DEFAULT_PROTOCOL = "text"
|
22
|
+
PROTOCOL_MAX_DATALEN = 2 ** 16
|
23
|
+
DEFAULT_PORT = 9000
|
24
|
+
|
25
|
+
|
26
|
+
class _BindUDP(asyncore.dispatcher):
|
27
|
+
def __init__(self, family, tunnel, bindaddress, bindport):
|
28
|
+
asyncore.dispatcher.__init__(self)
|
29
|
+
self.create_socket(family, socket.SOCK_DGRAM)
|
30
|
+
self.set_reuse_addr()
|
31
|
+
self.bind((bindaddress, bindport))
|
32
|
+
|
33
|
+
self._family = family
|
34
|
+
self._tunnel = tunnel
|
35
|
+
self._port = bindport
|
36
|
+
|
37
|
+
def writable(self):
|
38
|
+
return False
|
39
|
+
|
40
|
+
def handle_read(self):
|
41
|
+
"""implement asyncore.dispatcher#handle_read"""
|
42
|
+
data, addr = self.recvfrom(RECV_MAX)
|
43
|
+
|
44
|
+
self._tunnel.client_data(
|
45
|
+
socket.SOCK_DGRAM, self._family, self._port, addr, data)
|
46
|
+
|
47
|
+
def receive_data(self, addr, data):
|
48
|
+
family, addr, port = addr
|
49
|
+
self.sendto(data, (addr, port))
|
50
|
+
|
51
|
+
|
52
|
+
class _BindTCP(asyncore.dispatcher):
|
53
|
+
class Connection(asyncore.dispatcher_with_send):
|
54
|
+
def __init__(self, bind, sock, addr):
|
55
|
+
asyncore.dispatcher_with_send.__init__(self, sock)
|
56
|
+
self.bind = bind
|
57
|
+
self.addr = addr
|
58
|
+
|
59
|
+
def handle_close(self):
|
60
|
+
"""implement asyncore.dispatcher_with_send#handle_close."""
|
61
|
+
self.bind.conn_handle_close(self.addr)
|
62
|
+
|
63
|
+
def handle_error(self):
|
64
|
+
"""implement asyncore.dispatcher_with_send#handle_error."""
|
65
|
+
|
66
|
+
def handle_read(self):
|
67
|
+
"""implement asyncore.dispatcher#handle_read."""
|
68
|
+
self.bind.conn_handle_data(self.addr, self.recv(RECV_MAX))
|
69
|
+
|
70
|
+
def __init__(self, family, tunnel, bindaddress, bindport):
|
71
|
+
asyncore.dispatcher.__init__(self)
|
72
|
+
self.create_socket(family, socket.SOCK_STREAM)
|
73
|
+
self.set_reuse_addr()
|
74
|
+
self.bind((bindaddress, bindport))
|
75
|
+
self.listen(5)
|
76
|
+
|
77
|
+
self._family = family
|
78
|
+
self._tunnel = tunnel
|
79
|
+
self._port = bindport
|
80
|
+
self._connections = dict()
|
81
|
+
|
82
|
+
def handle_close(self):
|
83
|
+
"""implement asyncore.dispatcher#handle_close."""
|
84
|
+
self.close()
|
85
|
+
|
86
|
+
def handle_accept(self):
|
87
|
+
"""implement asyncore.dispatcher#handle_accept."""
|
88
|
+
pair = self.accept()
|
89
|
+
|
90
|
+
if pair is not None:
|
91
|
+
sock, addr = pair
|
92
|
+
log.debug("open tcp/%d: %s:%d", self._port, addr[0], addr[1])
|
93
|
+
self._connections[addr] = self.Connection(self, sock, addr)
|
94
|
+
self.client_state(addr, Protocol.OPEN)
|
95
|
+
|
96
|
+
def conn_handle_data(self, addr, data):
|
97
|
+
"""Receive data from TCP connections."""
|
98
|
+
self.client_data(addr, data)
|
99
|
+
|
100
|
+
def conn_handle_close(self, addr):
|
101
|
+
"""Remove the client connection associated with addr."""
|
102
|
+
log.debug("closed tcp/%d: %s:%d", self._port, addr[0], addr[1])
|
103
|
+
|
104
|
+
client = self._connections[addr]
|
105
|
+
client.close()
|
106
|
+
|
107
|
+
del self._connections[addr]
|
108
|
+
self.client_state(addr, Protocol.CLOSE)
|
109
|
+
|
110
|
+
def client_data(self, addr, data):
|
111
|
+
self._tunnel.client_data(
|
112
|
+
socket.SOCK_STREAM, self._family, self._port, addr, data)
|
113
|
+
|
114
|
+
def client_state(self, addr, state):
|
115
|
+
self._tunnel.client_state(
|
116
|
+
socket.SOCK_STREAM, self._family, self._port, addr, state)
|
117
|
+
|
118
|
+
def close(self):
|
119
|
+
for client in self._connections.values():
|
120
|
+
client.close()
|
121
|
+
|
122
|
+
self._connections = {}
|
123
|
+
asyncore.dispatcher.close(self)
|
124
|
+
|
125
|
+
def receive_data(self, addr, data):
|
126
|
+
try:
|
127
|
+
client = self._connections[addr]
|
128
|
+
except KeyError:
|
129
|
+
log.error("no such client: %s", addr)
|
130
|
+
self.close()
|
131
|
+
return
|
132
|
+
|
133
|
+
client.send(data)
|
134
|
+
|
135
|
+
|
136
|
+
class _LineProtocol(object):
|
137
|
+
delimiter = '\n'
|
138
|
+
|
139
|
+
buffer_limit = 1048576
|
140
|
+
|
141
|
+
def __init__(self):
|
142
|
+
self._lp_buffer = ""
|
143
|
+
self._lp_size = 0
|
144
|
+
self._lp_limit = self.buffer_limit
|
145
|
+
|
146
|
+
def set_mode(self, size):
|
147
|
+
self._lp_size = size
|
148
|
+
|
149
|
+
def handle_read(self):
|
150
|
+
"""implement asyncore.dispatcher#handle_read."""
|
151
|
+
|
152
|
+
data = self.recv(RECV_MAX)
|
153
|
+
|
154
|
+
if len(self._lp_buffer) + len(data) > self._lp_limit:
|
155
|
+
log.error("buffer limit reached, closing connection")
|
156
|
+
self.close()
|
157
|
+
return
|
158
|
+
|
159
|
+
if self._lp_size == 0:
|
160
|
+
self._handle_line(data)
|
161
|
+
else:
|
162
|
+
self._handle_text(data)
|
163
|
+
|
164
|
+
def _handle_line(self, data):
|
165
|
+
while True:
|
166
|
+
try:
|
167
|
+
i = data.index(self.delimiter)
|
168
|
+
except ValueError:
|
169
|
+
break
|
170
|
+
|
171
|
+
try:
|
172
|
+
self.receive_line(self._lp_buffer + data[:i])
|
173
|
+
except:
|
174
|
+
log.error("receive_line failed", exc_info=sys.exc_info())
|
175
|
+
self.close()
|
176
|
+
return
|
177
|
+
|
178
|
+
self._lp_buffer = ""
|
179
|
+
data = data[i + 2:]
|
180
|
+
|
181
|
+
if len(data) > 0:
|
182
|
+
self._lp_buffer += data
|
183
|
+
|
184
|
+
def _handle_text(self, data):
|
185
|
+
self._lp_buffer += data
|
186
|
+
|
187
|
+
while len(self._lp_buffer) >= self._lp_size:
|
188
|
+
size = self._lp_size
|
189
|
+
self._lp_size = 0
|
190
|
+
|
191
|
+
try:
|
192
|
+
self.receive_text(self._lp_buffer[:size])
|
193
|
+
except:
|
194
|
+
log.error("failed to receive text", exc_info=sys.exc_info())
|
195
|
+
self.close()
|
196
|
+
return
|
197
|
+
|
198
|
+
self._lp_buffer = self._lp_buffer[size:]
|
199
|
+
|
200
|
+
def send_line(self, line):
|
201
|
+
"""Send a line of data using the specified delimiter."""
|
202
|
+
self.send(line + self.delimiter)
|
203
|
+
|
204
|
+
|
205
|
+
BIND_PROTOCOLS = {
|
206
|
+
socket.SOCK_STREAM: _BindTCP,
|
207
|
+
socket.SOCK_DGRAM: _BindUDP,
|
208
|
+
}
|
209
|
+
|
210
|
+
|
211
|
+
class Protocol(object):
|
212
|
+
ST_HEADER = struct.Struct("!HHHBB")
|
213
|
+
|
214
|
+
ST_STATE = struct.Struct("!H")
|
215
|
+
ST_PEER_ADDR_AF_INET = struct.Struct("!4sH")
|
216
|
+
ST_PEER_ADDR_AF_INET6 = struct.Struct("!16sH")
|
217
|
+
|
218
|
+
STATE = 0x0000
|
219
|
+
DATA = 0x0001
|
220
|
+
|
221
|
+
OPEN = 0x0000
|
222
|
+
CLOSE = 0x0001
|
223
|
+
|
224
|
+
PACKET_TYPES = {
|
225
|
+
STATE: ST_STATE,
|
226
|
+
DATA: None,
|
227
|
+
}
|
228
|
+
|
229
|
+
def __init__(self, conn):
|
230
|
+
self._header = None
|
231
|
+
self._c = conn
|
232
|
+
|
233
|
+
def parse_st_addr(self, family):
|
234
|
+
if family == socket.AF_INET:
|
235
|
+
return self.ST_PEER_ADDR_AF_INET
|
236
|
+
|
237
|
+
if family == socket.AF_INET6:
|
238
|
+
return self.ST_PEER_ADDR_AF_INET6
|
239
|
+
|
240
|
+
raise Exception("Unsupported family: %d" % (family))
|
241
|
+
|
242
|
+
def peer_addr_pack(self, family, addr):
|
243
|
+
st_addr = self.parse_st_addr(family)
|
244
|
+
ip, port = addr
|
245
|
+
ip = socket.inet_pton(family, ip)
|
246
|
+
return st_addr.pack(ip, port), st_addr.size
|
247
|
+
|
248
|
+
def peer_addr_unpack(self, family, data):
|
249
|
+
st_addr = self.parse_st_addr(family)
|
250
|
+
ip, port = st_addr.unpack(data[:st_addr.size])
|
251
|
+
ip = socket.inet_ntop(family, ip)
|
252
|
+
return (ip, port), st_addr.size
|
253
|
+
|
254
|
+
def client_data(self, protocol, family, port, addr, data):
|
255
|
+
"""Send client DATA."""
|
256
|
+
addr, addr_size = self.peer_addr_pack(family, addr)
|
257
|
+
|
258
|
+
length = self.ST_HEADER.size + addr_size + len(data)
|
259
|
+
|
260
|
+
if length > PROTOCOL_MAX_DATALEN:
|
261
|
+
raise Exception("Maximum possible frame length exceeded")
|
262
|
+
|
263
|
+
header = self.ST_HEADER.pack(length, self.DATA, port, family, protocol)
|
264
|
+
|
265
|
+
frame = header + addr + data
|
266
|
+
self._c.send(frame)
|
267
|
+
|
268
|
+
def client_state(self, protocol, family, port, addr, state):
|
269
|
+
"""Send a client STATE update."""
|
270
|
+
addr_data, addr_size = self.peer_addr_pack(family, addr)
|
271
|
+
|
272
|
+
length = self.ST_HEADER.size + addr_size + self.ST_STATE.size
|
273
|
+
|
274
|
+
if length > PROTOCOL_MAX_DATALEN:
|
275
|
+
raise Exception("Maximum possible frame length exceeded")
|
276
|
+
|
277
|
+
header_data = self.ST_HEADER.pack(
|
278
|
+
length, self.STATE, port, family, protocol)
|
279
|
+
state_data = self.ST_STATE.pack(state)
|
280
|
+
|
281
|
+
frame = header_data + addr_data + state_data
|
282
|
+
self._c.send(frame)
|
283
|
+
|
284
|
+
def setup(self):
|
285
|
+
self._c.set_mode(self.ST_HEADER.size)
|
286
|
+
|
287
|
+
def receive_line(self, line):
|
288
|
+
raise Exception("did not expect line")
|
289
|
+
|
290
|
+
def receive_text(self, data):
|
291
|
+
if self._header is None:
|
292
|
+
self._header = self.ST_HEADER.unpack(data)
|
293
|
+
self._c.set_mode(self._header[0] - self.ST_HEADER.size)
|
294
|
+
return
|
295
|
+
|
296
|
+
length, frame_type, port, family, protocol = self._header
|
297
|
+
addr, addr_size = self.peer_addr_unpack(family, data)
|
298
|
+
rest = data[addr_size:]
|
299
|
+
|
300
|
+
if frame_type == self.DATA:
|
301
|
+
tunnel_id = (family, protocol, port)
|
302
|
+
self._c.receive_data(tunnel_id, addr, rest)
|
303
|
+
else:
|
304
|
+
raise Exception("Unexpected frame type: %d" % (frame_type))
|
305
|
+
|
306
|
+
self._c.set_mode(self.ST_HEADER.size)
|
307
|
+
self._header = None
|
308
|
+
|
309
|
+
|
310
|
+
class TunnelClient(_LineProtocol, asyncore.dispatcher_with_send):
|
311
|
+
def __init__(self, metadata, addr):
|
312
|
+
asyncore.dispatcher_with_send.__init__(self)
|
313
|
+
_LineProtocol.__init__(self)
|
314
|
+
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
|
315
|
+
self.connect(addr)
|
316
|
+
|
317
|
+
self._metadata = metadata
|
318
|
+
self._tunnels = dict()
|
319
|
+
self._protocol = None
|
320
|
+
self._addr = addr
|
321
|
+
|
322
|
+
def handle_error(self):
|
323
|
+
"""implement asyncore.dispatcher#handle_error."""
|
324
|
+
exc_info = sys.exc_info()
|
325
|
+
e = exc_info[1]
|
326
|
+
|
327
|
+
if isinstance(e, socket.error):
|
328
|
+
if e.errno == errno.ECONNREFUSED:
|
329
|
+
log.warn("connection refused: %s", self._addr)
|
330
|
+
self.close()
|
331
|
+
return
|
332
|
+
|
333
|
+
log.error("error: %s", str(exc_info[1]), exc_info=exc_info)
|
334
|
+
self.close()
|
335
|
+
|
336
|
+
def handle_close(self):
|
337
|
+
"""implement asyncore.dispatcher#handle_close."""
|
338
|
+
log.info("closed")
|
339
|
+
self.close()
|
340
|
+
|
341
|
+
def close(self):
|
342
|
+
for tunnel in self._tunnels.values():
|
343
|
+
tunnel.close()
|
344
|
+
|
345
|
+
self._tunnels = dict()
|
346
|
+
self._protocol = None
|
347
|
+
|
348
|
+
asyncore.dispatcher_with_send.close(self)
|
349
|
+
|
350
|
+
def client_data(self, *args):
|
351
|
+
if self._protocol is None:
|
352
|
+
raise Exception("protocol is not configured")
|
353
|
+
|
354
|
+
self._protocol.client_data(*args)
|
355
|
+
|
356
|
+
def client_state(self, *args):
|
357
|
+
if self._protocol is None:
|
358
|
+
raise Exception("protocol is not configured")
|
359
|
+
|
360
|
+
self._protocol.client_state(*args)
|
361
|
+
|
362
|
+
def _receive_binary(self, protocol, bindport, addr, data):
|
363
|
+
family = addr[0]
|
364
|
+
ip = socket.inet_pton(addr[0], addr[1])
|
365
|
+
port = addr[2]
|
366
|
+
datasize = len(data)
|
367
|
+
|
368
|
+
if datasize > PROTOCOL_MAX_DATALEN:
|
369
|
+
raise Exception("Maximum data length exceeded")
|
370
|
+
|
371
|
+
header = self.ST_HEADER.pack(
|
372
|
+
protocol, bindport, family, ip, port, datasize)
|
373
|
+
|
374
|
+
frame = header + data
|
375
|
+
self.send(frame)
|
376
|
+
|
377
|
+
def handle_connect(self):
|
378
|
+
"""implement asyncore.dispatcher#handle_connect."""
|
379
|
+
log.info("connected")
|
380
|
+
self.send_line(json.dumps(self._metadata))
|
381
|
+
|
382
|
+
def receive_line(self, line):
|
383
|
+
"""implement _LineProtocol#receive_line."""
|
384
|
+
|
385
|
+
if self._protocol is not None:
|
386
|
+
raise Exception("protocol already configured")
|
387
|
+
|
388
|
+
try:
|
389
|
+
self._protocol = self.configure(line)
|
390
|
+
except:
|
391
|
+
log.error("failed to receive line", exc_info=sys.exc_info())
|
392
|
+
self.close()
|
393
|
+
return
|
394
|
+
|
395
|
+
try:
|
396
|
+
self._protocol.setup()
|
397
|
+
except:
|
398
|
+
log.error("failed to setup protocol", exc_info=sys.exc_info())
|
399
|
+
self.close()
|
400
|
+
return
|
401
|
+
|
402
|
+
def configure(self, line):
|
403
|
+
config = json.loads(line)
|
404
|
+
log.info("CONFIG: %s", repr(config))
|
405
|
+
|
406
|
+
if not self._bind_all(config):
|
407
|
+
self.close()
|
408
|
+
return
|
409
|
+
|
410
|
+
return Protocol(self)
|
411
|
+
|
412
|
+
def receive_text(self, data):
|
413
|
+
if self._protocol is None:
|
414
|
+
raise Exception("protocol is not configured")
|
415
|
+
|
416
|
+
try:
|
417
|
+
self._protocol.receive_text(data)
|
418
|
+
except:
|
419
|
+
log.error("failed to receive text", exc_info=sys.exc_info())
|
420
|
+
self.close()
|
421
|
+
|
422
|
+
def receive_data(self, tunnel_id, addr, data):
|
423
|
+
try:
|
424
|
+
tunnel = self._tunnels[tunnel_id]
|
425
|
+
except KeyError:
|
426
|
+
log.error("no such tunnel: %s", tunnel_id)
|
427
|
+
return
|
428
|
+
|
429
|
+
tunnel.receive_data(addr, data)
|
430
|
+
|
431
|
+
def _bind_all(self, config):
|
432
|
+
"""Bind all protocol/port combinations from configuration."""
|
433
|
+
bind = config.get('bind', [])
|
434
|
+
|
435
|
+
for b in bind:
|
436
|
+
tunnel_id = (b['family'], b['protocol'], b['port'])
|
437
|
+
|
438
|
+
if tunnel_id in self._tunnels:
|
439
|
+
log.error("Already bound: %s", repr(tunnel_id))
|
440
|
+
continue
|
441
|
+
|
442
|
+
try:
|
443
|
+
family, protocol, port = tunnel_id
|
444
|
+
protocol = BIND_PROTOCOLS[protocol]
|
445
|
+
self._tunnels[tunnel_id] = protocol(
|
446
|
+
family, self, '127.0.0.1', port)
|
447
|
+
except:
|
448
|
+
log.error("failed to bind: %s", repr(b),
|
449
|
+
exc_info=sys.exc_info())
|
450
|
+
continue
|
451
|
+
|
452
|
+
if len(self._tunnels) != len(bind):
|
453
|
+
log.error("unable to bind everything: %s", repr(bind))
|
454
|
+
return False
|
455
|
+
|
456
|
+
log.info("ports bound")
|
457
|
+
return True
|
458
|
+
|
459
|
+
|
460
|
+
def hostip(string):
|
461
|
+
if ':' not in string:
|
462
|
+
return (string, DEFAULT_PORT)
|
463
|
+
|
464
|
+
ip, port = string.split(':', 2)
|
465
|
+
return (ip, int(port))
|
466
|
+
|
467
|
+
parser = argparse.ArgumentParser(sys.argv[0])
|
468
|
+
|
469
|
+
parser.add_argument(
|
470
|
+
"-j", "--json-metadata", dest="json_metadata",
|
471
|
+
help="Load metadata from JSON file.", metavar="<file>")
|
472
|
+
|
473
|
+
parser.add_argument(
|
474
|
+
"-d", "--debug", dest="debug", action="store_const", const=True,
|
475
|
+
default=False, help="Enable debugging.")
|
476
|
+
|
477
|
+
parser.add_argument(
|
478
|
+
"-c", "--connect", dest="connect", default=('127.0.0.1', DEFAULT_PORT),
|
479
|
+
type=hostip, metavar="<host>[:port]", help="Connect to the specified ")
|
480
|
+
|
481
|
+
|
482
|
+
def parse_args(args):
|
483
|
+
ns = parser.parse_args(args)
|
484
|
+
|
485
|
+
log_level = logging.INFO
|
486
|
+
|
487
|
+
if ns.debug:
|
488
|
+
log_level = logging.DEBUG
|
489
|
+
|
490
|
+
logging.basicConfig(level=log_level)
|
491
|
+
|
492
|
+
if ns.json_metadata:
|
493
|
+
with open(ns.json_metadata) as f:
|
494
|
+
ns.metadata = json.load(f)
|
495
|
+
else:
|
496
|
+
ns.metadata = dict()
|
497
|
+
|
498
|
+
log.info("Metadata: %s", repr(ns.metadata))
|
499
|
+
|
500
|
+
return ns
|
501
|
+
|
502
|
+
|
503
|
+
def _main(args):
|
504
|
+
ns = parse_args(args)
|
505
|
+
|
506
|
+
reconnect_timeout = 1.0
|
507
|
+
|
508
|
+
while True:
|
509
|
+
TunnelClient(ns.metadata, ns.connect)
|
510
|
+
asyncore.loop()
|
511
|
+
|
512
|
+
log.info("reconnecting in %ds", reconnect_timeout)
|
513
|
+
time.sleep(reconnect_timeout)
|
514
|
+
|
515
|
+
if __name__ == "__main__":
|
516
|
+
sys.exit(_main(sys.argv[1:]))
|
@@ -0,0 +1,384 @@
|
|
1
|
+
# $LICENSE
|
2
|
+
# Copyright 2013-2014 Spotify AB. All rights reserved.
|
3
|
+
#
|
4
|
+
# The contents of this file are licensed under the Apache License, Version 2.0
|
5
|
+
# (the "License"); you may not use this file except in compliance with the
|
6
|
+
# License. You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
12
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
13
|
+
# License for the specific language governing permissions and limitations under
|
14
|
+
# the License.
|
15
|
+
|
16
|
+
require 'ffwd/core/emitter'
|
17
|
+
require 'ffwd/core/interface'
|
18
|
+
require 'ffwd/core/reporter'
|
19
|
+
require 'ffwd/lifecycle'
|
20
|
+
require 'ffwd/logging'
|
21
|
+
require 'ffwd/plugin_channel'
|
22
|
+
require 'ffwd/tunnel/plugin'
|
23
|
+
require 'ffwd/utils'
|
24
|
+
|
25
|
+
module FFWD::Plugin::Tunnel
|
26
|
+
class BinaryProtocol < FFWD::Tunnel::Plugin
|
27
|
+
class BindUDP
|
28
|
+
class Handle < FFWD::Tunnel::Plugin::Handle
|
29
|
+
attr_reader :addr
|
30
|
+
|
31
|
+
def initialize bind, addr
|
32
|
+
@bind = bind
|
33
|
+
@addr = addr
|
34
|
+
end
|
35
|
+
|
36
|
+
def send_data data
|
37
|
+
@bind.send_data @addr, data
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize port, family, tunnel, block
|
42
|
+
@port = port
|
43
|
+
@family = family
|
44
|
+
@tunnel = tunnel
|
45
|
+
@block = block
|
46
|
+
end
|
47
|
+
|
48
|
+
def send_data addr, data
|
49
|
+
@tunnel.send_data Socket::SOCK_DGRAM, @family, @port, addr, data
|
50
|
+
end
|
51
|
+
|
52
|
+
def data! addr, data
|
53
|
+
handle = Handle.new self, addr
|
54
|
+
@block.call handle, data
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class BindTCP
|
59
|
+
class Handle < FFWD::Tunnel::Plugin::Handle
|
60
|
+
attr_reader :addr
|
61
|
+
|
62
|
+
def initialize bind, addr
|
63
|
+
@bind = bind
|
64
|
+
@addr = addr
|
65
|
+
@close = nil
|
66
|
+
@data = nil
|
67
|
+
end
|
68
|
+
|
69
|
+
def send_data data
|
70
|
+
@bind.send_data @addr, data
|
71
|
+
end
|
72
|
+
|
73
|
+
def close &block
|
74
|
+
@close = block
|
75
|
+
end
|
76
|
+
|
77
|
+
def data &block
|
78
|
+
@data = block
|
79
|
+
end
|
80
|
+
|
81
|
+
def recv_close
|
82
|
+
return if @close.nil?
|
83
|
+
@close.call
|
84
|
+
@close = nil
|
85
|
+
@data = nil
|
86
|
+
end
|
87
|
+
|
88
|
+
def recv_data data
|
89
|
+
return if @data.nil?
|
90
|
+
@data.call data
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def initialize port, family, tunnel, block
|
95
|
+
@port = port
|
96
|
+
@family = family
|
97
|
+
@tunnel = tunnel
|
98
|
+
@block = block
|
99
|
+
@handles = {}
|
100
|
+
end
|
101
|
+
|
102
|
+
def open addr
|
103
|
+
raise "Already open: #{addr}" if @handles[addr]
|
104
|
+
handle = @handles[addr] = Handle.new self, addr
|
105
|
+
@block.call handle
|
106
|
+
end
|
107
|
+
|
108
|
+
def close addr
|
109
|
+
raise "Not open: #{addr}" unless handle = @handles[addr]
|
110
|
+
handle.recv_close
|
111
|
+
@handles.delete addr
|
112
|
+
end
|
113
|
+
|
114
|
+
def data addr, data
|
115
|
+
unless handle = @handles[addr]
|
116
|
+
raise "Not available: #{addr}"
|
117
|
+
end
|
118
|
+
|
119
|
+
handle.recv_data data
|
120
|
+
end
|
121
|
+
|
122
|
+
def send_data addr, data
|
123
|
+
@tunnel.send_data Socket::SOCK_STREAM, @family, @port, addr, data
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
include FFWD::Logging
|
128
|
+
include FFWD::Lifecycle
|
129
|
+
|
130
|
+
Header = Struct.new(:length, :type, :port, :family, :protocol)
|
131
|
+
HeaderFormat = 'nnnCC'
|
132
|
+
HeaderSize = 8
|
133
|
+
|
134
|
+
PeerAddrAfInet = Struct.new(:ip, :port)
|
135
|
+
PeerAddrAfInetFormat = "a4n"
|
136
|
+
PeerAddrAfInetSize = 6
|
137
|
+
|
138
|
+
PeerAddrAfInet6 = Struct.new(:ip, :port)
|
139
|
+
PeerAddrAfInet6Format = "a16n"
|
140
|
+
PeerAddrAfInet6Size = 18
|
141
|
+
|
142
|
+
State = Struct.new(:state)
|
143
|
+
StateFormat = 'n'
|
144
|
+
StateSize = 2
|
145
|
+
|
146
|
+
STATE = 0x0000
|
147
|
+
DATA = 0x0001
|
148
|
+
|
149
|
+
OPEN = 0x0000
|
150
|
+
CLOSE = 0x0001
|
151
|
+
|
152
|
+
def initialize core, c
|
153
|
+
@c = c
|
154
|
+
@tcp_bind = {}
|
155
|
+
@udp_bind = {}
|
156
|
+
@input = FFWD::PluginChannel.build 'tunnel_input'
|
157
|
+
|
158
|
+
@metadata = nil
|
159
|
+
@processor = nil
|
160
|
+
@channel_id = nil
|
161
|
+
@statistics_id = nil
|
162
|
+
@header = nil
|
163
|
+
|
164
|
+
starting do
|
165
|
+
raise "no metadata" if @metadata.nil?
|
166
|
+
|
167
|
+
if host = @metadata[:host]
|
168
|
+
@statistics_id = "tunnel/#{host}"
|
169
|
+
@channel_id = "tunnel.input/#{host}"
|
170
|
+
else
|
171
|
+
@statistics_id = "tunnel/#{@c.get_peer}"
|
172
|
+
@channel_id = "tunnel.input/#{@c.get_peer}"
|
173
|
+
end
|
174
|
+
|
175
|
+
# setup a small core
|
176
|
+
emitter = FFWD::Core::Emitter.build @core.output, @metadata
|
177
|
+
@processor = FFWD::Core::Processor.build @input, emitter, @core.processors
|
178
|
+
|
179
|
+
@reporter = FFWD::Core::Reporter.new [@input, @processor]
|
180
|
+
|
181
|
+
if @core.debug
|
182
|
+
@core.debug.monitor @channel_id, @input, FFWD::Debug::Input
|
183
|
+
end
|
184
|
+
|
185
|
+
if @core.statistics
|
186
|
+
@core.statistics.register @statistics_id, @reporter
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
stopping do
|
191
|
+
if @core.statistics and @statistics_id
|
192
|
+
@core.statistics.unregister @statistics_id
|
193
|
+
@statistics_id = nil
|
194
|
+
end
|
195
|
+
|
196
|
+
@metadata = nil
|
197
|
+
@processor = nil
|
198
|
+
@channel_id = nil
|
199
|
+
@tcp_bind = {}
|
200
|
+
@udp_bind = {}
|
201
|
+
end
|
202
|
+
|
203
|
+
@core = core.reconnect @input
|
204
|
+
|
205
|
+
@core.tunnel_plugins.each do |t|
|
206
|
+
instance = t.setup @core, self
|
207
|
+
instance.depend_on self
|
208
|
+
end
|
209
|
+
|
210
|
+
@input.depend_on self
|
211
|
+
@core.depend_on self
|
212
|
+
end
|
213
|
+
|
214
|
+
def tcp port, &block
|
215
|
+
@tcp_bind[[port, Socket::AF_INET]] = BindTCP.new(
|
216
|
+
port, Socket::AF_INET, self, block)
|
217
|
+
end
|
218
|
+
|
219
|
+
def udp port, &block
|
220
|
+
@udp_bind[[port, Socket::AF_INET]] = BindUDP.new(
|
221
|
+
port, Socket::AF_INET, self, block)
|
222
|
+
end
|
223
|
+
|
224
|
+
def read_metadata data
|
225
|
+
d = {}
|
226
|
+
|
227
|
+
d[:tags] = FFWD.merge_sets @core.tags, data["tags"]
|
228
|
+
d[:attributes] = FFWD.merge_sets @core.attributes, data["attributes"]
|
229
|
+
|
230
|
+
if host = data["host"]
|
231
|
+
d[:host] = host
|
232
|
+
end
|
233
|
+
|
234
|
+
if ttl = data["ttl"]
|
235
|
+
d[:ttl] = ttl
|
236
|
+
end
|
237
|
+
|
238
|
+
d
|
239
|
+
end
|
240
|
+
|
241
|
+
def send_config
|
242
|
+
response = {}
|
243
|
+
|
244
|
+
tcp = @tcp_bind.keys.map{|port, family|
|
245
|
+
{:protocol => Socket::SOCK_STREAM,
|
246
|
+
:family => family,
|
247
|
+
:port => port}}
|
248
|
+
udp = @udp_bind.keys.map{|port, family|
|
249
|
+
{:protocol => Socket::SOCK_DGRAM,
|
250
|
+
:family => family,
|
251
|
+
:port => port}}
|
252
|
+
|
253
|
+
response[:bind] = tcp + udp
|
254
|
+
|
255
|
+
response = JSON.dump(response)
|
256
|
+
@c.send_data "#{response}\n"
|
257
|
+
end
|
258
|
+
|
259
|
+
def receive_metadata data
|
260
|
+
@metadata = read_metadata data
|
261
|
+
start
|
262
|
+
send_config
|
263
|
+
end
|
264
|
+
|
265
|
+
def receive_line line
|
266
|
+
raise "already have metadata" if @metadata
|
267
|
+
receive_metadata JSON.load(line)
|
268
|
+
@c.set_text_mode HeaderSize
|
269
|
+
end
|
270
|
+
|
271
|
+
def parse_addr_format family
|
272
|
+
if family == Socket::AF_INET
|
273
|
+
return PeerAddrAfInetFormat, PeerAddrAfInetSize
|
274
|
+
end
|
275
|
+
|
276
|
+
if family == Socket::AF_INET6
|
277
|
+
return PeerAddrAfInet6Format, PeerAddrAfInet6Size
|
278
|
+
end
|
279
|
+
|
280
|
+
raise "Unsupported address family: #{family}"
|
281
|
+
end
|
282
|
+
|
283
|
+
def peer_addr_pack family, addr
|
284
|
+
format, size = parse_addr_format family
|
285
|
+
return addr.pack(format), size
|
286
|
+
end
|
287
|
+
|
288
|
+
def peer_addr_unpack family, data
|
289
|
+
format, size = parse_addr_format family
|
290
|
+
return data[0,size].unpack(format), size
|
291
|
+
end
|
292
|
+
|
293
|
+
def receive_frame header, addr, data
|
294
|
+
if header.type == DATA
|
295
|
+
tunnel_data header, addr, data
|
296
|
+
return
|
297
|
+
end
|
298
|
+
|
299
|
+
if header.type == STATE
|
300
|
+
state = data.unpack(StateFormat)[0]
|
301
|
+
tunnel_state header, addr, state
|
302
|
+
return
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
def receive_binary_data data
|
307
|
+
if @header
|
308
|
+
addr, addr_size = peer_addr_unpack @header.family, data
|
309
|
+
data = data[addr_size,data.size - addr_size]
|
310
|
+
receive_frame @header, addr, data
|
311
|
+
@header = nil
|
312
|
+
@c.set_text_mode HeaderSize
|
313
|
+
return
|
314
|
+
end
|
315
|
+
|
316
|
+
fields = data.unpack HeaderFormat
|
317
|
+
@header = Header.new(*fields)
|
318
|
+
rest = @header.length - HeaderSize
|
319
|
+
@c.set_text_mode rest
|
320
|
+
end
|
321
|
+
|
322
|
+
def send_data protocol, family, port, addr, data
|
323
|
+
addr_data, addr_size = peer_addr_pack family, addr
|
324
|
+
length = HeaderSize + addr_size + data.size
|
325
|
+
# Struct.new(:length, :type, :port, :family, :protocol)
|
326
|
+
header_data = [
|
327
|
+
length, DATA, port, family, protocol].pack HeaderFormat
|
328
|
+
frame = header_data + addr_data + data
|
329
|
+
@c.send_data frame
|
330
|
+
end
|
331
|
+
|
332
|
+
def tunnel_data header, addr, data
|
333
|
+
if header.protocol == Socket::SOCK_DGRAM
|
334
|
+
if udp = @udp_bind[[header.port, header.family]]
|
335
|
+
udp.data! addr, data
|
336
|
+
end
|
337
|
+
|
338
|
+
return
|
339
|
+
end
|
340
|
+
|
341
|
+
if header.protocol == Socket::SOCK_STREAM
|
342
|
+
unless bind = @tcp_bind[[header.port, header.family]]
|
343
|
+
log.error "Nothing listening on tcp/#{header.port}"
|
344
|
+
return
|
345
|
+
end
|
346
|
+
|
347
|
+
bind.data addr, data
|
348
|
+
return
|
349
|
+
end
|
350
|
+
|
351
|
+
log.error "DATA: Unsupported protocol: #{header.protocol}"
|
352
|
+
end
|
353
|
+
|
354
|
+
def tunnel_state header, addr, state
|
355
|
+
if header.protocol == Socket::SOCK_DGRAM
|
356
|
+
# ignored
|
357
|
+
log.error "UDP does not handle: #{state}"
|
358
|
+
return
|
359
|
+
end
|
360
|
+
|
361
|
+
if header.protocol == Socket::SOCK_STREAM
|
362
|
+
unless bind = @tcp_bind[[header.port, header.family]]
|
363
|
+
log.error "Nothing listening on tcp/#{header.port}"
|
364
|
+
return
|
365
|
+
end
|
366
|
+
|
367
|
+
if state == OPEN
|
368
|
+
bind.open addr
|
369
|
+
return
|
370
|
+
end
|
371
|
+
|
372
|
+
if state == CLOSE
|
373
|
+
bind.close addr
|
374
|
+
return
|
375
|
+
end
|
376
|
+
|
377
|
+
log.error "Unknown state: #{state}"
|
378
|
+
return
|
379
|
+
end
|
380
|
+
|
381
|
+
log.error "STATE: Unsupported protocol: #{header.protocol}"
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# $LICENSE
|
2
|
+
# Copyright 2013-2014 Spotify AB. All rights reserved.
|
3
|
+
#
|
4
|
+
# The contents of this file are licensed under the Apache License, Version 2.0
|
5
|
+
# (the "License"); you may not use this file except in compliance with the
|
6
|
+
# License. You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
12
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
13
|
+
# License for the specific language governing permissions and limitations under
|
14
|
+
# the License.
|
15
|
+
|
16
|
+
require 'ffwd/connection'
|
17
|
+
|
18
|
+
module FFWD::Plugin::Tunnel
|
19
|
+
class ConnectionTCP < FFWD::Connection
|
20
|
+
include FFWD::Logging
|
21
|
+
include EM::Protocols::LineText2
|
22
|
+
|
23
|
+
def self.plugin_type
|
24
|
+
"tunnel_in_tcp"
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize bind, core, tunnel_protocol
|
28
|
+
@bind = bind
|
29
|
+
@core = core
|
30
|
+
@tunnel_protocol = tunnel_protocol
|
31
|
+
@protocol_instance = nil
|
32
|
+
end
|
33
|
+
|
34
|
+
def receive_line line
|
35
|
+
@protocol_instance.receive_line line
|
36
|
+
end
|
37
|
+
|
38
|
+
def receive_binary_data data
|
39
|
+
@protocol_instance.receive_binary_data data
|
40
|
+
end
|
41
|
+
|
42
|
+
def get_peer
|
43
|
+
peer = get_peername
|
44
|
+
port, ip = Socket.unpack_sockaddr_in(peer)
|
45
|
+
"#{ip}:#{port}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def post_init
|
49
|
+
@protocol_instance = @tunnel_protocol.new @core, self
|
50
|
+
end
|
51
|
+
|
52
|
+
def unbind
|
53
|
+
log.info "Shutting down tunnel connection"
|
54
|
+
@protocol_instance.stop
|
55
|
+
@protocol_instance = nil
|
56
|
+
end
|
57
|
+
|
58
|
+
def metadata?
|
59
|
+
not @metadata.nil?
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# $LICENSE
|
2
|
+
# Copyright 2013-2014 Spotify AB. All rights reserved.
|
3
|
+
#
|
4
|
+
# The contents of this file are licensed under the Apache License, Version 2.0
|
5
|
+
# (the "License"); you may not use this file except in compliance with the
|
6
|
+
# License. You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
12
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
13
|
+
# License for the specific language governing permissions and limitations under
|
14
|
+
# the License.
|
15
|
+
|
16
|
+
module FFWD
|
17
|
+
module Plugin
|
18
|
+
module Tunnel
|
19
|
+
VERSION = "0.1.0"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# $LICENSE
|
2
|
+
# Copyright 2013-2014 Spotify AB. All rights reserved.
|
3
|
+
#
|
4
|
+
# The contents of this file are licensed under the Apache License, Version 2.0
|
5
|
+
# (the "License"); you may not use this file except in compliance with the
|
6
|
+
# License. You may obtain a copy of the License at
|
7
|
+
#
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
9
|
+
#
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
12
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
13
|
+
# License for the specific language governing permissions and limitations under
|
14
|
+
# the License.
|
15
|
+
|
16
|
+
require 'eventmachine'
|
17
|
+
require 'base64'
|
18
|
+
|
19
|
+
require_relative 'tunnel/connection_tcp'
|
20
|
+
require_relative 'tunnel/binary_protocol'
|
21
|
+
|
22
|
+
require 'ffwd/logging'
|
23
|
+
require 'ffwd/plugin'
|
24
|
+
require 'ffwd/protocol'
|
25
|
+
|
26
|
+
module FFWD::Plugin::Tunnel
|
27
|
+
include FFWD::Plugin
|
28
|
+
include FFWD::Logging
|
29
|
+
|
30
|
+
register_plugin "tunnel"
|
31
|
+
|
32
|
+
DEFAULT_HOST = 'localhost'
|
33
|
+
DEFAULT_PORT = 9000
|
34
|
+
DEFAULT_PROTOCOL = 'tcp'
|
35
|
+
DEFAULT_PROTOCOL_TYPE = 'text'
|
36
|
+
|
37
|
+
CONNECTIONS = {
|
38
|
+
:tcp => ConnectionTCP
|
39
|
+
}
|
40
|
+
|
41
|
+
def self.setup_input opts, core
|
42
|
+
opts[:host] ||= DEFAULT_HOST
|
43
|
+
opts[:port] ||= DEFAULT_PORT
|
44
|
+
protocol = FFWD.parse_protocol(opts[:protocol] || DEFAULT_PROTOCOL)
|
45
|
+
protocol_type = opts[:protocol_type] || DEFAULT_PROTOCOL_TYPE
|
46
|
+
|
47
|
+
unless connection = CONNECTIONS[protocol.family]
|
48
|
+
raise "No connection for protocol family: #{protocol.family}"
|
49
|
+
end
|
50
|
+
|
51
|
+
if core.tunnel_plugins.empty?
|
52
|
+
raise "Nothing requires tunneling"
|
53
|
+
end
|
54
|
+
|
55
|
+
protocol.bind opts, core, log, connection, BinaryProtocol
|
56
|
+
end
|
57
|
+
end
|
metadata
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ffwd-tunnel
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- John-John Tedro
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-02-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: ffwd
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec-mocks
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- udoprog@spotify.com
|
58
|
+
executables:
|
59
|
+
- ffwd-tunnel-agent
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- bin/ffwd-tunnel-agent
|
64
|
+
- lib/ffwd/plugin/tunnel.rb
|
65
|
+
- lib/ffwd/plugin/tunnel/binary_protocol.rb
|
66
|
+
- lib/ffwd/plugin/tunnel/version.rb
|
67
|
+
- lib/ffwd/plugin/tunnel/connection_tcp.rb
|
68
|
+
homepage: https://github.com/spotify/ffwd
|
69
|
+
licenses:
|
70
|
+
- Apache 2.0
|
71
|
+
metadata: {}
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options: []
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - '>='
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
requirements: []
|
87
|
+
rubyforge_project:
|
88
|
+
rubygems_version: 2.0.3
|
89
|
+
signing_key:
|
90
|
+
specification_version: 4
|
91
|
+
summary: Simple tunneling support for FFWD.
|
92
|
+
test_files: []
|