summaryrefslogtreecommitdiffstats
path: root/sshuttle/options.py
blob: 0a311aa0c042b9bcbbd07e44da35f8fcf9cf79bd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
import re
import socket
from argparse import ArgumentParser, Action, ArgumentTypeError as Fatal

from sshuttle import __version__


# Subnet file, supporting empty lines and hash-started comment lines
def parse_subnetport_file(s):
    try:
        handle = open(s, 'r')
    except OSError:
        raise Fatal('Unable to open subnet file: %s' % s)

    raw_config_lines = handle.readlines()
    subnets = []
    for _, line in enumerate(raw_config_lines):
        line = line.strip()
        if not line:
            continue
        if line[0] == '#':
            continue
        subnets.append(parse_subnetport(line))

    return subnets


# 1.2.3.4/5:678, 1.2.3.4:567, 1.2.3.4/16 or just 1.2.3.4
# [1:2::3/64]:456, [1:2::3]:456, 1:2::3/64 or just 1:2::3
# example.com:123 or just example.com
#
# In addition, the port number can be specified as a range:
# 1.2.3.4:8000-8080.
#
# Can return multiple matches if the domain name used in the request
# has multiple IP addresses.
def parse_subnetport(s):

    if s.count(':') > 1:
        rx = r'(?:\[?([\w\:]+)(?:/(\d+))?]?)(?::(\d+)(?:-(\d+))?)?$'
    else:
        rx = r'([\w\.\-]+)(?:/(\d+))?(?::(\d+)(?:-(\d+))?)?$'

    m = re.match(rx, s)
    if not m:
        raise Fatal('%r is not a valid address/mask:port format' % s)

    # Ports range from fport to lport. If only one port is specified,
    # fport is defined and lport is None.
    #
    # cidr is the mask defined with the slash notation
    host, cidr, fport, lport = m.groups()
    try:
        addrinfo = socket.getaddrinfo(host, 0, 0, socket.SOCK_STREAM)
    except socket.gaierror:
        raise Fatal('Unable to resolve address: %s' % host)

    # If the address is a domain with multiple IPs and a mask is also
    # provided, proceed cautiously:
    if cidr is not None:
        addr_v6 = [a for a in addrinfo if a[0] == socket.AF_INET6]
        addr_v4 = [a for a in addrinfo if a[0] == socket.AF_INET]

        # Refuse to proceed if IPv4 and IPv6 addresses are present:
        if len(addr_v6) > 0 and len(addr_v4) > 0:
            raise Fatal("%s has IPv4 and IPv6 addresses, so the mask "
                        "of /%s is not supported. Specify the IP "
                        "addresses directly if you wish to specify "
                        "a mask." % (host, cidr))

        # Warn if a domain has multiple IPs of the same type (IPv4 vs
        # IPv6) and the mask is applied to all of the IPs.
        if len(addr_v4) > 1 or len(addr_v6) > 1:
            print("WARNING: %s has multiple IP addresses. The "
                  "mask of /%s is applied to all of the addresses."
                  % (host, cidr))

    rv = []
    for a in addrinfo:
        family, _, _, _, addr = a

        # Largest possible slash value we can use with this IP:
        max_cidr = 32 if family == socket.AF_INET else 128

        if cidr is None:  # if no mask, use largest mask
            cidr_to_use = max_cidr
        else:   # verify user-provided mask is appropriate
            cidr_to_use = int(cidr)
            if not 0 <= cidr_to_use <= max_cidr:
                raise Fatal('Slash in CIDR notation (/%d) is '
                            'not between 0 and %d'
                            % (cidr_to_use, max_cidr))

        rv.append((family, addr[0], cidr_to_use,
                   int(fport or 0), int(lport or fport or 0)))

    return rv


# 1.2.3.4:567 or just 1.2.3.4 or just 567
# [1:2::3]:456 or [1:2::3] or just [::]:567
# example.com:123 or just example.com
def parse_ipport(s):
    s = str(s)
    if s.isdigit():
        rx = r'()(\d+)$'
    elif ']' in s:
        rx = r'(?:\[([^]]+)])(?::(\d+))?$'
    else:
        rx = r'([\w\.\-]+)(?::(\d+))?$'

    m = re.match(rx, s)
    if not m:
        raise Fatal('%r is not a valid IP:port format' % s)

    host, port = m.groups()
    host = host or '0.0.0.0'
    port = int(port or 0)

    try:
        addrinfo = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)
    except socket.gaierror:
        raise Fatal('Unable to resolve address: %s' % host)

    if len(addrinfo) > 1:
        print("WARNING: Host %s has more than one IP, only using one of them."
              % host)

    family, _, _, _, addr = min(addrinfo)
    # Note: addr contains (ip, port)
    return (family,) + addr[:2]


def parse_list(lst):
    """Parse a comma separated string into a list."""
    return re.split(r'[\s,]+', lst.strip()) if lst else []


class Concat(Action):
    def __init__(self, option_strings, dest, nargs=None, **kwargs):
        if nargs is not None:
            raise ValueError("nargs not supported")
        super(Concat, self).__init__(option_strings, dest, **kwargs)

    def __call__(self, parser, namespace, values, option_string=None):
        curr_value = getattr(namespace, self.dest, None) or []
        setattr(namespace, self.dest, curr_value + values)


