diff options
author | Austin Adams <git@austinjadams.com> | 2015-11-17 20:03:02 -0500 |
---|---|---|
committer | Austin Adams <git@austinjadams.com> | 2015-12-15 22:57:34 -0500 |
commit | 6b8dcbcade2878f88722e1e9a50052bc473a5ce9 (patch) | |
tree | ac74935965d52642949075dab91cb8a93be49d44 | |
download | execd-master.tar.gz execd-master.tar.xz |
-rw-r--r-- | .gitignore | 5 | ||||
-rw-r--r-- | LICENSE | 22 | ||||
-rw-r--r-- | body.go | 136 | ||||
-rw-r--r-- | client.go | 97 | ||||
-rw-r--r-- | execc/main.go | 53 | ||||
-rw-r--r-- | execd/figlet/README | 115 | ||||
-rw-r--r-- | execd/figlet/execd.service | 10 | ||||
-rwxr-xr-x | execd/figlet/fig | 43 | ||||
-rwxr-xr-x | execd/figlet/prepchroot | 56 | ||||
-rw-r--r-- | execd/main.go | 91 |
10 files changed, 628 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..75713e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.*.sw? + +# ignore binaries +/execd/execd +/execc/execc @@ -0,0 +1,22 @@ +The MIT/X11 License + +Copyright (c) 2015 Austin Adams + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. @@ -0,0 +1,136 @@ +// Copyright (c) 2015 Austin Adams +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. + +package execd + +import ( + "bufio" + "bytes" + "io" + "strconv" +) + +type bodyReader struct { + read int64 + length int64 + reader *bufio.Reader +} + +func NewBodyReader(in io.Reader) *bodyReader { + return &bodyReader{0, -1, bufio.NewReader(in)} +} + +func (br *bodyReader) Read(p []byte) (n int, err error) { + if br.length == -1 { + var line string + line, err = br.reader.ReadString('\n') + + if err != nil { + return + } + + // kill newline + line = line[:len(line)-1] + + br.length, err = strconv.ParseInt(line, 10, 64) + + if err != nil { + return + } + } + + if br.read < br.length { + n, err = br.reader.Read(p) + br.read += int64(n) + } + + if err == nil && br.read >= br.length { + err = io.EOF + } + + return +} + +type argBodyReader struct { + *bodyReader +} + +func NewArgBodyReader(in io.Reader) *argBodyReader { + return &argBodyReader{NewBodyReader(in)} +} + +func (abr *argBodyReader) Args() (args []string, err error) { + // read arguments + for { + var line string + line, err = abr.bodyReader.reader.ReadString('\n') + + if err != nil { + args = nil + break + } + + // remove trailing \n + line = line[:len(line)-1] + + if line == "" { + break + } else { + args = append(args, line) + } + } + return +} + +type bodyWriter struct { + *bytes.Buffer + out io.Writer +} + +func NewBodyWriter(out io.Writer) *bodyWriter { + return &bodyWriter{&bytes.Buffer{}, out} +} + +func (bw *bodyWriter) Flush() (err error) { + _, err = bw.out.Write([]byte(strconv.Itoa(bw.Buffer.Len()) + "\n")) + + if err == nil { + _, err = bw.Buffer.WriteTo(bw.out) + } + + return +} + +type argBodyWriter struct { + *bodyWriter +} + +func NewArgBodyWriter(out io.Writer) *argBodyWriter { + return &argBodyWriter{NewBodyWriter(out)} +} + +func (abw *argBodyWriter) WriteArgs(args []string) (err error) { + for i := 0; i <= len(args); i++ { + var line string + + if i < len(args) { + line = args[i] + } + + _, err = abw.bodyWriter.out.Write([]byte(line + "\n")) + if err != nil { + break + } + } + return +} diff --git a/client.go b/client.go new file mode 100644 index 0000000..65de409 --- /dev/null +++ b/client.go @@ -0,0 +1,97 @@ +// Copyright (c) 2015 Austin Adams +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. + +package execd + +import ( + "bytes" + "io" + "io/ioutil" + "net" +) + +type Client struct { + net.Conn +} + +func NewClient(conn net.Conn) *Client { + return &Client{conn} +} + +func DialClient(network, addr string) (*Client, error) { + conn, err := net.Dial(network, addr) + + if err != nil { + return nil, err + } + + return NewClient(conn), nil +} + +func (c *Client) Exec(in io.Reader, out io.Writer, args ...string) error { + bodyWriter := NewArgBodyWriter(c.Conn) + bodyReader := NewBodyReader(c.Conn) + + if err := bodyWriter.WriteArgs(args); err != nil { + return err + } + + if _, err := io.Copy(bodyWriter, in); err != nil { + return err + } + + // now that we know the size of the data to send, send the length + // and then the data + if err := bodyWriter.Flush(); err != nil { + return err + } + + if _, err := io.Copy(out, bodyReader); err != nil { + return err + } + + return nil +} + +func (c *Client) ExecString(input string, args ...string) (output string, err error) { + in := bytes.NewBufferString(input) + out := bytes.NewBuffer(nil) + + err = c.Exec(in, out, args...) + + if err == nil { + output = out.String() + } + + return +} + +type devNull struct{} + +func (dn devNull) Read(_ []byte) (n int, err error) { + return 0, io.EOF +} + +func (dn devNull) Write(p []byte) (n int, err error) { + return len(p), nil +} + +func (dn devNull) ReadFrom(in io.Reader) (n int64, err error) { + // ioutil has a nice implementation of this, so let's not reinvent the wheel + // XXX this type assertion is a hack, but it's safe with the current + // implementation of the go stdlib + return ioutil.Discard.(io.ReaderFrom).ReadFrom(in) +} + +// useful for black-holing input or output from Exec() +var DevNull devNull diff --git a/execc/main.go b/execc/main.go new file mode 100644 index 0000000..cf3df17 --- /dev/null +++ b/execc/main.go @@ -0,0 +1,53 @@ +// Copyright (c) 2015 Austin Adams +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. + +package main + +import ( + "log" + "net" + "os" + + "code.austinjadams.com/execd" +) + +const ( + argProgram = iota + argWhere + argProg + argCount +) + +func main() { + log.SetFlags(0) + + if len(os.Args) < argCount { + log.Fatalln("usage:", os.Args[0], "<where> <prog> [args...]") + } + + conn, err := net.Dial("tcp", os.Args[argWhere]) + + if err != nil { + log.Fatalln(err) + } + + client := execd.NewClient(conn) + + args := os.Args[argProg:] + + if err = client.Exec(os.Stdin, os.Stdout, args...); err != nil { + log.Fatalln(err) + } + + client.Close() +} diff --git a/execd/figlet/README b/execd/figlet/README new file mode 100644 index 0000000..d2b8444 --- /dev/null +++ b/execd/figlet/README @@ -0,0 +1,115 @@ +building a chroot for execd +--------------------------- + +as a (probably pointless and inadequate) security measure, i run execd +in a systemd-nspawn container. in case it's ever compromised, this +container has no internet access and tight resource limits. + +here's how: + +1. use debootstrap to create a debian chroot, like: + + # debootstrap --include=figlet,vim-tiny,iproute2,dbus --variant=minbase jessie /var/lib/container/execd + + packages i've `--include`d: + + * dbus: without a system bus running, the host systemd can't do handy + things to the container systemd. for example, if you leave + this out, you can't run `machinectl login execd`, which is + great for debugging, or `systemctl -M execd status`. + * vim-tiny (optional): having an implementation of vi is nice if you + choose to log in + * iproute2 (optional): again, if you choose to log in, having the + ability to run `ip` etc. is nice, but not + necesary + + debootstrap doesn't require a debian host, but you may have to tweak + that command line a bit if you're on another distro. On distributions + with more recent versions of systemd, for example, /var/lib/machines + seems to be the place to put nspawn containers, not + /var/lib/containers as on jessie. + +2. build execd: + + $ go get code.austinjadams.com/execd + $ cd $GOPATH/src/code.austinjadams.com/execd/execd + $ go build + +3. set up the chroot for execd: + + $ cd figlet + # ./prepchroot /var/lib/container/execd + +4. on the host, tell nspawn to use veth: + + # mkdir /etc/systemd/system/systemd-nspawn@execd.service.d/ + # cat >/etc/systemd/system/systemd-nspawn@execd.service.d/veth-and-resource-controls.conf <<EOF + [Service] + ExecStart= + $(grep ExecStart /lib/systemd/system/systemd-nspawn@.service) --network-veth + CPUQuota=10% + MemoryLimit=32M + EOF + + in more recent versions of systemd, you should use .nspawn files + instead. see systemd.nspawn(5). + +5. still on the host, start and enable networkd: + + # systemctl start systemd-networkd + # systemctl enable systemd-networkd + + networkd is nice because it sets up the container's networking + automatically. however, using other networking management tools (like + NetworkManager) seems to confuse the poor fella. indeed, networkd + seems to add a default route via the container's veth device, which + we'll have to remove: + + $ ip route + default dev ve-execd scope link metric 99 <-- huh? + # ip route del default dev ve-execd <-- bye! + + unfortunately, i still don't understand the conditions under which + networkd adds this default route. on some of my jessie systems, it + does, and on others, it doesn't. + +5. start it + + # systemctl start systemd-nspawn@execd + + or, with a more recent version of systemd (i.e., not jessie): + + # machinectl start execd + +6. add its ip address to /etc/hosts + + Newer versions of systemd offer nss-mymachines(8), which resolves + container names to their leased ip addresses, but unfortunately, the + version in jessie (215) doesn't, so we'll have to add the container + name to `/etc/hosts` manually. + + # journalctl -M execd -u systemd-networkd | grep address | tail -1 + Dec 08 16:49:41 execd systemd-networkd[27]: host0 : IPv4 link-local address 169.254.146.86 + # printf '169.254.146.86\texecd\n' >>/etc/hosts + $ ping execd + PING execd (169.254.146.86) 56(84) bytes of data. + 64 bytes from execd (169.254.146.86): icmp_seq=1 ttl=64 time=0.104 ms + 64 bytes from execd (169.254.146.86): icmp_seq=2 ttl=64 time=0.091 ms + 64 bytes from execd (169.254.146.86): icmp_seq=3 ttl=64 time=0.091 ms + 64 bytes from execd (169.254.146.86): icmp_seq=4 ttl=64 time=0.094 ms + 64 bytes from execd (169.254.146.86): icmp_seq=5 ttl=64 time=0.095 ms + ^C + --- execd ping statistics --- + 5 packets transmitted, 5 received, 0% packet loss, time 3996ms + rtt min/avg/max/mdev = 0.091/0.095/0.104/0.004 ms + +7. test it: + + $ execc execd:4000 figlet <<<"hello, world!" + _ _ _ _ _ _ + | |__ ___| | | ___ __ _____ _ __| | __| | | + | '_ \ / _ \ | |/ _ \ \ \ /\ / / _ \| '__| |/ _` | | + | | | | __/ | | (_) | \ V V / (_) | | | | (_| |_| + |_| |_|\___|_|_|\___( ) \_/\_/ \___/|_| |_|\__,_(_) + |/ + not bad! diff --git a/execd/figlet/execd.service b/execd/figlet/execd.service new file mode 100644 index 0000000..19271e3 --- /dev/null +++ b/execd/figlet/execd.service @@ -0,0 +1,10 @@ +[Unit] +Description=execd + +[Service] +Type=simple +User=nobody +ExecStart=/usr/local/bin/execd -listen :4000 + +[Install] +WantedBy=multi-user.target diff --git a/execd/figlet/fig b/execd/figlet/fig new file mode 100755 index 0000000..acbd16f --- /dev/null +++ b/execd/figlet/fig @@ -0,0 +1,43 @@ +#!/bin/sh +# provide an abstraction for figlet +# in theory, this script (and the interface it provides) works the same +# on a testing machine and in the container + +if [ $# -lt 1 ] || [ $# -gt 2 ] || + { [ "$1" != ls ] && [ "$1" != default ] && [ -z "$2" ]; }; then + echo "usage: $0 <dir> <font> -> exec figlet on font 'font' in subdir 'dir', reading text from stdin" + echo " $0 ls [dir] -> list subdirs or with a subdir as an argument, the fonts in that subdir" + echo " $0 default -> print the default font" + exit 1 +fi >&2 + +# where the prepchroot script puts the fonts +dir="/usr/local/share/figlet" +# where my distro puts the fonts +altdir="/usr/share/figlet" + +if [ ! -d "$dir" ]; then + if [ ! -d "$altdir" ]; then + echo "couldn't find the font directory. tried $dir and $altdir." >&2 + exit 2 + else + dir="$altdir" + fi +fi + +if [ "$1" = ls ]; then + if [ -z "$2" ]; then + for d in $dir/*; do + echo "$(basename "$d")" + done + else + for font in $dir/"$2"/*.flf; do + base="$(basename "$font")" + echo "${base%.flf}" + done + fi +elif [ "$1" = default ]; then + exec figlet -I3 +else + exec figlet -d "$dir/$1" -f "$2" +fi diff --git a/execd/figlet/prepchroot b/execd/figlet/prepchroot new file mode 100755 index 0000000..f6a694a --- /dev/null +++ b/execd/figlet/prepchroot @@ -0,0 +1,56 @@ +#!/bin/bash +set -e +set -o pipefail + +# see http://www.figlet.org/ +officialurl=ftp://ftp.figlet.org/pub/figlet/fonts/ours.tar.gz +contriburl=ftp://ftp.figlet.org/pub/figlet/fonts/contributed.tar.gz + +[ -z "$1" ] && { + echo "usage: $0 <dir>" >&2 + exit 1 +} + +dir="$1" + +[ ! -d "$dir" ] && { + echo "$dir isn't a directory." >&2 + exit 2 +} + +# XXX this is a frustrating limitation +[ `id -u` -ne 0 ] && { + echo "you must run this program as root :(" >&2 + exit 3 +} + +fontdir="$dir/usr/local/share/figlet" +mkdir -p "$fontdir"/{official,contrib} +# XXX ugly hack and big security issue +curl "$officialurl" | tar xzC "$fontdir"/official --strip-components=1 +curl "$contriburl" | tar xzC "$fontdir"/contrib --strip-components=1 +# XXX this is a scary and unnecessary command. +# instead of doing this, i should try to understand why cursive.ttf +# is a symlink and why there are subdirs. but for now, there are so +# many fonts even without the ones in the subdirectories that i +# just can't bring myself to care +find "$fontdir" -mindepth 2 -maxdepth 2 ! -type f -exec rm -rvf {} \; + +install -m755 fig "$dir"/usr/local/bin/fig +install -m755 ../execd "$dir"/usr/local/bin/execd +install -m644 execd.service "$dir"/etc/systemd/system/execd.service +ln -sv /etc/systemd/system/execd.service "$dir"/etc/systemd/system/multi-user.target.wants/ +ln -sv /lib/systemd/system/systemd-networkd.service "$dir"/etc/systemd/system/multi-user.target.wants/ +echo execd >"$dir"/etc/hostname + +{ + printf '\n# make `machinectl login` work\n' + for i in {0..8}; do + echo pts/$i + done +} >> "$dir"/etc/securetty + +echo "killing root password in chroot..." +chroot "$dir" passwd -d root + +echo done diff --git a/execd/main.go b/execd/main.go new file mode 100644 index 0000000..ef40bf9 --- /dev/null +++ b/execd/main.go @@ -0,0 +1,91 @@ +// Copyright (c) 2015 Austin Adams +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. + +package main + +import ( + "flag" + "io" + "log" + "net" + "os/exec" + + "code.austinjadams.com/execd" +) + +func handle(c net.Conn) { + // if we bail out, close the connection + defer c.Close() + + for { + bodyWriter := execd.NewBodyWriter(c) + bodyReader := execd.NewArgBodyReader(c) + + args, err := bodyReader.Args() + + // we're done + if err == io.EOF { + break + } else if err != nil { + log.Println(err) + break + } + + cmd := exec.Command(args[0], args[1:]...) + cmd.Stdin = bodyReader + cmd.Stdout = bodyWriter + + log.Println("running command", args, "...") + err = cmd.Run() + log.Println("done") + + if err != nil { + log.Println(err) + break + } + + // write command output into c + err = bodyWriter.Flush() + + if err != nil { + log.Println(err) + break + } + } +} + +func main() { + listen := flag.String("listen", "127.0.0.1:4000", "where to listen") + timestamps := flag.Bool("timedlog", false, "show timestamps in logging") + flag.Parse() + + if !*timestamps { + log.SetFlags(0) + } + + sock, err := net.Listen("tcp", *listen) + + if err != nil { + log.Fatalln(err) + } + + for { + conn, err := sock.Accept() + + if err != nil { + log.Fatalln(err) + } + + go handle(conn) + } +} |