Initial working client

master
Ambrose Chua 2019-12-22 17:11:40 +08:00
parent 93073c4d8e
commit 841f3a16e3
4 changed files with 161 additions and 33 deletions

View File

@ -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/<interface>.netdev",
Usage: "Path to save a networkd configuration file",
DefaultText: "/etc/systemd/network/<interface>",
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
}

View File

@ -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{

View File

@ -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
}

9
lib/types.go Normal file
View File

@ -0,0 +1,9 @@
package lib
type PeerConfigResponse struct {
InterfaceIPs []string
AllowedIPs []string
PublicKey string
Endpoint string
PersistentKeepalive int
}