Skip to content

Commit

Permalink
Initial changes for cloud-cmd.
Browse files Browse the repository at this point in the history
  • Loading branch information
freb committed Aug 10, 2019
1 parent 3af3fc3 commit 36c9271
Show file tree
Hide file tree
Showing 5 changed files with 326 additions and 48 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
cloud-cmd
out-*.xml
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
# Fork

Forked from tomsteele/cloud-proxy and repurposed to support scanning only, and maybe eventuall additional commands.
Forked from `tomsteele/cloud-proxy` and repurposed to support scanning only, and maybe eventuall additional commands.

./cloud-cmd -p 1,2,3,4-200,500 -nmap

```shell
nmap -oX ex.tcp.1.datascan-01 nmap -v4 -Pn -n -sS --max-rtt-timeout 120ms --min-rtt-timeout 50ms --initial-rtt-timeout 120ms --max-retries 1 --max-rate 10 --min-hostgroup 4096

m := map[string]interface{}{"name": "John", "age": 47}
t := template.Must(template.New("").Parse("Hi {{.name}}. Your age is {{.age}}\n"))
t.Execute(os.Stdout, m)
```

the basic idea is that for nmap, you pass in a port list. the list will be devided up into a number of equal chuncks that matches the number of launched droplets. You also pass in a templated nmap command. The templated nmap command as one variable for the split ports `{{.ports}}`, one variable for the instance index `{{.index}}` (which will be a zero padded number with the padding matching the number of launched hosts (0 for 1-9, 1 for 10-99, 2 for 100-999, etc.)). We should probably also make available `{{.ip}}` for the public ip address of the droplet and `{{.hostname}}` (or dropletname) for the name of the droplet

The nmap command will run and stdout will be streamed into output files matching the index. This would mean you're meant to run nmap with `-oX -`. Though we could create an output file and download that at the end, or stream that file back as its written. I think we start with -oX first.

We should support some watch command, but for now it may be easiest to just print out the makings of a watch command to be filled in by the caller.

Just need to figure out what I want to actuall print to the screen now.

# cloud-proxy
cloud-proxy creates multiple DO droplets and then starts local socks proxies using SSH. After exiting, the droplets are deleted.
Expand Down
6 changes: 4 additions & 2 deletions do.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"fmt"

