#!/bin/bash -f
### BEGIN INIT INFO
# Provides:          6to4
# Required-Start:    $networking $local_fs $remote_fs
# Required-Stop:     $local_fs $remote_fs
# Should-Start:
# Should-Stop:
# Default-Start:     S
# Default-Stop:      0 1 6
# Short-Description: automatic 6to4 tunnel
# Description:       Set up IPv6 using 6to4 tunnel to IPv4 anycast if possible.
#		     This requires a first-class IPv4 address, however failure
#		     should this not be available is graceful.  An IPv6 subnet
#		     will be created, routed, and advertised on each local
#		     network if radvd is installed.
### END INIT INFO

# Author: Barak A. Pearlmutter

#  auto6to4 - robustly and automatically enable IPv6 cloud around IPv4 host.
#  Copyright 2009-2012, Barak A. Pearlmutter <barak@cs.nuim.ie>
#   Hamilton Institute, NUI Maynooth, Co. Kildare, Ireland
#   http://www.bcl.hamilton.ie/~barak/
#   (Much thanks to David Malone.  David wrote the book on IPv6; buy it!)
#
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 2 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.

# For the latest version visit http://github.com/barak/auto6to4

DESC=auto6to4

# Debian Dependencies
#  Depends: iproute, ipv6calc
#  Suggests: radvd

## See also

# Similar script: http://www.linux.it/~md/6to4
# Information: http://www.wlug.org.nz/6to4

## To Do

# - more graceful error handling
# - figure out how to integrate into /etc/network/if-up.d/ etc
# - copy more schmutz from /etc/init.d/radvd
# - make into nice Debian package
# - use debconf to control radvd and true external IPv4 address
# - after setup test the tunnel and bring down if non-functional

set -e

usage()
{
    echo "Usage: auto6to4 [OPTION]... {start|stop|restart|reload|force-reload}"
    echo
    echo "auto6to4 sets up IPv6 using an IPv4 protocol 41 tunnel to anycast address."
    echo
    echo "  -i <ip-addr>   Use given external IPv4 address (for use behind NAT)"
    echo "  -r             Disable radvd IPv6 route advertiser (if radvd is installed"
    echo "                 and this option is not given, an IPv6 subnet will be routed"
    echo "                 and advertised on the local network.)"
    echo "  -I iface0      Interface for IPv6 subnet (repeat for many; default: all)"
    echo "  -d             Debug (verbose output)"
    echo "  -h, --help     Print this help text and exit"
    echo "  -v, --version  Print version and exit"
}

version()
{
    echo auto6to4 1.20
}

start()
{
    ## Get info and decide whether to bring up tunnel.

    # Check for IPv6 support in kernel
    if [ \! -e /proc/sys/net/ipv6 ]; then
	echo "error: IPv6 support not enabled in kernel, ${DESC} fails"
	exit
    fi

    # Get list of local 1st-class IPv4 addresses, unless given in option,
    # being careful to screen out addresses in non-global ranges.
    if [ -z ${ip4addr} ]; then
	ip4addr=$(ip -oneline -4 addr show \
	    | tr / ' ' \
	    | awk '{print $4}' \
	    | egrep -v '^127[.]' \
	    | egrep -v '^10[.]' \
	    | egrep -v '^172[.]1[6-9][.]' \
	    | egrep -v '^172[.]2[0-9][.]' \
	    | egrep -v '^172[.]3[0-1][.]' \
	    | egrep -v '^192[.]168[.]' \
	    | egrep -v '^169[.]254[.]')
    fi

    ${debug} && echo "${DESC}: Suitable IPv4 addresses: ${ip4addr}"

    # abort if there are none
    if [ -z "${ip4addr}" ]; then
	echo "warning: unable to enable 6to4 tunnel,"
	echo "   no suitable IPv4 address configured."
	exit 1
    fi

    # abort if there are more than one
    if echo "${ip4addr}" | egrep --silent ' '; then
	echo "warning: not attempting to enable 6to4 tunnel,"
	echo "   multiple suitable IPv4 addresses configured."
	echo " Addresses: ${ip4addr}."
	echo " Consider using option -i <IPv4-address-to-use>"
	exit 1
    fi


    ## Attempt to bring up tunnel

    # Convert chosen 1st-class IPv4 address into tunnel endpoint address
    ip6net=$(ipv6calc --in ipv4 --out ipv6 --action conv6to4 ${ip4addr})
    ip6addr=${ip6net}1

    ${debug} && echo "${DESC}: ip6net: ${ip6net}"
    ${debug} && echo "${DESC}: ip6addr: ${ip6addr}"

    delete_tunnel

    flush_tunnel_route

    # configure and start the tunnel
    ${debug} && echo "${DESC}: adding tunnel ${tunnel}"
    ip tunnel add ${tunnel} mode sit ttl 128 remote any local ${ip4addr}
    ${debug} && echo "${DESC}: turning on tunnel ${tunnel}"
    ip link set dev ${tunnel} up
    ${debug} && echo "${DESC}: setting address ${ip6addr}/16 on tunnel ${tunnel}"
    ip -6 addr add ${ip6addr}/16 dev ${tunnel}
    ${debug} && echo "${DESC}: setting route ::/96 via tunnel ${tunnel}"
    ip -6 route add ::/96 dev ${tunnel}
    ${debug} && echo "${DESC}: setting route 2000::/3 via tunnel ${tunnel}"
    ip -6 route add 2000::/3 via ::${ip4relay} dev ${tunnel}


    ## Route and advertise IPv6 addresses on LAN

    if [ -z ${radvd} ]; then
	# echo "radvd disabled"
	exit
    fi

    # check if radvd is available
    if [ \! -x ${radvd} ]; then
	echo "warning: no radvd executable, not advertising"
	exit
    fi

    # Build a configuration file for radvd.
    # We make one stanza for each suitable interface,
    # with a subnet allocated to each.

    if [ ! -d ${radvd_dir} ]; then
	mkdir --parents ${radvd_dir}
	chown ${radvd_uid} ${radvd_dir}
    fi
    echo > ${radvd_cnf}

    if [ -z ${radvd_interfaces} ]; then
	radvd_interfaces=$(ip -oneline -4 addr show \
	    | awk '{print $2}' | sort | uniq | egrep -v '^lo$')
    fi
    if [ -z "${radvd_interfaces}" ]; then
	echo "error: no radvd interfaces"
	exit 1
    fi
    ${debug} && echo "${DESC}: radvd interfaces:" ${radvd_interfaces}
    inum=1
    for i in ${radvd_interfaces}; do
	pref=$(echo ${ip6net}${inum} | sed 's/::/:/g')
	cat >> ${radvd_cnf} <<EOF
interface ${i}
{
   AdvSendAdvert on;
   IgnoreIfMissing on;
   AdvDefaultLifetime 600;
   AdvDefaultPreference low;
   prefix ${pref}::/64
   {
   };
};
EOF
	# flush existing 6to4 subnets on target interface
	for a in $(ip -6 a list dev ${i} | egrep 'inet6.*2002:' \
	    | awk '{print $2}'); do
	    ${debug} && echo "${DESC}: flush old subnet ${a} dev ${i}"
	    ip -6 a del ${a} dev ${i}
	done

	# set up to route subnet on target interface
	${debug} && echo "${DESC}: route via ${pref}::1/64 dev ${i}"

	ip -6 addr add ${pref}::1/64 dev ${i}
	inum=$((inum + 1))
    done

    # Enable routing
    ${debug} && echo "${DESC}: enabling IPv6 routing"
    sysctl -q -w net.ipv6.conf.all.accept_ra=0
    sysctl -q -w net.ipv6.conf.all.forwarding=1

    kill_radvd

    # Start advertising
    ${debug} && echo "${DESC}: starting radvd"
    ${radvd} ${radvd_options}
}

