back end dev
Build a Golang Remote Administrator
Managing remote device with Go and Rails

Managing Devices with Go

Expanding on a case study started in this post, at work we came across the need for a binary agent that we can deploy on devices to help with troubleshooting and management. Because we use all types of devices, including Android, iOS, Linux, and Windows, we needed a cross-compatible solution. Golang sounded like the perfect tool.

Prototyping quickly in Go, this post is a look at what goes into building something for Mac, Windows, Android, and Linux; as well as how easy Go makes it to access system information.

Setup

First, download and install Go from source, or using your favorite package manager. On Mac, that looks like this:

brew install go

There's some wizardry required to set up the environment Go expects. Create a directory somewhere and add it to your path (I'm using ~/_go):

# ~/.bash_profile
export GOPATH="$HOME/_go"
export GOROOT="$(brew --prefix golang)/libexec"

And grab two optional packages, for taking screenshots and compiling for mobile:

go get github.com/kbinani/screenshot
go get golang.org/x/mobile/app

Now create a directory agent (or literally anything else) inside tthe GOAPTH, and create a file agent.go inside of it.

Writing the Agent

Imports

Let's pull in all of the modules we'll need. The uncommented lines are for gather system information, building the data object, and making a web request to the API. The commented lines are for taking and encoding screenshots, and for building the app for android.

// ~/_go/src/agent/agent.go

import (
    "fmt"
    "io/ioutil"
    "bytes"
    "encoding/json"
    "net/http"
    "net"
    "os"
    // "golang.org/x/mobile/app"
    // "github.com/kbinani/screenshot"
    // "image/png"
    // "encoding/base64"
)
Data Structs

The data structures look very similar to the models used in the Rails API. It keeps everything easier for me personally, but go crazy here.

type Nic struct {
    Addr    string `json:"ip_addr"`
    Mac     string `json:"mac_addr"`
    Name    string `json:"iface_name"`
}

type Device struct {
    Id      string `json:"id"`
    Addrs   []Nic `json:"ip_addrs"`
}
Network Info
//// Get IPs
func externalIP() (Device, error) {
    ifaces, err := net.Interfaces()
    results := []Nic{}
    device := Device{}    // init new Nic struct

    if err != nil {
        return device, err
    }
    for _, iface := range ifaces {
        nic := Nic{}    // init new Nic struct
        name, error := os.Hostname()

        if error != nil {
            return device, error
        }
        device.Id = name

        if iface.Flags&net.FlagUp == 0 {
            continue // interface down
        }
        if iface.Flags&net.FlagLoopback != 0 {
            continue // loopback interface
        }
        addrs, err := iface.Addrs()
        if err != nil {
            return device, err
        }
        for _, addr := range addrs {
            fmt.Println(iface)
            var ip net.IP
            switch v := addr.(type) {
            case *net.IPNet:
                ip = v.IP
            case *net.IPAddr:
                ip = v.IP
            }
            if ip == nil || ip.IsLoopback() {
                continue
            }
            ip = ip.To4()
            if ip == nil {
                continue // not an ipv4 address
            }
            nic.Name = iface.Name
            nic.Addr = ip.String()
            nic.Mac = iface.HardwareAddr.String()
            results = append(results,nic)       // Append nic{} to []results
        }
    }

    device.Addrs = results
    return device, nil
}
Main Function

If building for Mac or Windows:

func main() {
    url := "http://localhost:3000/devices/"

    fmt.Println("URL:>", url)

    // UNCOMMENT IF NOT COMPILING FOR MOBILE
    ip, err := externalIP()
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(ip)

    // POST request to remote endpoint
    j, _ := json.Marshal(ip)
    var jsonStr = []byte(j)
    req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonStr))
    req.Header.Set("X-Custom-Header", "myvalue")
    req.Header.Set("Content-Type", "application/json")

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    // Print Response
    fmt.Println("response Status:", resp.Status)
    fmt.Println("response Headers:", resp.Header)
    body, _ := ioutil.ReadAll(resp.Body)
    fmt.Println("response Body:", string(body))

}

Conclusion

import (
    "fmt"
    "io/ioutil"
    "bytes"
    "encoding/json"
    "net/http"
    "net"
    "os"
)

type Nic struct {
    Addr    string `json:"ip_addr"`
    Mac     string `json:"mac_addr"`
    Name    string `json:"iface_name"`
}

type Device struct {
    Id      string `json:"id"`
    Addrs   []Nic `json:"ip_addrs"`
}

func main() {
    url := "http://localhost:3000/devices/"

    fmt.Println("URL:>", url)

    // UNCOMMENT IF NOT COMPILING FOR MOBILE
    ip, err := externalIP()
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(ip)

    // POST request to remote endpoint
    j, _ := json.Marshal(ip)
    var jsonStr = []byte(j)
    req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonStr))
    req.Header.Set("X-Custom-Header", "myvalue")
    req.Header.Set("Content-Type", "application/json")

    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    // Print Response
    fmt.Println("response Status:", resp.Status)
    fmt.Println("response Headers:", resp.Header)
    body, _ := ioutil.ReadAll(resp.Body)
    fmt.Println("response Body:", string(body))

}

func externalIP() (Device, error) {
    ifaces, err := net.Interfaces()
    results := []Nic{}
    device := Device{}    // init new Nic struct

    if err != nil {
        return device, err
    }
    for _, iface := range ifaces {
        nic := Nic{}    // init new Nic struct
        name, error := os.Hostname()

        if error != nil {
            return device, error
        }
        device.Id = name

        if iface.Flags&net.FlagUp == 0 {
            continue // interface down
        }
        if iface.Flags&net.FlagLoopback != 0 {
            continue // loopback interface
        }
        addrs, err := iface.Addrs()
        if err != nil {
            return device, err
        }
        for _, addr := range addrs {
            fmt.Println(iface)
            var ip net.IP
            switch v := addr.(type) {
            case *net.IPNet:
                ip = v.IP
            case *net.IPAddr:
                ip = v.IP
            }
            if ip == nil || ip.IsLoopback() {
                continue
            }
            ip = ip.To4()
            if ip == nil {
                continue // not an ipv4 address
            }
            nic.Name = iface.Name
            nic.Addr = ip.String()
            nic.Mac = iface.HardwareAddr.String()
            results = append(results,nic)       // Append nic{} to []results
        }
    }

    device.Addrs = results
    return device, nil
}