-
Notifications
You must be signed in to change notification settings - Fork 0
/
captive_portal
executable file
·216 lines (160 loc) · 5.65 KB
/
captive_portal
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
#!/usr/bin/env python
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""Command-line client to start the captive portal."""
__author__ = '[email protected] (Will Angley)'
import fnmatch
import os
import subprocess
import sys
import urlparse
import google3
import bup.options
import tr.helpers
optspec = """
captive_portal start [options...]
captive_portal restart [options...]
captive_portal stop
--
a,allowed-ips= list of allowed IPs/subnets []
A,authorizer-url= authorizer URL to query []
d,dry-run don't modify iptables
e,extra-tls-hosts= list of extra allowed TLS hosts []
p,port= port on which HTTP bouncer will listen [8888]
u,url= redirect URL []
"""
sniproxy_conf = """# -- captive_portal filtering config --
resolver {
nameserver 127.0.0.1
}
listen 8443 {
protocol tls
table https_hosts
}
table https_hosts {
%(tls_host_lines)s
}
"""
RUNFILES_DIR = ['/tmp/run']
SNIPROXY_CONFIG_FILE = ['/tmp/sniproxy.conf']
def iptables(*args):
if opt.dry_run:
return 0
return subprocess.call(['iptables'] + list(args))
def ip6tables(*args):
if opt.dry_run:
return 0
return subprocess.call(['ip6tables'] + list(args))
def ip46tables(*args):
return iptables(*args) | ip6tables(*args)
def pidfile(name):
return os.path.join(RUNFILES_DIR[0], '%s.pid' % name)
def babysit(name, *args):
"""Start a command under the babysitter."""
baby = subprocess.Popen(('babysit', '60', 'startpid', pidfile(name), name)
+ args)
if baby.poll():
print >>sys.stderr, '%s failed to start!' % name
return baby.returncode
return 0
def start_allowed_ips():
"""Configure iptables to allow traffic to whitelisted IP addresses."""
code = 0
for dest in opt.allowed_ips.split():
# IPv4 only because our version of TR-069 is too
code |= iptables('-t', 'filter', '-A', 'acs-captive-portal-filter',
'-d', dest, '-j', 'ACCEPT')
code |= iptables('-t', 'nat', '-A', 'acs-captive-portal-nat', '-d', dest,
'-j', 'ACCEPT')
return code
def stop_iptables():
iptables('-t', 'nat', '-F', 'acs-captive-portal-nat')
ip46tables('-t', 'filter', '-F', 'acs-captive-portal-input')
ip46tables('-t', 'filter', '-F', 'acs-captive-portal-filter')
def start_http_bouncer():
"""Start the HTTP bouncer and NAT outbound traffic through it."""
code = babysit('http_bouncer', '-p', str(opt.port), '-u', opt.url)
if code:
return code
code |= ip46tables('-t', 'filter', '-A', 'acs-captive-portal-input',
'-p', 'tcp', '--dport', str(opt.port), '-j', 'ACCEPT')
# TODO(willangley): should this also work on IPv6?
code |= iptables('-t', 'nat', '-A', 'acs-captive-portal-nat', '-p', 'tcp',
'--dport', '80', '-j', 'REDIRECT',
'--to-ports', str(opt.port))
return code
def stop_http_bouncer():
subprocess.call(['killpid', pidfile('http_bouncer')])
def start_authorizer():
"""Start the authorizer."""
return babysit('authorizer', '-u', opt.authorizer_url)
def stop_authorizer():
subprocess.call(['killpid', pidfile('authorizer')])
def tls_host_lines(tls_hosts):
"""Make sniproxy config lines allowing hosts or wildcards in tls_hosts."""
config_fragments = []
for host in tls_hosts:
# wildcards are easier to work with, but sniproxy needs regexps
if '*' in host:
host_with_gunk = fnmatch.translate(host)
host = '%s$' % host_with_gunk[:-len(fnmatch.translate(''))]
host = host.replace('\\', '\\\\')
config_fragments.append(' %s *:443' % host)
return '\n'.join(config_fragments)
def start_sniproxy():
"""Start sniproxy, with a custom config if requested on the command line."""
tls_hosts = []
if opt.url:
redirect_host = urlparse.urlparse(opt.url).netloc
tls_hosts.append(redirect_host)
if opt.extra_tls_hosts:
tls_hosts += opt.extra_tls_hosts.split()
if tls_hosts:
tr.helpers.WriteFileAtomic(
SNIPROXY_CONFIG_FILE[0],
sniproxy_conf % {
'tls_host_lines': tls_host_lines(tls_hosts)})
else:
# sniproxy will fall back to a config baked into the image in this case.
if os.path.exists(SNIPROXY_CONFIG_FILE[0]):
os.unlink(SNIPROXY_CONFIG_FILE[0])
return subprocess.call(['start', 'sniproxy'])
def stop_sniproxy():
subprocess.call(['stop', 'sniproxy'])
if __name__ == '__main__':
o = bup.options.Options(optspec)
(opt, unused_flags, extra) = o.parse(sys.argv[1:])
if len(extra) != 1:
o.fatal('Got %d commands, want exactly 1.' % len(extra))
exitcode = 0
command = extra[0]
if command in ['start', 'restart', 'stop']:
stop_authorizer()
stop_http_bouncer()
stop_iptables()
stop_sniproxy()
else:
o.fatal('Could not understand command: %s' % command)
if command in ['start', 'restart']:
if opt.allowed_ips:
exitcode |= start_allowed_ips()
if opt.port and opt.url:
exitcode |= start_http_bouncer()
if opt.authorizer_url:
exitcode |= start_authorizer()
# We want sniproxy to be running config for autoprovisioning,
# even if the captive portal is otherwise off.
exitcode |= start_sniproxy()
sys.exit(exitcode)