"github.com/digitalocean/godo"
Expand All @@ -9,7 +10,7 @@ import (

func doRegions(client *godo.Client) ([]string, error) {
slugs := []string{}
regions, _, err := client.Regions.List(&godo.ListOptions{})
regions, _, err := client.Regions.List(context.Background(), &godo.ListOptions{})
if err != nil {
return slugs, err
}
Expand All @@ -32,7 +33,8 @@ func newDropLetMultiCreateRequest(prefix, region, keyID string, count int) *godo
Region: region,
Size: "512mb",
Image: godo.DropletCreateImage{
Slug: "ubuntu-14-04-x64",
// Slug: "ubuntu-14-04-x64",
Slug: "debian-10-x64",
},
SSHKeys: []godo.DropletCreateSSHKey{
godo.DropletCreateSSHKey{
Expand Down
117 changes: 114 additions & 3 deletions machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,46 @@ package main

import (
"bufio"
"bytes"
"context"
"fmt"
"log"
"os"
"os/exec"
"strings"
"text/template"

"golang.org/x/crypto/ssh"

"github.com/digitalocean/godo"
)

// Machine is just a wrapper around a created droplet.
type Machine struct {
ID int
Index int // Index (starting with 1) for the order droplet was created
Name string
IPv4 string
SSHActive bool
Stderr *bufio.Reader
Listener string
CMD *exec.Cmd

SSHConfig *ssh.ClientConfig
SSHClient *ssh.Client
Template string
Ports string
}

// Println uses fmt.Println to log to the console, formatted for this machine.
func (m *Machine) Println(a ...interface{}) {
a = append([]interface{}{fmt.Sprintf("%s (%d):", m.Name, m.Index)}, a...)
log.Println(a...)
}

// Printf uses fmt.Printf to log to the console, formatted for this machine.
func (m *Machine) Printf(format string, a ...interface{}) {
log.Printf(fmt.Sprintf("%s (%d): %s", m.Name, m.Index, format), a...)
}

// IsReady ensures that the machine has an IP address.
Expand All @@ -26,7 +51,7 @@ func (m *Machine) IsReady() bool {

// GetIPs populates the IPv4 address of the machine.
func (m *Machine) GetIPs(client *godo.Client) error {
droplet, _, err := client.Droplets.Get(m.ID)
droplet, _, err := client.Droplets.Get(context.Background(), m.ID)
if err != nil {
return err
}
Expand All @@ -37,6 +62,93 @@ func (m *Machine) GetIPs(client *godo.Client) error {
return nil
}

func (m *Machine) InstallPackages(packages []string) error {
session, err := m.SSHClient.NewSession()
if err != nil {
return fmt.Errorf("error creating ssh session: %v", err)
}
out, err := session.CombinedOutput("apt-get update")
if err != nil {
return fmt.Errorf("error running apt-get update command: %v", err)
}
_ = out
// if session.CombinedOutput err == nil, command returned with zero exit status
// we only really need out in verbose mode or on error
// m.Println(string(out))
session.Close()

session, err = m.SSHClient.NewSession()
if err != nil {
return fmt.Errorf("error creating ssh session: %v", err)
}
out, err = session.CombinedOutput("apt-get install -y " + strings.Join(packages, " "))
if err != nil {
return fmt.Errorf("error running apt-get install command: %v", err)
}
_ = out
// if session.CombinedOutput err == nil, command returned with zero exit status
// we only really need out in verbose mode or on error
// m.Println(string(out))
session.Close()

return nil
}

// RunCommand runs the templated command on the remote host. This should
// be launched in a go routine using a waitgroup.
func (m *Machine) RunCommand(filename string) error {
output, err := os.Create(filename)
if err != nil {
return fmt.Errorf("error creating file for stdout: %v", err)
}
session, err := m.SSHClient.NewSession()
if err != nil {
return fmt.Errorf("error creating ssh session: %v", err)
}
session.Stdout = output
// hook up a stderr pipe to use the machine's println to record the status. i think launching this
// in a goroutine and then terminating the goroutine on eof would be good enough.

stderr, err := session.StderrPipe()
if err != nil {
return err
}
m.Stderr = bufio.NewReader(stderr)

cmd, err := m.Command()
if err != nil {
return fmt.Errorf("error creating templated command: %v", err)
}

if err = session.Start(cmd); err != nil {
return fmt.Errorf("error running command: %v", err)
}

go m.PrintStdError()

return session.Wait()
}

// Command generates the command to be run on the remote hosting using the defined Template.
func (m *Machine) Command() (string, error) {
vars := map[string]interface{}{
"ports": m.Ports,
"index": m.Index,
"ip": m.IPv4,
"name": m.Name,
}
var cmd bytes.Buffer
t, err := template.New("").Parse(m.Template)
if err != nil {
return "", fmt.Errorf("error parsing template: %v", err)
}
err = t.Execute(&cmd, vars)
if err != nil {
return "", fmt.Errorf("error executing template: %v", err)
}
return cmd.String(), nil
}

// StartSSHProxy starts a socks proxy on 127.0.0.1 or the desired port.
func (m *Machine) StartSSHProxy(port, sshKeyLocation string) error {
m.Listener = port
Expand All @@ -55,7 +167,7 @@ func (m *Machine) StartSSHProxy(port, sshKeyLocation string) error {

// Destroy deletes the droplet.
func (m *Machine) Destroy(client *godo.Client) error {
_, err := client.Droplets.Delete(m.ID)
_, err := client.Droplets.Delete(context.Background(), m.ID)
return err
}

Expand Down Expand Up @@ -84,7 +196,6 @@ func (m *Machine) PrintStdError() {
fmt.Println(str)
}
}

}

func printProxyChains(machines []Machine) {
Expand Down
Loading

0 comments on commit 36c9271

Please sign in to comment.