diff --git a/src/miniccc/commands.go b/src/miniccc/commands.go index 1b30b23f3..ea870c3fb 100644 --- a/src/miniccc/commands.go +++ b/src/miniccc/commands.go @@ -7,11 +7,16 @@ package main import ( "bufio" "bytes" - log "minilog" + "fmt" + "net" + "net/url" "os/exec" "path/filepath" - "ron" "strings" + "time" + + log "minilog" + "ron" ) func processCommand(cmd *ron.Command) { @@ -44,6 +49,10 @@ func processCommand(cmd *ron.Command) { resp.Stdout, resp.Stderr = runCommand(cmd.Stdin, cmd.Stdout, cmd.Stderr, cmd.Command, cmd.Background) } + if cmd.ConnTest != nil { + resp.Stdout, resp.Stderr = testConnect(cmd.ConnTest) + } + if len(cmd.FilesRecv) != 0 { sendFiles(cmd.ID, cmd.FilesRecv) } @@ -267,3 +276,45 @@ func killAll(needle string) { } } } + +func testConnect(test *ron.ConnTest) (string, string) { + log.Debug("testConnect called with %v", *test) + + uri, err := url.Parse(test.Endpoint) + if err != nil { + return "", fmt.Sprintf("unable to parse test URI %s: %v", test.Endpoint, err) + } + + timeout := time.After(test.Wait) + + for { + select { + case <-timeout: + return fmt.Sprintf("%s | fail", uri.Host), "" + default: + if conn, err := net.DialTimeout(uri.Scheme, uri.Host, 500*time.Millisecond); err == nil { + defer conn.Close() + + if uri.Scheme == "udp" { + if err := conn.SetDeadline(time.Now().Add(500 * time.Millisecond)); err != nil { + return fmt.Sprintf("%s | fail", uri.Host), "" + } + + if len(test.Packet) > 0 { + if _, err := conn.Write(test.Packet); err != nil { + return fmt.Sprintf("%s | fail", uri.Host), "" + } + } + + buf := make([]byte, 1) + + if _, err := conn.Read(buf); err != nil { + return fmt.Sprintf("%s | fail", uri.Host), "" + } + } + + return fmt.Sprintf("%s | pass", uri.Host), "" + } + } + } +} diff --git a/src/minimega/cc_cli.go b/src/minimega/cc_cli.go index bdcf03f0e..12cc45e7e 100644 --- a/src/minimega/cc_cli.go +++ b/src/minimega/cc_cli.go @@ -5,18 +5,21 @@ package main import ( + "encoding/base64" "encoding/json" "errors" "fmt" - "minicli" - log "minilog" "net" "os" - "ron" "sort" "strconv" "strings" "syscall" + "time" + + "minicli" + log "minilog" + "ron" ) type ccMount struct { @@ -84,6 +87,23 @@ without arguments displays the existing mounts. Users can use "clear cc mount" to unmount the filesystem of one or all VMs. This should be done before killing or stopping the VM ("clear namespace " will handle this automatically). +"cc test-conn" allows users to test network connectivity from a guest to the +given IP or domain name and port. The wait timeout should be specified as a Go +duration string (e.g. 5s, 1m). If "udp" is used, a "base64 udp packet" that will +generate a valid response must be specified. Results of the test will be written +to the command's STDOUT file, whether it passed or failed. An example test is as +follows: + + cc test-conn tcp 10.0.0.68 443 wait 10s + +If the above test passes, STDOUT for the command will contain the following: + + 10.0.0.68:443 | pass + +If it fails, STDOUT will instead contain the following: + + 10.0.0.68:443 | fail + For more documentation, see the article "Command and Control API Tutorial".`, Patterns: []string{ "cc", @@ -112,6 +132,8 @@ For more documentation, see the article "Command and Control API Tutorial".`, "cc ", "cc ", + + "cc wait [base64 udp packet]", }, Call: wrapBroadcastCLI(cliCC), }, @@ -169,6 +191,7 @@ var ccCliSubHandlers = map[string]wrappedCLIFunc{ "send": cliCCFileSend, "tunnel": cliCCTunnel, "listen": cliCCListen, + "test-conn": cliCCTestConn, } func cliCC(ns *Namespace, c *minicli.Command, resp *minicli.Response) error { @@ -568,7 +591,7 @@ func cliCCClients(ns *Namespace, c *minicli.Command, resp *minicli.Response) err func cliCCCommand(ns *Namespace, c *minicli.Command, resp *minicli.Response) error { resp.Header = []string{ "id", "prefix", "command", "responses", "background", - "sent", "received", "level", "filter", + "sent", "received", "connectivity", "level", "filter", } resp.Tabular = [][]string{} @@ -593,6 +616,12 @@ func cliCCCommand(ns *Namespace, c *minicli.Command, resp *minicli.Response) err fmt.Sprintf("%v", v.FilesRecv), } + if v.ConnTest != nil { + row = append(row, fmt.Sprintf("%s (%v wait)", v.ConnTest.Endpoint, v.ConnTest.Wait)) + } else { + row = append(row, "") + } + if v.Level != nil { row = append(row, v.Level.String()) } else { @@ -642,6 +671,41 @@ func cliCCListen(ns *Namespace, c *minicli.Command, resp *minicli.Response) erro return ns.ccServer.Listen(port) } +func cliCCTestConn(ns *Namespace, c *minicli.Command, resp *minicli.Response) error { + if _, err := strconv.Atoi(c.StringArgs["port"]); err != nil { + return fmt.Errorf("invalid port %s: %v", c.StringArgs["port"], err) + } + + wait, err := time.ParseDuration(c.StringArgs["timeout"]) + if err != nil { + return fmt.Errorf("invalid wait duration %s: %v", c.StringArgs["timeout"], err) + } + + scheme := "tcp" + if c.BoolArgs["udp"] { + scheme = "udp" + } + + test := ron.ConnTest{ + Endpoint: fmt.Sprintf("%s://%s:%s", scheme, c.StringArgs["ip"], c.StringArgs["port"]), + Wait: wait, + } + + if packet := c.StringArgs["base64"]; len(packet) > 0 { + var err error + + test.Packet, err = base64.StdEncoding.DecodeString(packet) + if err != nil { + return fmt.Errorf("unable to decode base64 packet string: %v", err) + } + } + + cmd := &ron.Command{ConnTest: &test} + + resp.Data = ns.NewCommand(cmd) + return nil +} + // cliCCMount needs to collect mounts from both the local ccMounts for the // namespace and across the cluster. func cliCCMount(c *minicli.Command, respChan chan<- minicli.Responses) { diff --git a/src/ron/command.go b/src/ron/command.go index 75bb52590..10c454c72 100644 --- a/src/ron/command.go +++ b/src/ron/command.go @@ -8,6 +8,7 @@ import ( "fmt" log "minilog" "strings" + "time" ) type Filter struct { @@ -37,6 +38,9 @@ type Command struct { // Files to transfer back to the master FilesRecv []string + // Connectivity test to execute + ConnTest *ConnTest + // PID of the process to signal, -1 signals all processes PID int @@ -73,6 +77,12 @@ type Response struct { Stderr string } +type ConnTest struct { + Endpoint string + Wait time.Duration + Packet []byte +} + func (f *Filter) String() string { if f == nil { return "" @@ -128,10 +138,16 @@ func (c *Command) Copy() *Command { c2.Filter = new(Filter) *c2.Filter = *c.Filter } + if c.Level != nil { c2.Level = new(log.Level) *c2.Level = *c.Level } + if c.ConnTest != nil { + c2.ConnTest = new(ConnTest) + *c2.ConnTest = *c.ConnTest + } + return c2 }