# Override one function in the ArgumentParser so that we can have
# better control for how we parse files containing arguments. We
# expect one argument per line, but strip whitespace/quotes from the
# beginning/end of the lines.
class MyArgumentParser(ArgumentParser):
    def convert_arg_line_to_args(self, arg_line):
        # Ignore comments
        if arg_line.startswith("#"):
            return []

        # strip whitespace at beginning and end of line
        arg_line = arg_line.strip()

        # When copying parameters from the command line to a file,
        # some users might copy the quotes they used on the command
        # line into the config file. We ignore these if the line
        # starts and ends with the same quote.
        if arg_line.startswith("'") and arg_line.endswith("'") or \
           arg_line.startswith('"') and arg_line.endswith('"'):
            arg_line = arg_line[1:-1]

        return [arg_line]


parser = MyArgumentParser(
    prog="sshuttle",
    usage="%(prog)s [-l [ip:]port] [-r [user@]sshserver[:port]] <subnets...>",
    fromfile_prefix_chars="@"
)
parser.add_argument(
    "subnets",
    metavar="IP/MASK[:PORT[-PORT]]...",
    nargs="*",
    type=parse_subnetport,
    help="""
    capture and forward traffic to these subnets (whitespace separated)
    """
)
parser.add_argument(
    "-l", "--listen",
    metavar="[IP:]PORT",
    help="""
    transproxy to this ip address and port number
    """
)
parser.add_argument(
    "-H", "--auto-hosts",
    action="store_true",
    help="""
    continuously scan for remote hostnames and update local /etc/hosts as
    they are found
    """
)
parser.add_argument(
    "-N", "--auto-nets",
    action="store_true",
    help="""
    automatically determine subnets to route
    """
)
parser.add_argument(
    "--dns",
    action="store_true",
    help="""
    capture local DNS requests and forward to the remote DNS server
    """
)
parser.add_argument(
    "--ns-hosts",
    metavar="IP[,IP]",
    default=[],
    type=parse_list,
    help="""
    capture and forward DNS requests made to the following servers
    (comma separated)
    """
)
parser.add_argument(
    "--to-ns",
    metavar="IP[:PORT]",
    type=parse_ipport,
    help="""
    the DNS server to forward requests to; defaults to servers in
    /etc/resolv.conf on remote side if not given.
    """
)

parser.add_argument(
    "--method",
    choices=["auto", "nat", "nft", "tproxy", "pf", "ipfw"],
    metavar="TYPE",
    default="auto",
    help="""
    %(choices)s
    """
)
parser.add_argument(
    "--python",
    metavar="PATH",
    help="""
    path to python interpreter on the remote server
    """
)
parser.add_argument(
    "-r", "--remote",
    metavar="[USERNAME[:PASSWORD]@]ADDR[:PORT]",
    help="""
    ssh hostname (and optional username and password) of remote %(prog)s server
    """
)
parser.add_argument(
    "-x", "--exclude",
    metavar="IP/MASK[:PORT[-PORT]]",
    action="append",
    default=[],
    type=parse_subnetport,
    help="""
    exclude this subnet (can be used more than once)
    """
)
parser.add_argument(
    "-X", "--exclude-from",
    metavar="PATH",
    action=Concat,
    dest="exclude",
    type=parse_subnetport_file,
    help="""
    exclude the subnets in a file (whitespace separated)
    """
)
parser.add_argument(
    "-v", "--verbose",
    action="count",
    default=0,
    help="""
    increase debug message verbosity (can be used more than once)
    """
)
parser.add_argument(
    "-V", "--version",
    action="version",
    version=__version__,
    help="""
    print the %(prog)s version number and exit
    """
)
parser.add_argument(
    "-e", "--ssh-cmd",
    metavar="CMD",
    default="ssh",
    help="""
    the command to use to connect to the remote [%(default)s]
    """
)
parser.add_argument(
    "--seed-hosts",
    metavar="HOSTNAME[,HOSTNAME]",
    default=[],
    help="""
    comma-separated list of hostnames for initial scan (may be used with
    or without --auto-hosts)
    """
)
parser.add_argument(
    "--no-latency-control",
    action="store_false",
    dest="latency_control",
    help="""
    sacrifice latency to improve bandwidth benchmarks
    """
)
parser.add_argument(
    "--latency-buffer-size",
    metavar="SIZE",
    type=int,
    default=32768,
    dest="latency_buffer_size",
    help="""
    size of latency control buffer
    """
)
parser.add_argument(
    "--wrap",
    metavar="NUM",
    type=int,
    help="""
    restart counting channel numbers after this number (for testing)
    """
)
parser.add_argument(
    "--disable-ipv6",
    action="store_true",
    help="""
    disable IPv6 support
    """
)
parser.add_argument(
    "-D", "--daemon",
    action="store_true",
    help="""
    run in the background as a daemon
    """
)
parser.add_argument(
    "-s", "--subnets",
    metavar="PATH",
    action=Concat,
    dest="subnets_file",
    default=[],
    type=parse_subnetport_file,
    help="""
    file where the subnets are stored, instead of on the command line
    """
)
parser.add_argument(
    "--syslog"