diff --git a/Makefile b/Makefile index 5a7b5f0..2ed75b7 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,8 @@ CONFDIR ?= "$(PREFIX)/etc/vinyl/network.d" BINARIES := $(BINDIR)/linux-utils SCRIPTS := $(BINDIR)/useradd \ $(BINDIR)/groupadd \ - $(BINDIR)/netctl + $(BINDIR)/netctl \ + $(BINDIR)/wifi CONFIGS := $(CONFDIR)/eth0.toml.sample diff --git a/bin/cmd/root.go b/bin/cmd/root.go index 01739f1..0daa07d 100644 --- a/bin/cmd/root.go +++ b/bin/cmd/root.go @@ -54,6 +54,7 @@ var ( groupName string netctlDir string verbose bool + autoConnect bool ) // rootCmd represents the base command when called without any subcommands @@ -97,4 +98,5 @@ func reset() { groupName = "" netctlDir = netctl.DefaultPath verbose = false + autoConnect = false } diff --git a/bin/cmd/wifi.go b/bin/cmd/wifi.go new file mode 100644 index 0000000..86c9cc2 --- /dev/null +++ b/bin/cmd/wifi.go @@ -0,0 +1,67 @@ +// +build linux + +/* +Copyright © 2021 James Condron +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/vinyl-linux/linux-utils/wifi" +) + +// wifiCmd represents the wifi command +var wifiCmd = &cobra.Command{ + Use: "wifi", + Short: "Manage wifi networks", + Long: "Manage wifi networks", + RunE: func(cmd *cobra.Command, args []string) (err error) { + // load profiles, looking for wifi interfaces + w, err := wifi.New() + if err != nil { + return + } + + nets, err := w.List() + if err != nil { + return + } + + fmt.Println(nets) + + return nil + }, +} + +func init() { + rootCmd.AddCommand(wifiCmd) +} diff --git a/bin/cmd/wifiAdd.go b/bin/cmd/wifiAdd.go new file mode 100644 index 0000000..754cca4 --- /dev/null +++ b/bin/cmd/wifiAdd.go @@ -0,0 +1,84 @@ +// +build linux + +/* +Copyright © 2021 James Condron +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ +package cmd + +import ( + "fmt" + "strings" + "syscall" + + "github.com/spf13/cobra" + "github.com/vinyl-linux/linux-utils/wifi" + "golang.org/x/crypto/ssh/terminal" +) + +// wifiAddCmd represents the wifiAdd command +var wifiAddCmd = &cobra.Command{ + Use: "add [ssid]", + Short: "Add/ configure a wifi network", + Long: "Add/ configure a wifi network", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) (err error) { + ssid := args[0] + + w, err := wifi.New() + if err != nil { + return + } + + fmt.Printf("Network %q password > ", ssid) + password, err := terminal.ReadPassword(int(syscall.Stdin)) + fmt.Println() + + if err != nil { + return + } + + err = w.Create(ssid, strings.TrimSpace(string(password))) + if err != nil { + return + } + + if autoConnect { + err = w.Connect(ssid) + } + + return + }, +} + +func init() { + wifiCmd.AddCommand(wifiAddCmd) + + wifiAddCmd.Flags().BoolVarP(&autoConnect, "connect", "c", true, "connect to network after creating") +} diff --git a/bin/cmd/wifiConnect.go b/bin/cmd/wifiConnect.go new file mode 100644 index 0000000..47d9572 --- /dev/null +++ b/bin/cmd/wifiConnect.go @@ -0,0 +1,60 @@ +// +build linux + +/* +Copyright © 2021 James Condron +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/vinyl-linux/linux-utils/wifi" +) + +// wifiConnectCmd represents the wifiConnect command +var wifiConnectCmd = &cobra.Command{ + Use: "connect [ssid]", + Short: "Connect to the specified ssid", + Long: "Connect to the specified ssid", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) (err error) { + ssid := args[0] + + w, err := wifi.New() + if err != nil { + return + } + + return w.Connect(ssid) + }, +} + +func init() { + wifiCmd.AddCommand(wifiConnectCmd) +} diff --git a/go.mod b/go.mod index 3fb3d28..b8c89e1 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,8 @@ require ( github.com/pelletier/go-toml v1.9.5 github.com/spf13/cobra v1.6.1 github.com/vishvananda/netlink v1.1.0 + golang.org/x/crypto v0.6.0 + pifke.org/wpasupplicant v0.0.0-20221018205742-4b5b2dde8b55 ) require ( @@ -24,4 +26,5 @@ require ( golang.org/x/net v0.7.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.5.0 // indirect + golang.org/x/term v0.5.0 // indirect ) diff --git a/go.sum b/go.sum index 5b3ae85..b953728 100644 --- a/go.sum +++ b/go.sum @@ -78,6 +78,8 @@ github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1Y github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -112,6 +114,8 @@ golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -121,3 +125,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +pifke.org/wpasupplicant v0.0.0-20221018205742-4b5b2dde8b55 h1:dK1ABrY+GK1MxioJS5lqC/Z0gEZOU3sfS+znBk2ehfM= +pifke.org/wpasupplicant v0.0.0-20221018205742-4b5b2dde8b55/go.mod h1:PqiVjTFFZG+iqUphtbHFcBaF6L8jnE7ZXl+Z5zIqG2o= diff --git a/netctl/profile.go b/netctl/profile.go index 7205d74..b52b9d9 100644 --- a/netctl/profile.go +++ b/netctl/profile.go @@ -68,6 +68,7 @@ type Profile struct { Interface string `toml:"interface"` IPv4 Address `toml:",omitempty"` IPv6 Address `toml:",omitempty"` + Wifi bool `toml:wifi,omitempty` // link points to the underlying netlink object link netlink.Link @@ -93,13 +94,34 @@ func (p Profile) Up() (err error) { return } - // Loopback devices are special; we can go ahead and set them - // up the same way each time. In fact, the loopback file only needs - // the value of `Interface` to be set - if loopback.Match([]byte(p.Interface)) { - return p.UpLoopback() + switch { + case loopback.Match([]byte(p.Interface)): + // Loopback devices are special; we can go ahead and set them + // up the same way each time. In fact, the loopback file only needs + // the value of `Interface` to be set + err = p.UpLoopback() + + case p.Wifi: + // Wifi interfaces get handled via wpa_supplicant + err = p.UpWifi() + + default: + // Any interface left-over must be a wired interface + err = p.UpWired() } + return +} + +// UpWifi brings up a wifi network via wpa_supplicant +func (p Profile) UpWifi() (err error) { + err = fmt.Errorf("bringing up wifi networks has not been implemented yet") + + return +} + +// UpWired brings a wired network connection up +func (p Profile) UpWired() (err error) { for idx, addr := range []Address{ p.IPv4, p.IPv6, diff --git a/netctl/profile_test.go b/netctl/profile_test.go index d543b0d..0afca3f 100644 --- a/netctl/profile_test.go +++ b/netctl/profile_test.go @@ -151,6 +151,11 @@ func TestUp(t *testing.T) { Interface: "lo", } + wifi := Profile{ + Interface: "test0", + Wifi: true, + } + for _, test := range []struct { name string profile Profile @@ -163,6 +168,7 @@ func TestUp(t *testing.T) { {"with dhcp errors", ip4DHCPErr, testNetLinkHandle{}, true}, {"dhcp client errors", ip4DHCPClientErr, testNetLinkHandle{}, true}, {"loopback", loopback, testNetLinkHandle{}, false}, + {"wifi fails, not implemented", wifi, testNetLinkHandle{}, true}, } { t.Run(test.name, func(t *testing.T) { handle = test.handle diff --git a/wifi/wifi.go b/wifi/wifi.go new file mode 100644 index 0000000..a78f864 --- /dev/null +++ b/wifi/wifi.go @@ -0,0 +1,229 @@ +//go:build linux +// +build linux + +package wifi + +import ( + "fmt" + "strconv" + "strings" + + "github.com/vinyl-linux/linux-utils/netctl" + "pifke.org/wpasupplicant" +) + +var ( + networkPath = netctl.DefaultPath +) + +type Networks []Network + +func (ns Networks) String() string { + lines := make([]string, 0) + lines = append(lines, fmt.Sprintf("%s\t\t%s\t%s", "SSID", "Active", "Has Configuration")) + + for _, n := range ns { + lines = append(lines, n.String()) + } + + return strings.Join(lines, "\n") +} + +type Network struct { + SSID string + Type string + Active bool + Configured bool +} + +func (n Network) String() string { + var active, configured string + if n.Active { + active = "*" + } + + if n.Configured { + configured = "*" + } + + return fmt.Sprintf("%q\t\t%s\t\t%s", n.SSID, active, configured) +} + +type Wifi struct { + conn wpasupplicant.Conn +} + +func New() (w Wifi, err error) { + n, err := netctl.New(networkPath) + if err != nil { + return + } + + foundCount := 0 + iface := "" + for _, p := range n.Profiles { + if p.Wifi { + iface = p.Interface + foundCount++ + } + } + + switch foundCount { + case 0: + err = fmt.Errorf("no wireless interfaces configured") + + case 1: + // nop - expected case + + default: + err = fmt.Errorf("%d wireless interfaces configured, 1 expected", foundCount) + } + + if err != nil { + return + } + + w.conn, err = wpasupplicant.Unixgram(iface) + + return +} + +// List returns a list of the networks visible to the nic +func (w Wifi) List() (nets Networks, err error) { + err = w.conn.Scan() + if err != nil { + return + } + + conns, err := w.conn.ListNetworks() + if err != nil { + return + } + + status, err := w.conn.Status() + if err != nil { + return + } + + curr := status.SSID() + + scanResults, errs := w.conn.ScanResults() + if len(errs) != 0 { + err = flattenErrs(errs) + + return + } + + nets = make(Networks, len(scanResults)) + + for i, n := range scanResults { + ssid := n.SSID() + + nets[i] = Network{ + SSID: ssid, + Active: ssid == curr, + Configured: contains(ssid, conns), + } + } + + return +} + +// Create takes an SSID, pre-shared key, and creates a connection +func (w Wifi) Create(ssid, psk string) (err error) { + id, err := w.conn.AddNetwork() + if err != nil { + return + } + + err = w.conn.SetNetwork(id, "ssid", ssid) + if err != nil { + return + } + + return w.conn.SetNetwork(id, "psk", psk) +} + +// Connect will connect to an SSID +func (w Wifi) Connect(ssid string) (err error) { + nets, err := w.conn.ListNetworks() + if err != nil { + return + } + + var id int + for _, n := range nets { + if ssid == n.SSID() { + id, err = strconv.Atoi(n.NetworkID()) + if err != nil { + return + } + + return w.conn.SelectNetwork(id) + } + } + + return fmt.Errorf("ssid %s not found", ssid) +} + +// Disconnect will disconnect from all wifi networks +func (w Wifi) Disconnect() (err error) { + status, err := w.conn.Status() + if err != nil { + return + } + + if status.WPAState() != "COMPLETED" { + return + } + + nets, err := w.conn.ListNetworks() + if err != nil { + return + } + + var id int + for _, n := range nets { + if n.SSID() != status.SSID() { + continue + } + + id, err = strconv.Atoi(n.NetworkID()) + if err != nil { + return + } + + return w.conn.DisableNetwork(id) + } + + return fmt.Errorf("not connected to anything, nothing to disconnect") +} + +// Save will save wifi config. +// +// It exists outside of, say, the create function because in some contexts +// we may not want to persist config. For instance: we may store keys and +// details externally (say with vault, or on an external disk). +func (w Wifi) Save() error { + return w.conn.SaveConfig() +} + +func contains(ssid string, cn []wpasupplicant.ConfiguredNetwork) bool { + for _, n := range cn { + if ssid == n.SSID() { + return true + } + } + + return false +} + +func flattenErrs(errs []error) (err error) { + err = fmt.Errorf("error(s): ") + + for _, e := range errs { + err = fmt.Errorf("%w %s", err, e.Error()) + } + + return +}