From 841f3a16e376d667586847d04a54c333bc5dae57 Mon Sep 17 00:00:00 2001 From: Ambrose Chua Date: Sun, 22 Dec 2019 17:11:40 +0800 Subject: [PATCH] Initial working client --- cmd/request.go | 128 ++++++++++++++++++++++++++++++++++++++++++++----- cmd/server.go | 25 ++++------ lib/client.go | 32 +++++++++++-- lib/types.go | 9 ++++ 4 files changed, 161 insertions(+), 33 deletions(-) create mode 100644 lib/types.go diff --git a/cmd/request.go b/cmd/request.go index 71f449e..011397b 100644 --- a/cmd/request.go +++ b/cmd/request.go @@ -1,12 +1,18 @@ package cmd import ( - "log" + "fmt" + "os" + "os/exec" + "text/template" "github.com/serverwentdown/wireguard-negotiator/lib" "github.com/urfave/cli/v2" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" ) +var ErrTypeNotValid = fmt.Errorf("network interface backend type not valid") + var CmdRequest = &cli.Command{ Name: "request", Usage: "Set up local WireGuard", @@ -29,8 +35,8 @@ var CmdRequest = &cli.Command{ Name: "networkd", Aliases: []string{"n"}, Value: "", - DefaultText: "/etc/systemd/network/.netdev", - Usage: "Path to save a networkd configuration file", + DefaultText: "/etc/systemd/network/", + Usage: "Path to save networkd configuration. Appends .netdev and .network extensions", }, &cli.StringFlag{ Name: "type", @@ -56,22 +62,118 @@ var CmdRequest = &cli.Command{ func runRequest(ctx *cli.Context) error { inter := ctx.String("interface") netBackend := ctx.String("type") - noneConfig := ctx.String("none") - if !ctx.IsSet("none") { - noneConfig = "/etc/wireguard/" + inter + ".conf" - } + /* + noneConfig := ctx.String("none") + if !ctx.IsSet("none") { + noneConfig = "/etc/wireguard/" + inter + ".conf" + } + */ networkdConfig := ctx.String("networkd") if !ctx.IsSet("networkd") { - networkdConfig = "/etc/systemd/network/" + inter + ".netdev" + networkdConfig = "/etc/systemd/network/" + inter } client := lib.NewClient(ctx.String("server"), ctx.Bool("insecure")) - log.Println(inter) - log.Println(netBackend) - log.Println(noneConfig) - log.Println(networkdConfig) - log.Println(client) + // Generate the private key and public key + privateKey, err := wgtypes.GeneratePrivateKey() + if err != nil { + return err + } + publicKey := privateKey.PublicKey() + + // Ensure that given files can be opened + var netdevFile, networkFile *os.File + switch netBackend { + case "networkd": + netdevFile, err = os.OpenFile(networkdConfig+".netdev", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) + if err != nil { + return fmt.Errorf("opening %s failed: %w", networkdConfig+".netdev", err) + } + networkFile, err = os.OpenFile(networkdConfig+".network", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644) + if err != nil { + return fmt.Errorf("opening %s failed: %w", networkdConfig+".network", err) + } + default: + return fmt.Errorf("%w: %s", ErrTypeNotValid, netBackend) + } + + // Perform the request + peerConfigResponse, err := client.Request(publicKey.String()) + if err != nil { + return err + } + config := interfaceAndPeerConfig{ + peerConfigResponse, + privateKey.String(), + inter, + } + + // Generate configuration + switch netBackend { + case "networkd": + err = configureNetworkd(config, netdevFile, networkFile) + if err != nil { + return err + } + default: + return fmt.Errorf("%w: %s", ErrTypeNotValid, netBackend) + } return nil } + +type interfaceAndPeerConfig struct { + lib.PeerConfigResponse + PrivateKey string + InterfaceName string +} + +const networkdNetdevTemplate = ` +[NetDev] +Name = {{.InterfaceName}} +Kind = wireguard +Description = WireGuard {{.InterfaceName}} generated with wireguard-negotiator + +[WireGuard] +PrivateKey = {{.PrivateKey}} + +[WireGuardPeer] +PublicKey = {{.PublicKey}} +AllowedIPs = {{range $i, $a := .AllowedIPs}}{{if gt $i 0}}, {{end}}{{.}}{{end}} +Endpoint = {{.Endpoint}} +PersistentKeepalive = {{.PersistentKeepalive}} +` + +const networkdNetworkTemplate = ` +[Match] +Name = {{.InterfaceName}} + +[Network] +{{range $i, $a := .InterfaceIPs}} +Address = {{.}} +{{end}} +` + +func configureNetworkd(config interfaceAndPeerConfig, netdevFile *os.File, networkFile *os.File) error { + // For ease of maintenance, just render a textual template + netdevTemplate := template.Must(template.New("networkd-netdev").Parse(networkdNetdevTemplate)) + networkTemplate := template.Must(template.New("networkd-network").Parse(networkdNetworkTemplate)) + + err := netdevTemplate.Execute(netdevFile, config) + if err != nil { + return fmt.Errorf("netdev template: %w", err) + } + err = networkTemplate.Execute(networkFile, config) + if err != nil { + return fmt.Errorf("network template: %w", err) + } + + // For now, simply run one fixed command to reread from the config file + cmd := exec.Command("systemctl", "restart", "systemd-networkd") + err = cmd.Run() + if err != nil { + return fmt.Errorf("systemctl restart systemd-networkd failed: %w", err) + } + return nil +} diff --git a/cmd/server.go b/cmd/server.go index ef66faa..930db66 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -12,6 +12,7 @@ import ( "os/signal" "syscall" + "github.com/serverwentdown/wireguard-negotiator/lib" "github.com/urfave/cli/v2" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "gopkg.in/ini.v1" @@ -140,13 +141,7 @@ func runServer(ctx *cli.Context) error { IP: interfIPNet.IP.Mask(interfIPNet.Mask), Mask: interfIPNet.Mask, } - resp := struct { - InterfaceIPs []string - AllowedIPs []string - PublicKey string - Endpoint string - PersistentKeepaliveInterval int - }{ + resp := lib.PeerConfigResponse{ []string{ipNet.String()}, []string{netIPNet.String()}, serverPublicKey, @@ -219,16 +214,16 @@ func gater(queue chan request, result chan request) { } func configAddPeer(config string, req request) error { - // For every request, we'll just open the config file again and rewrite it - // We don't need to optimise this because it happens infrequently + // For every request, open the config file again and rewrite it. Acceptable + // because this happens infrequently - // Preferably in the future, we treat the configuration as a database + // Preferably in the future, treat the configuration as a database - // For now, we append to the config file + // For now, append to the config file cfg := ini.Empty() sec, _ := cfg.NewSection("Peer") publicKey := sec.Key("PublicKey") - // TODO: Do we need validation? + // TODO: Validation is needed publicKey.SetValue(req.publicKey) allowedIPs := sec.Key("AllowedIPs") allowedHost := ipToIPNetWithHostMask(req.ip) @@ -261,9 +256,9 @@ func configReadInterfacePublicKey(config string) (string, error) { } func interAddPeer(inter string, req request, config string) error { - // For every request, we also need to dynamically add the peer to the interface + // For every request, dynamically add the peer to the interface - // For now, we simply run one fixed command to reread from the config file + // For now, simply run one fixed command to reread from the config file cmd := exec.Command("wg", "setconf", inter, config) err := cmd.Run() if err != nil { @@ -272,8 +267,6 @@ func interAddPeer(inter string, req request, config string) error { return nil } -// Helpers - func ipToIPNetWithHostMask(ip net.IP) net.IPNet { if ip4 := ip.To4(); ip4 != nil { return net.IPNet{ diff --git a/lib/client.go b/lib/client.go index 0fb0144..18b90c1 100644 --- a/lib/client.go +++ b/lib/client.go @@ -2,11 +2,16 @@ package lib import ( "crypto/tls" + "encoding/json" + "fmt" "net/http" + "net/url" ) +var ErrRequestFailed = fmt.Errorf("request for peer config was not successful") + type Client struct { - ServerURL string + serverURL string httpClient *http.Client } @@ -18,12 +23,31 @@ func NewClient(serverURL string, insecure bool) *Client { } } return &Client{ - ServerURL: serverURL, - // We don't need to set a connection timeout + serverURL: serverURL, + // Client is not used in time or resource sensitive environments, therefore + // omitting timeout reduces code httpClient: &http.Client{}, } } -func (c *Client) Create() { +func (c *Client) Request(publicKey string) (PeerConfigResponse, error) { + peerConfigRequest := url.Values{} + peerConfigRequest.Set("PublicKey", publicKey) + resp, err := c.httpClient.PostForm(c.serverURL+"/request", peerConfigRequest) + if err != nil { + return PeerConfigResponse{}, fmt.Errorf("unable to request: %w", err) + } + if resp.StatusCode != http.StatusOK { + return PeerConfigResponse{}, ErrRequestFailed + } + decoder := json.NewDecoder(resp.Body) + + var peerConfigResponse PeerConfigResponse + err = decoder.Decode(&peerConfigResponse) + if err != nil { + return PeerConfigResponse{}, fmt.Errorf("unable to request: %w", err) + } + + return peerConfigResponse, nil } diff --git a/lib/types.go b/lib/types.go new file mode 100644 index 0000000..c545f4f --- /dev/null +++ b/lib/types.go @@ -0,0 +1,9 @@ +package lib + +type PeerConfigResponse struct { + InterfaceIPs []string + AllowedIPs []string + PublicKey string + Endpoint string + PersistentKeepalive int +}