summaryrefslogtreecommitdiffgithub
diff options
context:
space:
mode:
authorAustin Adams <git@austinjadams.com>2015-11-17 20:03:02 -0500
committerAustin Adams <git@austinjadams.com>2015-12-15 22:57:34 -0500
commit6b8dcbcade2878f88722e1e9a50052bc473a5ce9 (patch)
treeac74935965d52642949075dab91cb8a93be49d44
downloadexecd-6b8dcbcade2878f88722e1e9a50052bc473a5ce9.tar.gz
execd-6b8dcbcade2878f88722e1e9a50052bc473a5ce9.tar.xz
initial commitHEADmaster
-rw-r--r--.gitignore5
-rw-r--r--LICENSE22
-rw-r--r--body.go136
-rw-r--r--client.go97
-rw-r--r--execc/main.go53
-rw-r--r--execd/figlet/README115
-rw-r--r--execd/figlet/execd.service10
-rwxr-xr-xexecd/figlet/fig43
-rwxr-xr-xexecd/figlet/prepchroot56
-rw-r--r--execd/main.go91
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
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..6aedb51
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/body.go b/body.go
new file mode 100644
index 0000000..57c9c81
--- /dev/null
+++ b/body.go
@@ -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)
+ }
+}