#!/bin/bash # -*- tab-width: 4; sh-basic-offset: 4 -*- # distcc-tool # Copyright 2013 Luke Shumaker # # This file is part of Parabola. # # Parabola 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 3 of the License, or # (at your option) any later version. # # Parabola 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. # # You should have received a copy of the GNU General Public License # along with Parabola. If not, see . # This program has very few dependencies: # - bash: I don't know what version, I use fairly modern features # - socat # - cat: any version # - grep: any version # - mkdir: any version # - rm: any version # - sed: any version # - sleep: any version # On Parabola, this means the packages: # bash, coreutils, grep, sed, socat panic() { echo 'panic: malformed call to internal function' >&2 exit 1 } error() { fmt=$1; shift printf "ERROR: $fmt\n" "$@" >&2 exit 1 } usage() { echo "Usage: $0 COMMAND [COMMAND-ARGS]" echo "Tool for using distcc within a networkless chroot" echo echo "Commands:" echo ' help print this message' echo ' odaemon CHROOTPATH daemon to run outside of the chroot' echo ' idaemon DISTCC_HOSTS daemon to run inside of the chroot' echo ' rewrite DISTCC_HOSTS prints a rewritten version of DISTCC_HOSTS' echo ' client HOST PORT connects stdio to TCP:$HOST:$PORT' echo 'Commands: for internal use' echo ' server counterpart to client; spawned by odaemon' } errusage() { if [[ $# -gt 0 ]]; then fmt=$1; shift printf "ERROR: $fmt\n" "$@" >&2 fi usage >&2 exit 1 } main() { local cmd=$1 shift case "$cmd" in help) [[ $# -eq 0 ]] || errusage '%s: invalid number of arguments' "$cmd" usage;; odaemon|idaemon|rewrite) [[ $# -eq 1 ]] || errusage '%s: invalid number of arguments' "$cmd" $cmd "$@";; client) [[ $# -eq 2 ]] || errusage '%s: invalid number of arguments' "$cmd" $cmd "$@";; server) [[ $# -eq 0 ]] || errusage '%s: invalid number of arguments' "$cmd" $cmd "$@";; *) errusage 'unknown subcommand: %s' "$cmd";; esac } ################################################################################ # DISTCC_HOSTS parser # ################################################################################ # usage: parse_DISTCC_HOSTS true|false DISTCC_HOSTS # parses DISTCC_HOSTS and: # $1==true : It sets up port forwarding for inside the choot, sleep forever # $1==false: Prints a modified version of DISTCC_HOSTS that uses the forwarded # ports that were set up when $1==true. parse_DISTCC_HOSTS() { { [[ $# -eq 2 ]] && { [[ $1 == true ]] || [[ $1 == false ]]; }; } || panic local forward_ports=$1 local DISTCC_HOSTS=$2 local pids=() # child pids local newhosts=() local newport=8000 # next port to be used for port forwarding # This is based on the grammar specified in distcc(1) local HOSTSPEC for HOSTSPEC in $(sed 's/#.*//' <<<"$DISTCC_HOSTS"); do case "$HOSTSPEC" in # LOCAL_HOST localhost|localhost/*|--localslots=*|--localslots_cpp=*) # "localhost" runs commands directly, not talking to distccd at # localhost, use an IP or real hostname for that. # So, just pass these through. newhosts+=("$HOSTSPEC") ;; # SSH_HOST *@*) # SSH_HOST doesn't allow custom port numbers, and even if it # did, ssh would complain about MITM. Instead, we configure an # ssh ProxyCommand so that ssh works fine. newhosts+=("$HOSTSPEC") ;; # GLOBAL_OPTION --*) # pass these through newhosts+=("$HOSTSPEC") ;; # ZEROCONF +zeroconf) error "$0 does not support the +zeroconf option" exit 1 ;; # TCP_HOST or OLDSTYLE_TCP_HOST *) declare HOSTID= PORT= LIMIT= OPTIONS= if [[ $HOSTSPEC =~ ^([^:/]+)(:([0-9]+))?(/([0-9]+))?(,.*)?$ ]]; then # TCP_HOST HOSTID=${BASH_REMATCH[1]} PORT=${BASH_REMATCH[3]} LIMIT=${BASH_REMATCH[5]} OPTIONS=${BASH_REMATCH[6]} elif [[ $HOSTSPEC =~ ^([^:/]+)(/([0-9]+))?(:([0-9]+))?(,.*)?$ ]]; then # OLDSTYLE_TCP_HOST HOSTID=${BASH_REMATCH[1]} LIMIT=${BASH_REMATCH[3]} PORT=${BASH_REMATCH[5]} OPTIONS=${BASH_REMATCH[6]} else error "Could not parse HOSTSPEC: %s" "$HOSTSPEC" fi # set up port forwaring if $forward_ports; then socat TCP-LISTEN:${newport},fork SYSTEM:"$0 client $HOSTID ${PORT:-3632}" & pids+=($!) fi # add the forwarded port local newhost="127.0.0.1:$newport" [[ -z $LIMIT ]] || newhost+="/$LIMIT" [[ -z $OPTIONS ]] || newhost+="$OPTIONS" newhosts+=("$newhost") : $((newport++)) ;; esac done if $forward_ports; then if [[ -z "${pids[*]}" ]]; then # listen on port 8000, but immediatly close, just so that we are # listening on something socat TCP-LISTEN:${newport},fork SYSTEM:true & pids+=($!) fi trap "kill -- ${pids[*]}" EXIT wait "${pids[@]}" else printf '%s\n' "${newhosts[*]}" fi } ################################################################################ # Port forwarding primitives # ################################################################################ # Usage: server # Reads "host port" from the first line of stdin, then connects stdio to the # specified TCP socket. server() { [[ $# -eq 0 ]] || panic while read -r host port; do socat STDIO TCP:"$host:$port" break done } # Usage: client HOST PORT # For usage inside of a chroot. It talks through the UNIX-domain socket to an # instance of `server` outside, in order to connect stdio to the specified TCP # socket. client() { [[ $# -eq 2 ]] || panic local file=/socket { printf '%s\n' "$*"; cat; } | socat UNIX-CONNECT:"$file" STDIO } ################################################################################ # High-level routines # ################################################################################ # Usage: configure_ssh # Edits ~/.ssh/config to use `client` as a ProxyCommand so that ssh can work # inside of the chroot. configure_ssh() { [[ $# -eq 0 ]] || panic if ! grep "^\s*ProxyCommand $0 client" ~/.ssh/config &>/dev/null; then [[ -d ~/.ssh ]] || mkdir ~/.ssh echo 'Host *' >> ~/.ssh/config echo " ProxyCommand $0 client %h %p" >> ~/.ssh/config fi } # Usage: idaemon DISTCC_HOSTS # Sets things up inside of the chroot to forward distcc hosts out. # It configures an ssh ProxyCommand. idaemon() { [[ $# -eq 1 ]] || panic local DISTCC_HOSTS=$1 configure_ssh parse_DISTCC_HOSTS true "$DISTCC_HOSTS" } # Usage: odaemon CHROOTPATH # Listens on "$CHROOTPATH/socket" and spawns a `server` for each new connection. odaemon() { [[ $# -eq 1 ]] || panic local chrootpath=$1 trap "rm -f '$chrootpath/socket'" EXIT socat UNIX-LISTEN:"$chrootpath/socket",fork SYSTEM:"$0 server" } # Usage: rewrite DISTCC_HOSTS # Prints a modified version of DISTCC_HOSTS for inside the chroot. rewrite() { [[ $# -eq 1 ]] || panic parse_DISTCC_HOSTS false "$1" } main "$@"