1
|
|
|
# encoding: utf-8 |
2
|
|
|
""" |
3
|
|
|
tcp.py |
4
|
|
|
|
5
|
|
|
Created by Thomas Mangin on 2013-07-13. |
6
|
|
|
Copyright (c) 2013-2017 Exa Networks. All rights reserved. |
7
|
|
|
License: 3-clause BSD. (See the COPYRIGHT file) |
8
|
|
|
""" |
9
|
|
|
|
10
|
|
|
import re |
11
|
|
|
import base64 |
12
|
|
|
import socket |
13
|
|
|
import select |
14
|
|
|
import platform |
15
|
|
|
|
16
|
|
|
from struct import pack, calcsize |
17
|
|
|
|
18
|
|
|
from exabgp.util.errstr import errstr |
19
|
|
|
|
20
|
|
|
from exabgp.protocol.family import AFI |
21
|
|
|
from exabgp.protocol.ip import IP |
22
|
|
|
from exabgp.reactor.network.error import errno |
23
|
|
|
from exabgp.reactor.network.error import error |
24
|
|
|
|
25
|
|
|
from exabgp.reactor.network.error import NotConnected |
26
|
|
|
from exabgp.reactor.network.error import BindingError |
27
|
|
|
from exabgp.reactor.network.error import MD5Error |
28
|
|
|
from exabgp.reactor.network.error import NagleError |
29
|
|
|
from exabgp.reactor.network.error import TTLError |
30
|
|
|
from exabgp.reactor.network.error import AsyncError |
31
|
|
|
|
32
|
|
|
from exabgp.logger import log |
33
|
|
|
|
34
|
|
|
|
35
|
|
|
def create(afi): |
36
|
|
|
try: |
37
|
|
|
if afi == AFI.ipv4: |
38
|
|
|
io = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) |
39
|
|
|
if afi == AFI.ipv6: |
40
|
|
|
io = socket.socket(socket.AF_INET6, socket.SOCK_STREAM, socket.IPPROTO_TCP) |
41
|
|
|
try: |
42
|
|
|
io.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
|
|
|
|
43
|
|
|
except (socket.error, AttributeError): |
44
|
|
|
pass |
45
|
|
|
try: |
46
|
|
|
io.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) # pylint: disable=E1101 |
47
|
|
|
except (socket.error, AttributeError): |
48
|
|
|
pass |
49
|
|
|
except socket.error: |
50
|
|
|
raise NotConnected('Could not create socket') |
51
|
|
|
return io |
52
|
|
|
|
53
|
|
|
|
54
|
|
|
def bind(io, ip, afi): |
55
|
|
|
try: |
56
|
|
|
if afi == AFI.ipv4: |
57
|
|
|
io.bind((ip, 0)) |
58
|
|
|
if afi == AFI.ipv6: |
59
|
|
|
io.bind((ip, 0, 0, 0)) |
60
|
|
|
except socket.error as exc: |
61
|
|
|
raise BindingError('Could not bind to local ip %s - %s' % (ip, str(exc))) |
62
|
|
|
|
63
|
|
|
|
64
|
|
|
def connect(io, ip, port, afi, md5): |
65
|
|
|
try: |
66
|
|
|
if afi == AFI.ipv4: |
67
|
|
|
io.connect((ip, port)) |
68
|
|
|
if afi == AFI.ipv6: |
69
|
|
|
io.connect((ip, port, 0, 0)) |
70
|
|
|
except socket.error as exc: |
71
|
|
|
if exc.errno == errno.EINPROGRESS: |
72
|
|
|
return |
73
|
|
|
if md5: |
74
|
|
|
raise NotConnected( |
75
|
|
|
'Could not connect to peer %s:%d, check your MD5 password (%s)' % (ip, port, errstr(exc)) |
76
|
|
|
) |
77
|
|
|
raise NotConnected('Could not connect to peer %s:%d (%s)' % (ip, port, errstr(exc))) |
78
|
|
|
|
79
|
|
|
|
80
|
|
|
# http://lxr.free-electrons.com/source/include/uapi/linux/tcp.h#L197 |
81
|
|
|
# |
82
|
|
|
# #define TCP_MD5SIG_MAXKEYLEN 80 |
83
|
|
|
# |
84
|
|
|
# struct tcp_md5sig { |
85
|
|
|
# struct __kernel_sockaddr_storage tcpm_addr; /* address associated */ 128 |
86
|
|
|
# __u16 __tcpm_pad1; /* zero */ 2 |
87
|
|
|
# __u16 tcpm_keylen; /* key length */ 2 |
88
|
|
|
# __u32 __tcpm_pad2; /* zero */ 4 |
89
|
|
|
# __u8 tcpm_key[TCP_MD5SIG_MAXKEYLEN]; /* key (binary) */ 80 |
90
|
|
|
# } |
91
|
|
|
# |
92
|
|
|
# #define _K_SS_MAXSIZE 128 |
93
|
|
|
# |
94
|
|
|
# #define _K_SS_ALIGNSIZE (__alignof__ (struct sockaddr *)) |
95
|
|
|
# /* Implementation specific desired alignment */ |
96
|
|
|
# |
97
|
|
|
# typedef unsigned short __kernel_sa_family_t; |
98
|
|
|
# |
99
|
|
|
# struct __kernel_sockaddr_storage { |
100
|
|
|
# __kernel_sa_family_t ss_family; /* address family */ |
101
|
|
|
# /* Following field(s) are implementation specific */ |
102
|
|
|
# char __data[_K_SS_MAXSIZE - sizeof(unsigned short)]; |
103
|
|
|
# /* space to achieve desired size, */ |
104
|
|
|
# /* _SS_MAXSIZE value minus size of ss_family */ |
105
|
|
|
# } __attribute__ ((aligned(_K_SS_ALIGNSIZE))); /* force desired alignment */ |
106
|
|
|
|
107
|
|
|
|
108
|
|
|
def MD5(io, ip, port, md5, md5_base64): |
109
|
|
|
platform_os = platform.system() |
110
|
|
|
if platform_os == 'FreeBSD': |
111
|
|
|
if md5: |
112
|
|
|
if md5 != 'kernel': |
113
|
|
|
raise MD5Error( |
114
|
|
|
'FreeBSD requires that you set your MD5 key via ipsec.conf.\n' |
115
|
|
|
'Something like:\n' |
116
|
|
|
'flush;\n' |
117
|
|
|
'add <local ip> <peer ip> tcp 0x1000 -A tcp-md5 "password";' |
118
|
|
|
) |
119
|
|
|
try: |
120
|
|
|
TCP_MD5SIG = 0x10 |
121
|
|
|
io.setsockopt(socket.IPPROTO_TCP, TCP_MD5SIG, 1) |
122
|
|
|
except socket.error: |
123
|
|
|
raise MD5Error( |
124
|
|
|
'FreeBSD requires that you rebuild your kernel to enable TCP MD5 Signatures:\n' |
125
|
|
|
'options IPSEC\n' |
126
|
|
|
'options TCP_SIGNATURE\n' |
127
|
|
|
'device crypto\n' |
128
|
|
|
) |
129
|
|
|
elif platform_os == 'Linux': |
130
|
|
|
try: |
131
|
|
|
md5_bytes = None |
132
|
|
|
if md5: |
133
|
|
|
if md5_base64 is True: |
134
|
|
|
try: |
135
|
|
|
md5_bytes = base64.b64decode(md5) |
136
|
|
|
except TypeError: |
137
|
|
|
raise MD5Error("Failed to decode base 64 encoded PSK") |
138
|
|
|
elif md5_base64 is None and not re.match('.*[^a-f0-9].*', md5): # auto |
139
|
|
|
options = [md5 + '==', md5 + '=', md5] |
140
|
|
|
for md5 in options: |
141
|
|
|
try: |
142
|
|
|
md5_bytes = base64.b64decode(md5) |
143
|
|
|
break |
144
|
|
|
except TypeError: |
145
|
|
|
pass |
146
|
|
|
|
147
|
|
|
# __kernel_sockaddr_storage |
148
|
|
|
n_af = IP.toaf(ip) |
149
|
|
|
n_addr = IP.pton(ip) |
150
|
|
|
n_port = socket.htons(port) |
151
|
|
|
|
152
|
|
|
# pack 'x' is padding, so we want the struct |
153
|
|
|
# Do not use '!' for the pack, the network (big) endian switch in |
154
|
|
|
# struct.pack is fighting against inet_pton and htons (note the n) |
155
|
|
|
|
156
|
|
|
if IP.toafi(ip) == AFI.ipv4: |
157
|
|
|
# SS_MAXSIZE is 128 but addr_family, port and ipaddr (8 bytes total) are written independently of the padding |
158
|
|
|
SS_MAXSIZE_PADDING = 128 - calcsize('HH4s') # 8 |
159
|
|
|
sockaddr = pack('HH4s%dx' % SS_MAXSIZE_PADDING, socket.AF_INET, n_port, n_addr) |
160
|
|
|
else: |
161
|
|
|
SS_MAXSIZE_PADDING = 128 - calcsize('HI16sI') # 28 |
162
|
|
|
SIN6_FLOWINFO = 0 |
163
|
|
|
SIN6_SCOPE_ID = 0 |
164
|
|
|
sockaddr = pack('HHI16sI%dx' % SS_MAXSIZE_PADDING, n_af, n_port, SIN6_FLOWINFO, n_addr, SIN6_SCOPE_ID) |
165
|
|
|
|
166
|
|
|
TCP_MD5SIG_MAXKEYLEN = 80 |
167
|
|
|
TCP_MD5SIG = 14 |
168
|
|
|
|
169
|
|
|
if md5_bytes: |
170
|
|
|
key = pack('2xH4x%ds' % TCP_MD5SIG_MAXKEYLEN, len(md5_bytes), md5_bytes) |
171
|
|
|
io.setsockopt(socket.IPPROTO_TCP, TCP_MD5SIG, sockaddr + key) |
172
|
|
|
elif md5: |
173
|
|
|
md5_bytes = bytes(md5, 'ascii') |
174
|
|
|
key = pack('2xH4x%ds' % TCP_MD5SIG_MAXKEYLEN, len(md5_bytes), md5_bytes) |
175
|
|
|
io.setsockopt(socket.IPPROTO_TCP, TCP_MD5SIG, sockaddr + key) |
176
|
|
|
# else: |
177
|
|
|
# key = pack('2xH4x%ds' % TCP_MD5SIG_MAXKEYLEN, 0, b'') |
178
|
|
|
# io.setsockopt(socket.IPPROTO_TCP, TCP_MD5SIG, sockaddr + key) |
179
|
|
|
|
180
|
|
|
except socket.error as exc: |
181
|
|
|
if exc.errno != errno.ENOENT: |
182
|
|
|
raise MD5Error('This linux machine does not support TCP_MD5SIG, you can not use MD5 (%s)' % errstr(exc)) |
183
|
|
|
elif md5: |
184
|
|
|
raise MD5Error('ExaBGP has no MD5 support for %s' % platform_os) |
185
|
|
|
|
186
|
|
|
|
187
|
|
|
def nagle(io, ip): |
188
|
|
|
try: |
189
|
|
|
# diable Nagle's algorithm (no grouping of packets) |
190
|
|
|
io.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) |
191
|
|
|
except (socket.error, AttributeError): |
192
|
|
|
raise NagleError("Could not disable nagle's algorithm for %s" % ip) |
193
|
|
|
|
194
|
|
|
|
195
|
|
|
def TTL(io, ip, ttl): |
196
|
|
|
# None (ttl-security unset) or zero (maximum TTL) is the same thing |
197
|
|
|
if ttl: |
198
|
|
|
try: |
199
|
|
|
io.setsockopt(socket.IPPROTO_IP, socket.IP_TTL, ttl) |
200
|
|
|
except socket.error as exc: |
201
|
|
|
raise TTLError('This OS does not support IP_TTL (ttl-security) for %s (%s)' % (ip, errstr(exc))) |
202
|
|
|
|
203
|
|
|
|
204
|
|
|
def TTLv6(io, ip, ttl): |
205
|
|
|
if ttl: |
206
|
|
|
try: |
207
|
|
|
io.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_UNICAST_HOPS, ttl) |
208
|
|
|
except socket.error as exc: |
209
|
|
|
raise TTLError('This OS does not support unicast_hops (ttl-security) for %s (%s)' % (ip, errstr(exc))) |
210
|
|
|
|
211
|
|
|
|
212
|
|
|
def MIN_TTL(io, ip, ttl): |
213
|
|
|
# None (ttl-security unset) or zero (maximum TTL) is the same thing |
214
|
|
|
if ttl: |
215
|
|
|
try: |
216
|
|
|
io.setsockopt(socket.IPPROTO_IP, socket.IP_MINTTL, ttl) |
217
|
|
|
except socket.error as exc: |
218
|
|
|
raise TTLError('This OS does not support IP_MINTTL (ttl-security) for %s (%s)' % (ip, errstr(exc))) |
219
|
|
|
except AttributeError: |
220
|
|
|
pass |
221
|
|
|
|
222
|
|
|
try: |
223
|
|
|
io.setsockopt(socket.IPPROTO_IP, socket.IP_TTL, ttl) |
224
|
|
|
except socket.error as exc: |
225
|
|
|
raise TTLError( |
226
|
|
|
'This OS does not support IP_MINTTL or IP_TTL (ttl-security) for %s (%s)' % (ip, errstr(exc)) |
227
|
|
|
) |
228
|
|
|
|
229
|
|
|
|
230
|
|
|
def asynchronous(io, ip): |
231
|
|
|
try: |
232
|
|
|
io.setblocking(0) |
233
|
|
|
except socket.error as exc: |
234
|
|
|
raise AsyncError('could not set socket non-blocking for %s (%s)' % (ip, errstr(exc))) |
235
|
|
|
|
236
|
|
|
|
237
|
|
|
def ready(io): |
238
|
|
|
poller = select.poll() |
239
|
|
|
poller.register(io, select.POLLOUT | select.POLLNVAL | select.POLLERR) |
240
|
|
|
|
241
|
|
|
found = False |
242
|
|
|
|
243
|
|
|
while True: |
244
|
|
|
try: |
245
|
|
|
for _, event in poller.poll(0): |
246
|
|
|
if event & select.POLLOUT or event & select.POLLIN: |
247
|
|
|
found = True |
248
|
|
|
elif event & select.POLLHUP: |
249
|
|
|
yield False, 'could not connect, retrying' |
250
|
|
|
return |
251
|
|
|
elif event & select.POLLERR or event & select.POLLNVAL: |
252
|
|
|
yield False, 'connect attempt failed, issue with reading on the network, retrying' |
253
|
|
|
return |
254
|
|
|
|
255
|
|
|
if found: |
256
|
|
|
err = io.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) |
257
|
|
|
if not err: |
258
|
|
|
yield True, 'connection established' |
259
|
|
|
return |
260
|
|
|
elif err in error.block: |
261
|
|
|
yield False, 'connect attempt failed, retrying, reason %s' % errno.errorcode[err] |
262
|
|
|
return |
263
|
|
|
yield False, 'waiting for socket to become ready' |
264
|
|
|
except select.error as err: |
265
|
|
|
yield False, 'error, retrying %s' % str(err) |
266
|
|
|
return |
267
|
|
|
|