diff --git a/README.md b/README.md index fedeca2..51c3431 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 @@ -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 +``` diff --git a/cmd/find.go b/cmd/find.go new file mode 100644 index 0000000..a20e981 --- /dev/null +++ b/cmd/find.go @@ -0,0 +1,128 @@ +/* +Copyright © 2021 NAME HERE + +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() + } +} diff --git a/cmd/ip.go b/cmd/ip.go new file mode 100644 index 0000000..5b6a1b0 --- /dev/null +++ b/cmd/ip.go @@ -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 +} diff --git a/cmd/network.go b/cmd/network.go index 11a0f9e..2f86061 100644 --- a/cmd/network.go +++ b/cmd/network.go @@ -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 { diff --git a/cmd/root.go b/cmd/root.go index e604134..c41282f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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 @@ -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") } diff --git a/ssh/ssh.go b/ssh/ssh.go index 94ce8d9..c9b9b4a 100644 --- a/ssh/ssh.go +++ b/ssh/ssh.go @@ -23,6 +23,7 @@ type SSHConnection struct { Password string UseSSHKey bool Debug bool + Timeout int64 } func (c *SSHConnection) Connect(user string, address string) error { diff --git a/test/run-docker.sh b/test/run-docker.sh old mode 100644 new mode 100755