Skip to content

Commit

Permalink
feat: add find command (#9)
Browse files Browse the repository at this point in the history
* fix: add execution permissions to run-dockr test

* feat: add find command

* feat: use goroutines to process hosts

* chore: bump version to 1.3.0-rc3

* feat: blacklist WSL interface

* docs: cover new command

* docs: describe even more the find command

* docs: add examples
  • Loading branch information
sralloza authored Nov 25, 2021
1 parent 1cf6150 commit dd32133
Show file tree
Hide file tree
Showing 7 changed files with 316 additions and 4 deletions.
74 changes: 72 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,18 @@ That's why this repo was created. The first version was created in Python, but t

### Initial setup

What happens if you don't have an spare screen and keyboard? Don't worry, this script has your back. After flashing your raspbian image into your ssh card, execute the `boot` command. It will setup the ssh server and optionally a wifi connection to work the first time you turn your raspberry on. By default it will also add some lines to `cmdline.txt` to enable some features needed to run a k3s cluster. If you want to disable it, pass `--cmdline=""` to the `boot` command.
What happens if you don't have an spare screen and keyboard? Don't worry, this script has your back. After flashing your raspbian image into your ssh card, execute the `boot` command. It will setup the ssh server and optionally a **wifi connection** to work the first time you turn your raspberry on. By default it will also add some lines to `cmdline.txt` to enable some features needed to run a k3s cluster. If you want to disable it, pass `--cmdline=""` to the `boot` command.

Note: you must pass the path of your sd card (the `BOOT_PATH` argument). In windows it will likely be `E:/`, `F:/` or something similar.

### Raspberry's initial IPv4

When you plug in your raspberry after enabling ssh connection, you can't know what its IPv4 is unless you have a spare screen or you have access to your router's configuration.

This is where the `find` command comes in really handy. You only have to specify your network IP (like `--subnet=192.168.0.1/24` or `--subnet=10.0.0.1/24`). Well, in reality you don't have to even do this, because by default the program will get your local IP (excluding the WSL interface) and use it with a 24-bit mask to build your presumably network IP, so `LOCAL_IP/24`.

There are some useful flags to make this command work, but the defaults will probably be just OK. For more info, refer to the [find command docs](#find).

### SSH Keys management

I have some PCs with ssh keys, so naturally I would want to be able to ssh into the Raspberry from any of my PCs.
Expand Down Expand Up @@ -60,9 +68,41 @@ Flags:
--wifi-ssid string WiFi SSID

Global Flags:
--debug Enable debug 21:33:29
--debug Enable debug
```

### find

```shell
$ rpi-provisioner find --help
Find your raspberry pi in your local network using SSH.

Usage:
rpi-provisioner find [flags]

Flags:
-h, --help help for find
--live Print valid hosts right after found
--password string Password to login via ssh (default "raspberry")
--port int Port to connect via ssh (default 22)
--subnet string Subnet to find the raspberry
--time Show hosts processing time
--timeout int Timeout in ns to wait in ssh connections (default 1)
--user string User to login via ssh (default "pi")

Global Flags:
--debug Enable debug
```

More info:

- `--subnet`: this is the most important flag. You won't probably use it, but with this flag you can specify your local network's IP. If you left this blank, the program will try to generate it from your local IP address. If it is wrong, use this flag to really find your raspberry pi in your local network.
- `--live`: By default when you start the analysis, the valid raspberry's IP will only be shown at the end. You can use this flag to see as soon as it is discovered.
- `--user & --password`: login user and password to use via SSH. The default credentials for raspbian are `pi:raspberry`, as the default values for each flag. If you use another OS you can use this flags to change it.
- `--port`: just in case the default SSH port is not 22, use this flag to set it right.
- `--time`: instead of showing `Done` when the scan finishes, it will display `Done (x seconds)`, showing the analysis time.
- `--timeout`: Timeout in nanoseconds to wait in SSH connections. It is directly passed to the SSH Dial method. To be fair I don't really know if this works, so don't use it. By default is 1, but I don't know if it affects performance. If you know more about this flag, feel free to open an issue or a PR correcting the documentation.

### authorized-keys

```shell
Expand Down Expand Up @@ -166,3 +206,33 @@ Flags:
Global Flags:
--debug Enable debug
```
## Examples of how I really use each command
### boot example
```shell
rpi-provisioner boot --wifi-ssid $WIFI_SSID --wifi-pass $WIFI_PASS E:/
```
### find example
```shell
rpi-provisioner find --time --live
```
### authorized-keys example
rpi-provisioner authorized-keys --ssh-key --host $RASPBERRY_IP --user $USER --s3-path $S3_REGION/$S3_BUCKET/$S3_FILE
### layer1 example
```shell
rpi-provisioner layer1 --deployer-user $NEW_USER --deployer-password $NEW_PASSWORD --host $RASPBERRY_IP --hostname $HOSTNAME --s3-path $S3_REGION/$S3_BUCKET/$S3_FILE
```
### layer2 example
```shell
rpi-provisioner layer2 --user $USER --host $RASPBERRY_IP
```
128 changes: 128 additions & 0 deletions cmd/find.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
Copyright © 2021 NAME HERE <EMAIL ADDRESS>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd

import (
"fmt"
"net"
"sync"
"time"

"github.com/spf13/cobra"
"github.com/sralloza/rpi-provisioner/ssh"
)

type FindArgs struct {
subnet string
user string
password string
live bool
time bool
port int
timeout int
}

func NewFindCommand() *cobra.Command {
args := FindArgs{}
var findCmd = &cobra.Command{
Use: "find",
Short: "Find your raspberry pi in your local network",
Long: `Find your raspberry pi in your local network using SSH.`,
RunE: func(cmd *cobra.Command, posArgs []string) error {
if err := findHost(args); err != nil {
return err
}
return nil
},
}
findCmd.Flags().StringVar(&args.subnet, "subnet", "", "Subnet to find the raspberry")
findCmd.Flags().StringVar(&args.user, "user", "pi", "User to login via ssh")
findCmd.Flags().StringVar(&args.password, "password", "raspberry", "Password to login via ssh")
findCmd.Flags().IntVar(&args.port, "port", 22, "Port to connect via ssh")
findCmd.Flags().BoolVar(&args.live, "live", false, "Print valid hosts right after found")
findCmd.Flags().BoolVar(&args.time, "time", false, "Show hosts processing time")
findCmd.Flags().IntVar(&args.timeout, "timeout", 1, "Timeout in ns to wait in ssh connections")
return findCmd
}

func findHost(args FindArgs) error {
CIDR := args.subnet
if CIDR == "" {
defaultCDIR, err := getDefaultCDIR()
if err != nil {
return err
}
CIDR = defaultCDIR
}

fmt.Printf("Getting IP addresses from CIDR %v...\n", CIDR)
ipv4List, err := getIpsFromCIDR(CIDR)
if err != nil {
return err
}
fmt.Printf("Found %d IP addresses\n", len(ipv4List))

fmt.Println("Validating IP addresses...")
start := time.Now()
finder := Finder{totalIPs: ipv4List, findArgs: args}
validIPs := finder.findValidSSHHosts()
if args.time {
elapsed := time.Since(start)
fmt.Printf("Done (%s)\n", elapsed)
} else {
fmt.Println("Done")
}

fmt.Printf("Valid ips: %v\n", validIPs)
return nil
}

type Finder struct {
mu sync.Mutex
wg sync.WaitGroup
totalIPs []net.IP
validIPs []net.IP
findArgs FindArgs
}

func (f *Finder) findValidSSHHosts() []net.IP {
for _, ip := range f.totalIPs {
f.wg.Add(1)
go f.checkSSHConnection(ip)
}
f.wg.Wait()
return f.validIPs
}

func (f *Finder) checkSSHConnection(ipv4Addr net.IP) {
defer f.wg.Done()
connection := ssh.SSHConnection{
Password: f.findArgs.password,
UseSSHKey: false,
Debug: false,
Timeout: 1,
}
addr := fmt.Sprintf("%v:%d", ipv4Addr, f.findArgs.port)
err := connection.Connect(f.findArgs.user, addr)
if err == nil {
f.mu.Lock()
f.validIPs = append(f.validIPs, ipv4Addr)
if f.findArgs.live {
fmt.Printf("Found valid host: %v\n", ipv4Addr)
}
f.mu.Unlock()
}
}
112 changes: 112 additions & 0 deletions cmd/ip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package cmd

import (
"encoding/binary"
"errors"
"fmt"
"net"
)

var BlacklistedInterfaces = []string{"vEthernet (WSL)"}

func getIpsFromCIDR(CIDR string) ([]net.IP, error) {
_, ipv4Net, err := net.ParseCIDR(CIDR)

if err != nil {
return []net.IP{}, fmt.Errorf("error paring CIDR: %w", err)
}

// convert IPNet struct mask and address to uint32
// network is BigEndian
mask := binary.BigEndian.Uint32(ipv4Net.Mask)
start := binary.BigEndian.Uint32(ipv4Net.IP)

// find the final address
finish := (start & mask) | (mask ^ 0xffffffff)
ips := []net.IP{}

// loop through addresses as uint32
for i := start; i <= finish; i++ {
// convert back to net.IP
ip := make(net.IP, 4)
binary.BigEndian.PutUint32(ip, i)
ips = append(ips, ip)
}

return ips, nil
}

func getDefaultCDIR() (string, error) {
localIP, err := LocalIP()
if err != nil {
return "", fmt.Errorf("error getting local IP: %w", err)
}
return fmt.Sprintf("%v/24", localIP), nil
}

func isInterfaceBlacklisted(iName string) bool {
for _, blacklistedName := range BlacklistedInterfaces {
if blacklistedName == iName {
return true
}
}
return false
}

// LocalIP get the host machine local IP address
func LocalIP() (net.IP, error) {
ifaces, err := net.Interfaces()
if err != nil {
return nil, fmt.Errorf("error getting interfaces: %w", err)
}

for _, i := range ifaces {
if isInterfaceBlacklisted(i.Name) {
continue
}
addrs, err := i.Addrs()
if err != nil {
return nil, fmt.Errorf("error getting interface addresses: %w", err)
}

for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}

if isPrivateIP(ip) {
return ip, nil
}
}
}

return nil, errors.New("error getting local IP")
}

func isPrivateIP(ip net.IP) bool {
var privateIPBlocks []*net.IPNet
for _, cidr := range []string{
// don't check loopback ips
//"127.0.0.0/8", // IPv4 loopback
//"::1/128", // IPv6 loopback
//"fe80::/10", // IPv6 link-local
"10.0.0.0/8", // RFC1918
"172.16.0.0/12", // RFC1918
"192.168.0.0/16", // RFC1918
} {
_, block, _ := net.ParseCIDR(cidr)
privateIPBlocks = append(privateIPBlocks, block)
}

for _, block := range privateIPBlocks {
if block.Contains(ip) {
return true
}
}

return false
}
2 changes: 1 addition & 1 deletion cmd/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func setupNetworking(conn ssh.SSHConnection, args interfaceArgs) (bool, error) {

wlan0Provisioned, err := provisionStaticIPIface(conn, args, "wlan0", 200)
if err != nil {
return false, fmt.Errorf("error provisioning static IP for wlan0", err)
return false, fmt.Errorf("error provisioning static IP for wlan0: %w", err)
}

if !eth0Provisioned && !wlan0Provisioned {
Expand Down
3 changes: 2 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ var rootCmd = &cobra.Command{
After using this script use k3sup to launch the cluster.`,
SilenceErrors: true,
SilenceUsage: true,
Version: "1.3.0-rc2",
Version: "1.3.0-rc3",
}

var DebugFlag bool
Expand All @@ -46,6 +46,7 @@ func init() {
rootCmd.AddCommand(NewAuthorizedKeysCmd())
rootCmd.AddCommand(NewNetworkingCmd())
rootCmd.AddCommand(NewBootCmd())
rootCmd.AddCommand(NewFindCommand())

rootCmd.PersistentFlags().BoolVar(&DebugFlag, "debug", false, "Enable debug")
}
1 change: 1 addition & 0 deletions ssh/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type SSHConnection struct {
Password string
UseSSHKey bool
Debug bool
Timeout int64
}

func (c *SSHConnection) Connect(user string, address string) error {
Expand Down
Empty file modified test/run-docker.sh
100644 → 100755
Empty file.

0 comments on commit dd32133

Please sign in to comment.