#!/bin/sh set -e SCRIPT_VERSION="(unreleased version)" usage() { echo "Usage:" echo "weave setup" echo "weave launch [-password ] ..." echo "weave launch-dns " echo "weave connect " echo "weave run [--with-dns] ..." echo "weave start " echo "weave attach " echo "weave detach " echo "weave expose " echo "weave hide " echo "weave ps" echo "weave status" echo "weave version" echo "weave stop" echo "weave stop-dns" echo "weave reset" echo echo "where is of the form [:], and" echo " is of the form /" exit 1 } [ $(id -u) = 0 ] || { echo "weave must be run as 'root'" >&2 exit 1 } [ $# -gt 0 ] || usage if [ "$SCRIPT_VERSION" = "(unreleased version)" ] ; then IMAGE_VERSION=latest else IMAGE_VERSION=$SCRIPT_VERSION fi IMAGE_VERSION=${VERSION:-$IMAGE_VERSION} BASE_IMAGE=zettio/weave BASE_DNS_IMAGE=zettio/weavedns BASE_TOOLS_IMAGE=zettio/weavetools IMAGE=$BASE_IMAGE:$IMAGE_VERSION DNS_IMAGE=$BASE_DNS_IMAGE:$IMAGE_VERSION TOOLS_IMAGE=$BASE_TOOLS_IMAGE:$IMAGE_VERSION CONTAINER_NAME=weave DNS_CONTAINER_NAME=weavedns BRIDGE=weave CONTAINER_IFNAME=ethwe MTU=65535 PORT=6783 HTTP_PORT=6784 DNS_HTTP_PORT=6785 DOCKER_BRIDGE=${DOCKER_BRIDGE:-docker0} PROCFS=${PROCFS:-/proc} COMMAND=$1 shift 1 # utility function to check whether a command can be executed by the shell # see http://stackoverflow.com/questions/592620/how-to-check-if-a-program-exists-from-a-bash-script command_exists () { command -v $1 >/dev/null 2>&1 } run_tool() { TOOL_NET=$1 TOOL_COMMAND=$2 shift 2 docker run --rm --privileged --net=$TOOL_NET $TOOLS_IMAGE /bin/$TOOL_COMMAND "$@" } is_cidr() { # The regexp here is far from precise, but good enough. echo "$1" | grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$' >/dev/null } validate_cidr() { if ! is_cidr "$1" ; then echo "Invalid CIDR: $1" >&2 echo "CIDR must of be of form /, e.g. 10.0.1.1/24" >&2 exit 1 fi } run_iptables() { # -w is recent addition to iptables if [ -z "$CHECKED_IPTABLES_W" ] ; then if iptables -S -w >/dev/null 2>&1 ; then IPTABLES_W=-w fi CHECKED_IPTABLES_W=1 fi iptables $IPTABLES_W "$@" } # Add a rule to iptables, if it doesn't exist already add_iptables_rule() { IPTABLES_TABLE="$1" shift 1 if ! run_iptables -t $IPTABLES_TABLE -C "$@" >/dev/null 2>&1 then run_iptables -t $IPTABLES_TABLE -A "$@" >/dev/null fi } # Delete a rule from iptables, if it exist delete_iptables_rule() { IPTABLES_TABLE="$1" shift 1 if run_iptables -t $IPTABLES_TABLE -C "$@" >/dev/null 2>&1 then run_iptables -t $IPTABLES_TABLE -D "$@" >/dev/null fi } create_bridge() { [ ! -d /sys/class/net/$BRIDGE ] && { ip link add name $BRIDGE type bridge ip link set dev $BRIDGE address 7a:$(od -txC -An -N5 /dev/urandom | tr \ : | tail -c+2) # Attempting to set the bridge MTU to a high value directly # fails. Bridges take the lowest MTU of their interfaces. So # instead we create a temporary interface with the desired # MTU, attach that to the bridge, and then remove it again. ip link add name v${CONTAINER_IFNAME}du mtu $MTU type dummy ip link set dev v${CONTAINER_IFNAME}du master $BRIDGE ip link del dev v${CONTAINER_IFNAME}du # Work around the situation where there are no rules allowing traffic # across our bridge. E.g. ufw add_iptables_rule filter FORWARD -i $BRIDGE -o $BRIDGE -j ACCEPT # create a chain for masquerading run_iptables -t nat -N WEAVE >/dev/null 2>&1 || true add_iptables_rule nat POSTROUTING -j WEAVE } # disable offloading - we do this even when $BRIDGE already exists # simply because it has the desirable side-effect of ensuring we # have the $TOOLS_IMAGE and thus do not incur downloading-induced # delays in more time-sensitive sections elsewhere. # run_tool host ethtool -K $BRIDGE tx off >/dev/null ethtool -K $BRIDGE tx off >/dev/null ip link set dev $BRIDGE up } destroy_bridge() { if [ -d /sys/class/net/$BRIDGE ] ; then ip link del dev $BRIDGE fi run_iptables -t filter -D FORWARD -i $BRIDGE -o $BRIDGE -j ACCEPT 2>/dev/null || true run_iptables -t nat -F WEAVE >/dev/null 2>&1 || true run_iptables -t nat -D POSTROUTING -j WEAVE >/dev/null 2>&1 || true run_iptables -t nat -X WEAVE >/dev/null 2>&1 || true } docker_bridge_ip() { DOCKER_BRIDGE_IP=$(ip -4 addr show dev $DOCKER_BRIDGE | grep -m1 -o 'inet [.0-9]*') DOCKER_BRIDGE_IP=${DOCKER_BRIDGE_IP#inet } } # the following borrows from https://github.com/jpetazzo/pipework # Set $NETNS to the network namespace of container $1, $LOCAL_IFNAME # and $GUEST_IFNAME to suitable names for two ends of a veth pair, # specific to the container, and execute args $2 $3 ... as a command. # If an error is caused by container dying, swallow output from error. with_container_netns () { CONTAINER="$1" CONTAINER_PID=$(docker inspect --format='{{.State.Pid}}' $CONTAINER) if [ "$CONTAINER_PID" = 0 ] ; then echo "Container $CONTAINER not running." >&2 exit 1 fi if [ "$CONTAINER_PID" = "" ] ; then echo "Container $CONTAINER unknown to Docker." >&2 exit 1 fi NETNS=$CONTAINER_PID [ ! -d /var/run/netns ] && mkdir -p /var/run/netns rm -f /var/run/netns/$NETNS ln -s $PROCFS/$CONTAINER_PID/ns/net /var/run/netns/$NETNS LOCAL_IFNAME="v${CONTAINER_IFNAME}pl${CONTAINER_PID}" GUEST_IFNAME="v${CONTAINER_IFNAME}pg${CONTAINER_PID}" IP_TMPOUT=/tmp/weave_ip_out_$$ IP_TMPERR=/tmp/weave_ip_err_$$ rm -f $IP_TMPOUT $IP_TMPERR # Run the wrapped command STATUS=0 shift 1 if ! "$@" >$IP_TMPOUT 2>$IP_TMPERR ; then STATUS=1 if [ "$(docker inspect --format='{{.State.Pid}}' $CONTAINER)" != "$CONTAINER_PID" ] ; then echo "Container $CONTAINER died" >&2 else echo "Failure during network configuration for container $CONTAINER:" >&2 cat $IP_TMPERR >&2 fi else cat $IP_TMPOUT cat $IP_TMPERR >&2 fi rm -f $IP_TMPOUT $IP_TMPERR /var/run/netns/$NETNS return $STATUS } connect_container_to_bridge() { if [ "$(readlink $PROCFS/$CONTAINER_PID/ns/net)" = "$(readlink $PROCFS/$$/ns/net)" ] ; then echo "Container is running in the host network namespace, and therefore cannot be" >&2 echo "connected to weave. Perhaps the container was started with --net=host." >&2 return 1 fi if ! ip link add name $LOCAL_IFNAME mtu $MTU type veth peer name $GUEST_IFNAME mtu $MTU ; then return 1 fi if run_tool host ethtool -K $GUEST_IFNAME tx off >/dev/null && ip link set $LOCAL_IFNAME master $BRIDGE && ip link set $LOCAL_IFNAME up && ip link set $GUEST_IFNAME netns $NETNS ; then ip netns exec $NETNS ip link set $GUEST_IFNAME name $CONTAINER_IFNAME else # failed before we assigned the veth to the container's # namespace - delete it ip link del $LOCAL_IFNAME type veth || true return 1 fi } launch() { if ! ip netns exec $NETNS ip link show eth0 >/dev/null ; then echo "Perhaps you are running the docker daemon with container networking disabled (-b=none)." >&2 return 1 fi connect_container_to_bridge && run_tool container:$CONTAINER ethtool -K eth0 tx off >/dev/null && ip netns exec $NETNS ip link set $CONTAINER_IFNAME up } attach() { if ip netns exec $NETNS ip link show $CONTAINER_IFNAME >/dev/null 2>&1 ; then # container already has the expected network interface, so assume we set it up already; # just add the IP address. if ip netns exec $NETNS ip addr show dev $CONTAINER_IFNAME | grep -F $1 >/dev/null ; then # address was there already return 0 fi if ! ip netns exec $NETNS ip addr add $1 dev $CONTAINER_IFNAME ; then return 1 fi return 0 fi if ! connect_container_to_bridge || ! ip netns exec $NETNS ip addr add $1 dev $CONTAINER_IFNAME || ! ip netns exec $NETNS ip link set $CONTAINER_IFNAME up ; then return 1 fi # Route multicast packets across the weave network. if ! ip netns exec $NETNS ip route show | grep '^224\.0\.0\.0/4' >/dev/null ; then ip netns exec $NETNS ip route add 224.0.0.0/4 dev $CONTAINER_IFNAME fi } detach() { if ! ip netns exec $NETNS ip addr show dev $CONTAINER_IFNAME | grep -F $1 >/dev/null ; then # address is not there, leave the device alone return 0 fi if ! ip netns exec $NETNS ip addr del $1 dev $CONTAINER_IFNAME ; then return 1 fi if [ -n "$(ip netns exec $NETNS ip -f inet addr show dev $CONTAINER_IFNAME)" ] ; then # other addresses are left, leave the device alone return 0 fi # Deleting the interface will delete the multicast route we set up ip netns exec $NETNS ip link del $CONTAINER_IFNAME type veth } # Call url $4 with http verb $3 on container $1 at port $2 http_call() { if ! status=$(docker inspect --format='{{.State.Running}} {{.NetworkSettings.IPAddress}}' $1 2>/dev/null); then echo "$1 container is not present; have you launched it?" >&2 return 1 fi case "$status" in "true ") echo "$1 container has no IP address; is Docker networking enabled?" >&2 return 1 ;; true*) ip="${status#true }" ;; *) echo "$1 container is not running." >&2 return 1 ;; esac shift 1 http_call_ip $ip "$@" } http_call_ip() { ip="$1" port="$2" http_verb="$3" url="$4" shift 4 run_tool host curl -s -X $http_verb "$@" http://$ip:$port$url } # Perform operation $1 on container ID $2 to local DNS database at address $3 # This function is only called where we know $2 is a valid container name tell_dns() { if ! status=$(docker inspect --format='{{.State.Running}}' $DNS_CONTAINER_NAME 2>/dev/null) || [ "$status" != "true" ] ; then # weavedns not running - silently return return fi # get the long form of the container ID CONTAINER=$(docker inspect --format='{{.Id}}' $2 2>/dev/null) # extract IP address and routing prefix from CIDR WEAVE_ADDR_IP=$(echo $3 | sed -e 's/\([^/]*\)\/\(.*\)/\1/') MORE_ARGS=$(docker inspect --format='--data-urlencode fqdn={{.Config.Hostname}}.{{.Config.Domainname}}.' $CONTAINER 2>/dev/null) && true http_call $DNS_CONTAINER_NAME $DNS_HTTP_PORT $1 /name/$CONTAINER/$WEAVE_ADDR_IP $MORE_ARGS || true } # Tell the newly-started weaveDNS about existing weave IPs populate_dns() { DNS_IP=$(docker inspect --format='{{.NetworkSettings.IPAddress}}' $DNS_CONTAINER_NAME) WAIT_TIME=1 while ! http_call_ip $DNS_IP $DNS_HTTP_PORT GET /status >/dev/null && [ $WAIT_TIME -lt 4 ]; do sleep $WAIT_TIME WAIT_TIME=$((WAIT_TIME+1)) done if [ $WAIT_TIME = 4 ]; then echo "Timed out waiting for weaveDNS container to start." >&2 echo "If running, it will not be pre-populated." >&2 fi for CONTAINER in $(docker ps -q --no-trunc); do MORE_ARGS=$(docker inspect --format='--data-urlencode fqdn={{.Config.Hostname}}.{{.Config.Domainname}}.' $CONTAINER 2>/dev/null) && true if CONTAINER_ADDRS=$(with_container_netns $CONTAINER container_weave_addrs 2>&1) ; then CONTAINER_IPS=$(echo "$CONTAINER_ADDRS" | grep -o 'inet .*' | sed -e 's/inet \([^/]*\)\/\(.*\)/\1/') for IP in $CONTAINER_IPS; do http_call_ip $DNS_IP $DNS_HTTP_PORT PUT /name/$CONTAINER/$IP $MORE_ARGS done fi done } # Check that a container named $1 with image $2 is not running check_not_running() { case $(docker inspect --format='{{.State.Running}} {{.Config.Image}}' $1 2>/dev/null) in "true $2") echo "$1 is already running." >&2 exit 1 ;; "true $2:"*) echo "$1 is already running." >&2 exit 1 ;; "false $2") docker rm $1 >/dev/null ;; "false $2:"*) docker rm $1 >/dev/null ;; true*) echo "Found another running container named '$1'. Aborting." >&2 exit 1 ;; false*) echo "Found another container named '$1'. Aborting." >&2 exit 1 ;; esac } container_weave_addrs() { ip netns exec $NETNS ip addr show dev $CONTAINER_IFNAME } uname -s -r | sed -n 's|^\([^ ]*\) \([0-9]\+\)\.\([0-9]\+\).*|\1 \2 \3|p' | { if ! read sys maj min ; then echo "ERROR: Unable to parse operating system version $(uname -s -r)" >&2 exit 1 fi if [ "$sys" != 'Linux' ] ; then echo "ERROR: Operating systems other than Linux are not supported (you have $(uname -s -r))" >&2 exit 1 fi if ! [ \( "$maj" -eq 3 -a "$min" -ge 5 \) -o "$maj" -gt 3 ] ; then echo "WARNING: Linux kernel version 3.5 or newer is required (you have ${maj}.${min})" >&2 fi } ask_version() { if ! DOCKERIMAGE=$(docker inspect --format='{{.Image}}' $1 2>/dev/null) ; then if ! DOCKERIMAGE=$(docker inspect --format='{{.Id}}' $2 2>/dev/null) ; then echo "Unable to find $2 image." >&2 fi fi [ -n "$DOCKERIMAGE" ] && docker run --rm $DOCKERIMAGE --version } if ! command_exists ip ; then echo "ERROR: ip utility is missing. Please install it." >&2 exit 1 fi if ! ip netns list >/dev/null 2>&1 ; then echo "ERROR: $(ip -V) does not support network namespaces." >&2 echo " Please install iproute2-ss111010 or later." >&2 exit 1 fi if ! DOCKER_VERSION=$(docker -v | sed -n 's|^Docker version \([0-9]\+\.[0-9]\+.[0-9]\+\).*|\1|p') || [ -z "$DOCKER_VERSION" ] ; then echo "ERROR: Unable to parse docker version" >&2 exit 1 fi # guard against https://github.com/docker/docker/issues/8632 if [ "$DOCKER_VERSION" = "1.3.0" ] ; then echo "You are running Docker version $DOCKER_VERSION, which contains a bug that prevents weave from working properly. Please upgrade." >&2 exit 1 fi DOCKER_VERSION_MAJOR=$(echo "$DOCKER_VERSION" | cut -d. -f 1) DOCKER_VERSION_MINOR=$(echo "$DOCKER_VERSION" | cut -d. -f 2) DOCKER_VERSION_PATCH=$(echo "$DOCKER_VERSION" | cut -d. -f 3) case "$COMMAND" in setup) for img in $IMAGE $DNS_IMAGE $TOOLS_IMAGE ; do docker pull $img done ;; setup-bridge) create_bridge ;; launch) check_not_running $CONTAINER_NAME $BASE_IMAGE create_bridge # We set the router name to the bridge mac since that is # stable across re-creations of the containers. # # TODO use the mac of one of the physical host interfaces # (eth0, wlan0, etc) so the name becomes stable across host # restarts. MACADDR=$(cat /sys/class/net/$BRIDGE/address) # backward compatibility... if is_cidr "$1" ; then echo "WARNING: $1 parameter ignored; 'weave launch' no longer takes a CIDR as the first parameter" >&2 shift 1 fi if [ "$1" = "-password" ] ; then [ $# -gt 1 ] || usage WEAVE_PASSWORD="$2" export WEAVE_PASSWORD shift 2 fi # Set WEAVE_DOCKER_ARGS in the environment in order to supply # additional parameters, such as resource limits, to docker # when launching the weave container. CONTAINER=$(docker run --privileged -d --name=$CONTAINER_NAME \ -p $PORT:$PORT/tcp -p $PORT:$PORT/udp -e WEAVE_PASSWORD \ $WEAVE_DOCKER_ARGS $IMAGE -name $MACADDR -iface $CONTAINER_IFNAME "$@") with_container_netns $CONTAINER launch >/dev/null echo $CONTAINER ;; launch-dns) [ $# -gt 0 ] || usage CIDR=$1 shift 1 check_not_running $DNS_CONTAINER_NAME $BASE_DNS_IMAGE create_bridge docker_bridge_ip DNS_CONTAINER=$(docker run --privileged -d --name=$DNS_CONTAINER_NAME \ -p $DOCKER_BRIDGE_IP:53:53/udp -v /var/run/docker.sock:/var/run/docker.sock \ $DNS_IMAGE -iface $CONTAINER_IFNAME "$@") with_container_netns $DNS_CONTAINER attach $CIDR >/dev/null populate_dns echo $DNS_CONTAINER ;; connect) [ $# -eq 1 ] || usage http_call $CONTAINER_NAME $HTTP_PORT POST /connect -d "peer=$1" ;; status) http_call $CONTAINER_NAME $HTTP_PORT GET /status ;; ps) [ $# -eq 0 ] || usage for CONTAINER_ID in $(docker ps -q) ; do if CONTAINER_ADDRS=$(with_container_netns $CONTAINER_ID container_weave_addrs 2>&1) ; then CONTAINER_MAC=$(echo "$CONTAINER_ADDRS" | grep -o 'link/ether .*' | cut -d ' ' -f 2) CONTAINER_IPS=$(echo "$CONTAINER_ADDRS" | grep -o 'inet .*' | cut -d ' ' -f 2) echo $CONTAINER_ID $CONTAINER_MAC $CONTAINER_IPS fi done ;; version) [ $# -eq 0 ] || usage echo weave script $SCRIPT_VERSION ask_version $CONTAINER_NAME $IMAGE ask_version $DNS_CONTAINER_NAME $DNS_IMAGE if ! TOOLS_IMAGE_ID=$(docker inspect --format='{{.Id}}' $TOOLS_IMAGE 2>/dev/null) ; then echo "Unable to find $TOOLS_IMAGE image." >&2 else TOOLS_VERSION=$(docker images --no-trunc | grep $TOOLS_IMAGE_ID | grep -v latest | tr -s ' ' | cut -d ' ' -f 2 | tr "\n" ' ') if [ -n "$TOOLS_VERSION" ] ; then echo "weave tools $TOOLS_VERSION" else echo "weave tools (unreleased version)" fi fi ;; run) [ $# -gt 0 ] || usage if [ "$1" = "--with-dns" ] ; then shift 1 if [ \( "$DOCKER_VERSION_MAJOR" -lt 1 \) -o \ \( "$DOCKER_VERSION_MAJOR" -eq 1 -a \ "$DOCKER_VERSION_MINOR" -lt 2 \) ] ; then echo "ERROR: The '--with-dns' option requires Docker 1.2.0 or later; you are running $DOCKER_VERSION" >&2 exit 1 fi docker_bridge_ip DNS_ARG="--dns $DOCKER_BRIDGE_IP" DNS_SEARCH_ARG="--dns-search=." for arg in "$@"; do case $arg in --dns-search=*) DNS_SEARCH_ARG="" ;; *) ;; esac; done fi validate_cidr $1 CIDR=$1 shift 1 create_bridge CONTAINER=$(docker run $DNS_ARG $DNS_SEARCH_ARG -d "$@") with_container_netns $CONTAINER attach $CIDR >/dev/null tell_dns PUT $CONTAINER $CIDR echo $CONTAINER ;; start) [ $# -eq 2 ] || usage validate_cidr $1 create_bridge CONTAINER=$(docker start $2) with_container_netns $CONTAINER attach $1 >/dev/null tell_dns PUT $CONTAINER $1 echo $CONTAINER ;; attach) [ $# -eq 2 ] || usage validate_cidr $1 create_bridge with_container_netns $2 attach $1 >/dev/null tell_dns PUT $2 $1 ;; detach) [ $# -eq 2 ] || usage validate_cidr $1 with_container_netns $2 detach $1 >/dev/null tell_dns DELETE $2 $1 ;; expose) [ $# -eq 1 ] || usage validate_cidr $1 create_bridge if ! ip addr show dev $BRIDGE | grep -qF $1 then ip addr add dev $BRIDGE $1 add_iptables_rule nat WEAVE -o $BRIDGE ! -s $1 -j MASQUERADE add_iptables_rule nat WEAVE -s $1 ! -o $BRIDGE -j MASQUERADE fi ;; hide) [ $# -eq 1 ] || usage validate_cidr $1 create_bridge if ip addr show dev $BRIDGE | grep -qF $1 then ip addr del dev $BRIDGE $1 delete_iptables_rule nat WEAVE -o $BRIDGE ! -s $1 -j MASQUERADE delete_iptables_rule nat WEAVE -s $1 ! -o $BRIDGE -j MASQUERADE fi ;; stop) [ $# -eq 0 ] || usage if ! docker kill $CONTAINER_NAME >/dev/null 2>&1 ; then echo "Weave is not running." >&2 fi docker rm -f $CONTAINER_NAME >/dev/null 2>&1 || true run_tool host conntrack -D -p udp --dport $PORT >/dev/null 2>&1 || true ;; stop-dns) [ $# -eq 0 ] || usage if ! docker kill $DNS_CONTAINER_NAME >/dev/null 2>&1 ; then echo "WeaveDNS is not running." >&2 fi docker rm -f $DNS_CONTAINER_NAME >/dev/null 2>&1 || true ;; reset) [ $# -eq 0 ] || usage docker kill $CONTAINER_NAME >/dev/null 2>&1 || true docker kill $DNS_CONTAINER_NAME >/dev/null 2>&1 || true docker rm -f $CONTAINER_NAME >/dev/null 2>&1 || true docker rm -f $DNS_CONTAINER_NAME >/dev/null 2>&1 || true run_tool host conntrack -D -p udp --dport $PORT >/dev/null 2>&1 || true destroy_bridge for LOCAL_IFNAME in $(ip link show | grep v${CONTAINER_IFNAME}pl | cut -d ' ' -f 2 | tr -d ':') ; do ip link del $LOCAL_IFNAME done ;; *) echo "Unknown weave command '$COMMAND'" >&2 usage ;; esac