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