blob: 006ce34c2d6db5d2e5fc83a0e84ffd3c78cfbd69 [file] [log] [blame]
#!/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__ = 'willangley@google.com (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)