From 7b618637058ab5389a4f92d785eb84d7e636cc38 Mon Sep 17 00:00:00 2001 From: Justin Cormack Date: Wed, 9 Dec 2015 15:33:50 +0000 Subject: [PATCH] vendor pinata specific tools for now, until they are standalone packages Signed-off-by: Justin Cormack --- alpine/Dockerfile | 4 + alpine/Makefile | 4 + alpine/packages/9pudc/Dockerfile | 10 ++ alpine/packages/9pudc/Makefile | 8 + alpine/packages/9pudc/main.go | 140 ++++++++++++++++++ alpine/packages/README.md | 3 + alpine/packages/mdnstool/.gitignore | 1 + alpine/packages/mdnstool/Dockerfile | 10 ++ alpine/packages/mdnstool/Makefile | 8 + alpine/packages/mdnstool/README.md | 31 ++++ alpine/packages/mdnstool/mdnsmon/mdnsmon.go | 153 ++++++++++++++++++++ alpine/packages/mdnstool/mdnstool.go | 57 ++++++++ 12 files changed, 429 insertions(+) create mode 100644 alpine/packages/9pudc/Dockerfile create mode 100644 alpine/packages/9pudc/Makefile create mode 100644 alpine/packages/9pudc/main.go create mode 100644 alpine/packages/README.md create mode 100644 alpine/packages/mdnstool/.gitignore create mode 100644 alpine/packages/mdnstool/Dockerfile create mode 100644 alpine/packages/mdnstool/Makefile create mode 100644 alpine/packages/mdnstool/README.md create mode 100644 alpine/packages/mdnstool/mdnsmon/mdnsmon.go create mode 100644 alpine/packages/mdnstool/mdnstool.go diff --git a/alpine/Dockerfile b/alpine/Dockerfile index b8824726f..07b26450c 100644 --- a/alpine/Dockerfile +++ b/alpine/Dockerfile @@ -37,4 +37,8 @@ RUN \ rc-update add docker default && \ ln -s /bin/busybox /init +# docker mac specific, and should be moved anyway +RUN mkdir /Mac /Socket +COPY packages/9pudc/9pudc packages/mdnstool/mdnstool /sbin/ + CMD ["/bin/sh"] diff --git a/alpine/Makefile b/alpine/Makefile index da0bcabe0..26e22bebc 100644 --- a/alpine/Makefile +++ b/alpine/Makefile @@ -6,8 +6,12 @@ ETCFILES+=etc/init.d/chronyd initrd.img: Dockerfile mkinitrd.sh repositories $(ETCFILES) rm -f initrd.img + $(MAKE) -C packages/9pudc + $(MAKE) -C packages/mdnstool docker build -t moby:test . docker run -i moby:test /bin/mkinitrd.sh > $@ clean: rm -f initrd.img + $(MAKE) -C packages/9pudc clean + $(MAKE) -C packages/mdnstool clean diff --git a/alpine/packages/9pudc/Dockerfile b/alpine/packages/9pudc/Dockerfile new file mode 100644 index 000000000..7bdba0e3e --- /dev/null +++ b/alpine/packages/9pudc/Dockerfile @@ -0,0 +1,10 @@ +FROM golang:alpine + +RUN apk update && apk upgrade && apk add git + +RUN mkdir -p /go/src/9pudc +WORKDIR /go/src/9pudc + +COPY . /go/src/9pudc/ +RUN go get +RUN go install diff --git a/alpine/packages/9pudc/Makefile b/alpine/packages/9pudc/Makefile new file mode 100644 index 000000000..a4cf3f501 --- /dev/null +++ b/alpine/packages/9pudc/Makefile @@ -0,0 +1,8 @@ +all: 9pudc + +9pudc: Dockerfile main.go + docker build -t 9pudc:test . + docker run 9pudc:test cat /go/bin/9pudc > 9pudc + +clean: + rm -f 9pudc diff --git a/alpine/packages/9pudc/main.go b/alpine/packages/9pudc/main.go new file mode 100644 index 000000000..3a1e5a5c9 --- /dev/null +++ b/alpine/packages/9pudc/main.go @@ -0,0 +1,140 @@ +package main + +import ( + "flag" + "fmt" + "io" + "log" + "net" + "os" + "strconv" + "strings" + "syscall" + "time" +) + +var ( + path string + sock string + detach bool +) + +func init() { + flag.StringVar(&path, "path", "/9puds", "path of the 9P-mounted Unix domain socket tree") + flag.StringVar(&sock, "sock", "/tmp/forwarded.sock", "path of the local Unix domain socket to forward to") + flag.BoolVar(&detach, "detach", false, "detach from terminal") +} + +func main() { + log.SetFlags(0) + flag.Parse() + + if detach { + logFile, err := os.Create("/var/log/9pudc.log") + if err != nil { + log.Fatalln("Failed to open log file", err) + } + log.SetOutput(logFile) + null, err := os.OpenFile("/dev/null", os.O_RDWR, 0) + if err != nil { + log.Fatalln("Failed to open /dev/null", err) + } + fd := null.Fd() + syscall.Dup2(int(fd), int(os.Stdin.Fd())) + syscall.Dup2(int(fd), int(os.Stdout.Fd())) + syscall.Dup2(int(fd), int(os.Stderr.Fd())) + } + + eventsPath := path + "/events" + events, err := os.Open(eventsPath) + if err != nil { + log.Fatalln("Failed to open file", eventsPath, err) + } + // 512 bytes is easily big enough to read a whole connection id + buf := make([]byte, 512) + for { + n, err := events.Read(buf) + if err != nil { + log.Fatalln("Error reading events file", err) + } + id, err := strconv.Atoi(strings.TrimSpace(string(buf[0:n]))) + if err != nil { + log.Fatalln("Failed to parse integer connection id", err) + } + go handleOne(id) + } +} + +func handleOne(id int) { + log.Println(id, "handleOne") + readPath := fmt.Sprintf("%s/connections/%d/read", path, id) + // Remove will cause the server end to close + defer func(){ + log.Println(id, "handleOne closing, removing", readPath) + os.Remove(readPath) + }() + + read, err := os.Open(readPath) + if err != nil { + // Fatal because this is a bug in the server implementation + log.Fatalln("Failed to open read file", readPath, err) + } + defer read.Close() + + var conn *net.UnixConn + // Cope with the server socket appearing up to 10s later + for i := 0; i < 200; i++ { + conn, err = net.DialUnix("unix", nil, &net.UnixAddr{sock, "unix"}) + if err == nil { + break + } + time.Sleep(50 * time.Millisecond) + } + if err != nil { + // If the forwarding program has broken then close and continue + log.Println("Failed to connect to Unix domain socket after 10s", sock, err) + return + } + + w := make(chan int64) + go func() { + writePath := fmt.Sprintf("%s/connections/%d/write", path, id) + write, err := os.OpenFile(writePath, os.O_WRONLY, 0666) + if err != nil { + // Fatal because this is a bug in the server implementation + log.Fatalln("Failed to open write file", writePath, err) + } + log.Println(id, "copying from", sock, "to", writePath) + n, err := io.Copy(write, conn) + if err != nil { + log.Println("error copying from Unix domain socket to 9P", err) + } + log.Println(id, "wrote", n, "bytes to", writePath) + conn.CloseRead() + write.Close() + os.Remove(writePath) + w <- n + }() + + totalRead := int64(0) + log.Println(id, "copying from", readPath, "to", sock) + for { + // EOF is used to signal a chunk/packet of data + n, err := io.Copy(conn, read) + totalRead = totalRead + n + log.Println(id, "copied a packet of size", n, "bytes from stream") + if err != nil { + log.Println(id, "error copying from stream file to Unix domain socket:", err) + break + } + if n == 0 { + log.Println(id, "read zero-length chunk from stream file: treating as EOF") + break + } + } + conn.CloseWrite(); + + log.Println(id, "waiting for writer to close") + totalWritten := <-w + log.Println(id, "read", totalRead, "written", totalWritten) +} diff --git a/alpine/packages/README.md b/alpine/packages/README.md new file mode 100644 index 000000000..92ab2d819 --- /dev/null +++ b/alpine/packages/README.md @@ -0,0 +1,3 @@ +The directories below should be turned into proper packages, built from upstream. + +Currently these are temporarily vendored from pinata. diff --git a/alpine/packages/mdnstool/.gitignore b/alpine/packages/mdnstool/.gitignore new file mode 100644 index 000000000..c7e545396 --- /dev/null +++ b/alpine/packages/mdnstool/.gitignore @@ -0,0 +1 @@ +mdnstool diff --git a/alpine/packages/mdnstool/Dockerfile b/alpine/packages/mdnstool/Dockerfile new file mode 100644 index 000000000..183417164 --- /dev/null +++ b/alpine/packages/mdnstool/Dockerfile @@ -0,0 +1,10 @@ +FROM golang:alpine + +RUN apk update && apk upgrade && apk add git + +RUN mkdir -p /go/src/mdnstool +WORKDIR /go/src/mdnstool + +COPY . /go/src/mdnstool/ +RUN go get +RUN go install diff --git a/alpine/packages/mdnstool/Makefile b/alpine/packages/mdnstool/Makefile new file mode 100644 index 000000000..1b56f4bc4 --- /dev/null +++ b/alpine/packages/mdnstool/Makefile @@ -0,0 +1,8 @@ +all: mdnstool + +mdnstool: Dockerfile mdnstool.go mdnsmon/mdnsmon.go + docker build -t mdnstool:test . + docker run mdnstool:test cat /go/bin/mdnstool > mdnstool + +clean: + rm -f mdnstool diff --git a/alpine/packages/mdnstool/README.md b/alpine/packages/mdnstool/README.md new file mode 100644 index 000000000..a6d344285 --- /dev/null +++ b/alpine/packages/mdnstool/README.md @@ -0,0 +1,31 @@ +Tool to monitor a network interface for IP changes and publish an mDNS service. + +To publish `docker.local` and map it to the IP of interface `eth0`: + +``` +./mdnstool -if eth0 +``` + +Options: + +``` +Usage of ./mdnstool: + -hostname string + Hostname - must be FQDN and end with . (default "docker.local.") + -if string + Network interface to bind multicast listener to. This interface will be monitored for IP changes. (default "eth0") + -info string + TXT service description (default "Moby") + -instance string + Instance description (default "Moby") + -port int + Service port (default 22) + -service string + SRV service type (default "_ssh._tcp") +``` + +To build for Linux: + +``` +GOOS=linux GOARCH=386 go build -v +``` diff --git a/alpine/packages/mdnstool/mdnsmon/mdnsmon.go b/alpine/packages/mdnstool/mdnsmon/mdnsmon.go new file mode 100644 index 000000000..cb3998327 --- /dev/null +++ b/alpine/packages/mdnstool/mdnsmon/mdnsmon.go @@ -0,0 +1,153 @@ +package mdnsmon + +// mDNS server that publishes a service with the IP address(es) of a monitored network interface. + +import ( + "log" + "net" + "time" + + "github.com/hashicorp/mdns" +) + +type MDNSServer struct { + service *mdns.MDNSService + iface *net.Interface + ip_updates chan []net.IP + shutdown chan int +} + +// NewServer creates a new mDNS service and server configuration. +func NewServer(hostname string, instance string, port int, srv string, info []string, iface *net.Interface) (*MDNSServer, error) { + service, err := mdns.NewMDNSService(instance, srv, "local.", hostname, port, []net.IP{net.ParseIP("0.0.0.0")}, info) + if err != nil { + return nil, err + } + + ip_updates := make(chan []net.IP) + shutdown := make(chan int) + + return &MDNSServer{service: service, ip_updates: ip_updates, shutdown: shutdown, iface: iface}, nil +} + +// getIPs gets a list of IP addresses from an interface +func getIPs(iface *net.Interface) []net.IP { + addrs, err := iface.Addrs() + if err != nil { + log.Printf("Unable to read interface address(es), error: %s", err) + return []net.IP{} + } + + var ips []net.IP + for _, a := range addrs { + switch v := a.(type) { + case *net.IPNet: + if v.IP.To4() != nil { // Only support IPv4 for now + ips = append(ips, v.IP) + } + } + } + + return ips +} + +func (m *MDNSServer) runServer() { + var server *mdns.Server + var err error + + defer func() { + if server != nil { + log.Println("Shutting down mDNS server...") + server.Shutdown() + } + }() + + for { + select { + case ips := <-m.ip_updates: // New IP set received, registering service + // Update service/zone record + m.service.IPs = ips + + // Shutdown old mDNS server, if running + if server != nil { + log.Println("New configuration - shutting down mDNS server...") + server.Shutdown() + time.Sleep(1 * time.Second) + server = nil + } + + // Skip if no IPs found + if len(ips) == 0 { + log.Println("No IP address. Waiting for IP to be configured.") + continue + } + + // Create the mDNS server + log.Println("Answering requests for IP(s) ", ips) + server, err = mdns.NewServer(&mdns.Config{Zone: m.service, Iface: m.iface}) + if err != nil { + log.Println(err) + m.service.IPs = []net.IP{} // Reset IP set so we can automatically retry later + } + case <-m.shutdown: + break + } + } +} + +// isIPsequal compares to slices with IPs +func isIPsequal(a []net.IP, b []net.IP) bool { + if a == nil && b == nil { + return true + } + if a == nil && b != nil { + return false + } + if a != nil && b == nil { + return false + } + if len(a) != len(b) { + return false + } + for _, ip1 := range a { + match := false + for _, ip2 := range b { + if ip1.Equal(ip2) { + match = true + break + } + } + if match == false { // if one ip from a is not in b, return false + return false + } + } + return true +} + +// Start starts the background mDNS server and starts monitoring the network interface for IP changes. +func (m *MDNSServer) Start() { + // Start background server + go m.runServer() + + // Monitor interface for IP changes + for { + ips := getIPs(m.iface) + if !isIPsequal(ips, m.service.IPs) { + log.Println("IP configuration:", ips) + m.ip_updates <- ips + } + + //TODO(magnuss) Monitor using netlink? + if len(ips) == 0 { // Sleep shorter if no IP found + time.Sleep(1 * time.Second) + } else { + time.Sleep(60 * time.Second) // Takes longer to react on IP change, but mDNS has TTL of 120 sec + } + } + +} + +// Shutdown stops the background mDNS server and stops monitorint the network interface for IP changes. +func (m *MDNSServer) Shutdown() { + m.shutdown <- 0 // TODO(magnuss) Wait for mDNS to shutdown +} diff --git a/alpine/packages/mdnstool/mdnstool.go b/alpine/packages/mdnstool/mdnstool.go new file mode 100644 index 000000000..7c50cd5b6 --- /dev/null +++ b/alpine/packages/mdnstool/mdnstool.go @@ -0,0 +1,57 @@ +package main + +// CLI tool for mDNSmon. Monitors a network interface for IP changes and re-publishes the mDNS service + +import ( + "flag" + "log" + "net" + "os" + "syscall" + + "mdnstool/mdnsmon" +) + +func main() { + + hostname := flag.String("hostname", "docker.local.", "Hostname - must be FQDN and end with .") + instance := flag.String("instance", "Moby", "Instance description") + port := flag.Int("port", 22, "Service port") + srv := flag.String("service", "_ssh._tcp", "SRV service type") + info := flag.String("info", "Moby", "TXT service description") + iface_name := flag.String("if", "eth0", "Network interface to bind multicast listener to. This interface will be monitored for IP changes.") + detach := flag.Bool("detach", false, "Detach from terminal") + + flag.Parse() + + // Deatch from terminal (based on code from 9pudc) + if *detach { + logFile, err := os.Create("/var/log/mdnstool.log") + if err != nil { + log.Fatalln("Failed to open log file", err) + } + log.SetOutput(logFile) + null, err := os.OpenFile("/dev/null", os.O_RDWR, 0) + if err != nil { + log.Fatalln("Failed to open /dev/null", err) + } + fd := null.Fd() + syscall.Dup2(int(fd), int(os.Stdin.Fd())) + syscall.Dup2(int(fd), int(os.Stdout.Fd())) + syscall.Dup2(int(fd), int(os.Stderr.Fd())) + } + + iface, err := net.InterfaceByName(*iface_name) + if err != nil { + log.Fatal(err) + } + + s, err := mdnsmon.NewServer(*hostname, *instance, *port, *srv, []string{*info}, iface) + if err != nil { + log.Fatal(err) + } + + go s.Start() + defer s.Shutdown() + select {} +}