stop()
{
    kill_radvd

    flush_tunnel_route

    # shut down tunnel
    ${debug} && echo "${DESC}: setting down tunnel ${tunnel}"
    ip link set dev ${tunnel} down

    delete_tunnel
}

flush_tunnel_route()
{
    # Flush tunnel from routing table if present
    if [ $(ip -oneline -6 route list dev ${tunnel} 2> /dev/null | wc -l) != 0 ]; then
	${debug} && echo "${DESC}: flushing routes involving tunnel ${tunnel}"
	ip -6 route flush dev ${tunnel}
	ip -6 addr flush dev ${tunnel}
    fi
}

kill_radvd()
{
    # shut down any existing ravdvd, using appropriate UID for security
    if [ -f ${radvd_pidfile} ]; then
	${debug} && echo "${DESC}: killing existing radvd"
	su -s /bin/sh -c "kill $(cat ${radvd_pidfile})" ${radvd_uid}
    fi
}

delete_tunnel()
{
    # Kill tunnel if possibly alive
    if [ $(ip -oneline -6 tunnel show ${tunnel} 2> /dev/null | wc -l) != 0 ]; then
	${debug} && echo "${DESC}: deleting tunnel ${tunnel}"
	ip tunnel del ${tunnel}
    fi
}


# Name of tunnel interface to configure
tunnel=tun6in4
ip4relay=192.88.99.1

# IPv6 advertisement on local network
radvd=/usr/sbin/radvd
radvd_dir=/var/run/radvd
radvd_cnf=${radvd_dir}/radvd-auto.conf
radvd_pidfile=${radvd_dir}/radvd-auto.pid
radvd_uid=radvd
radvd_options="--config ${radvd_cnf} -u ${radvd_uid} -p ${radvd_pidfile}"
radvd_interfaces=""

# misc defaults
debug=false

# process options

if [ "$*" = "--help" ]; then
    usage
    exit 0
fi

if [ "$*" = "--version" ]; then
    version
    exit 0
fi

while getopts "hvi:rI:d" opt
do
    case "${opt}" in
	h) usage; exit 0 ;;
	v) version; exit 0 ;;
	i) ip4addr=${OPTARG} ;;
	I) radvd_interfaces="${radvd_interfaces} ${OPTARG}" ;;
	r) unset radvd ;;
	d) debug=true; echo "${DESC}: debug: ${debug}" ;;
	*) usage; exit 2 ;;
    esac
done

shift $((OPTIND - 1))

if [ $# != 1 ]; then
    version; echo; usage
    exit 1
fi


${debug} && echo "${DESC}: action: $1"

case "$1" in
    start)
	start;
	;;
    stop)
	stop;
	;;
    restart|reload|force-reload)
	stop || true
	start
	;;
    *)
	usage
	exit 1
	;;
esac

exit 0
