diff --git a/README.md b/README.md index d50846d..4bf2ad0 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ In summary: * Manage "client" keys * Exchange keys over HTTP(S) -* Exchange IP addressing (DHCP-like) +* Exchange IP addressing * Manually gate new peers * Sets up network interface on the "client" * Generate Ansible INI inventory @@ -27,3 +27,44 @@ The primary scenario this tool is going to be used for is to manage machines usi # Usage > TODO + +# Operation + +## Server + +The "server" manages a WireGuard interface, treating a WireGuard configuration file as a database. It assumes this interface and configuration exists. It only adds new peers to the configuration file and interface, and does not delete existing configuration. + +The "server" also exposes the HTTP server with the following endpoints: + +### `POST /request` + +Request for the assignment of an IP address and accepted as a peer. This blocks until the server has finished configuring the peer, therefore the client SHOULD NOT timeout. + +#### Request Body + +Content-Type: application/x-www-form-urlencoded + +| Name | Description | Required | +|------|-------------|----------| +| PublicKey | The public key of the "client" peer | X | + +#### Response Body + +Content-Type: application/json + +| Name | Type | Description | +|------|------|-------------| +| PublicKey | String | Base64 encoded public key of the "server" peer | +| Endpoint | String | The endpoint of the "server" peer | +| PersistentKeepaliveInterval | Number | Suggests a PersistentKeepaliveInterval | +| AllowedIPs | []String | List of allowed IP addresses in CIDR notation | +| InterfaceIPs | []String | List of IP addresses assigned to the "client" interface | + +## Client + +The "client" sets up a WireGuard interface, and relies on network backends to do so. *It should not be run more than once*. The following network backends are supported: + +- (Not implemented) `none`: Creates an interface and WireGuard configuration file +- `networkd`: Creates a `systemd.netdev` file in `/etc/systemd/network` + +It does so by performing `POST /request` to the "server". diff --git a/cmd/request.go b/cmd/request.go index ae2998a..71f449e 100644 --- a/cmd/request.go +++ b/cmd/request.go @@ -19,33 +19,58 @@ var CmdRequest = &cli.Command{ Usage: "Name for new WireGuard interface", }, &cli.StringFlag{ - Name: "config", + Name: "none", Aliases: []string{"c"}, Value: "", DefaultText: "/etc/wireguard/.conf", - Usage: "Path to the WireGuard configuration file", + Usage: "Path to save a WireGuard configuration file", }, &cli.StringFlag{ - Name: "type", - Value: "none", - Usage: "Select network interface backend. Currently only none and networkd are implemented", + Name: "networkd", + Aliases: []string{"n"}, + Value: "", + DefaultText: "/etc/systemd/network/.netdev", + Usage: "Path to save a networkd configuration file", + }, + &cli.StringFlag{ + Name: "type", + Aliases: []string{"t"}, + Value: "networkd", + Usage: "Select network interface backend. Currently only networkd is implemented", + }, + &cli.StringFlag{ + Name: "server", + Aliases: []string{"s"}, + Usage: "wireguard-negotiator server URL", + Required: true, + EnvVars: []string{"WGN_SERVER_URL"}, + }, + &cli.BoolFlag{ + Name: "insecure", + Usage: "Disable TLS verification", + EnvVars: []string{"WGN_SERVER_INSECURE"}, }, }, } func runRequest(ctx *cli.Context) error { inter := ctx.String("interface") - config := ctx.String("config") - if !ctx.IsSet("config") { - config = "/etc/wireguard/" + inter + ".conf" - } netBackend := ctx.String("type") + 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" + } client := lib.NewClient(ctx.String("server"), ctx.Bool("insecure")) log.Println(inter) - log.Println(config) log.Println(netBackend) + log.Println(noneConfig) + log.Println(networkdConfig) log.Println(client) return nil diff --git a/cmd/server.go b/cmd/server.go index beeb8c5..051dfd6 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -1,16 +1,23 @@ package cmd import ( + "encoding/json" + "fmt" "log" "net" "net/http" "os" "os/exec" + "os/signal" + "syscall" "github.com/urfave/cli/v2" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "gopkg.in/ini.v1" ) +var ErrNoAddressesFound = fmt.Errorf("No address found on the interface") + var CmdServer = &cli.Command{ Name: "server", Usage: "Start the wireguard-negotiator server", @@ -26,13 +33,14 @@ var CmdServer = &cli.Command{ Aliases: []string{"c"}, Value: "", DefaultText: "/etc/wireguard/.conf", - Usage: "Path to the WireGuard configuration file", + Usage: "Path to the existing WireGuard configuration file", }, &cli.StringFlag{ - Name: "endpoint", - Aliases: []string{"e"}, - Value: "", - Usage: "Set the endpoint address", + Name: "endpoint", + Aliases: []string{"e"}, + Value: "", + Required: true, + Usage: "Set the endpoint address", }, &cli.StringFlag{ Name: "listen", @@ -51,33 +59,37 @@ type request struct { func runServer(ctx *cli.Context) error { inter := ctx.String("interface") - interf, err := net.InterfaceByName(inter) - if err != nil { - log.Fatal(err) - } config := ctx.String("config") if !ctx.IsSet("config") { config = "/etc/wireguard/" + inter + ".conf" } endpoint := ctx.String("endpoint") - if !ctx.IsSet("endpoint") { - log.Fatal("Please specify endpoint with -endpoint") - } listen := ctx.String("listen") - // Obtain the server's public key - serverPublicKey := configReadInterfacePublicKey(config) + // Obtain the network interface + interf, err := net.InterfaceByName(inter) + if err != nil { + return err + } + // Obtain the server's public key + serverPublicKey, err := configReadInterfacePublicKey(config) + if err != nil { + return err + } + + // TODO: Define this allocation method + // TODO: Include allocation behaviour in README terribleCounterThatShouldNotExist := 1 interfAddrs, err := interf.Addrs() if err != nil { - log.Fatal(err) + return err } - // TODO: Define this allocation method - // TODO: Include allocation behaviour in README + + // Obtain interface address for use in allocation var interfIPNet *net.IPNet if len(interfAddrs) < 1 { - log.Fatal("No address found on the interface") + return ErrNoAddressesFound } _, interfIPNet, err = net.ParseCIDR(interfAddrs[0].String()) @@ -90,62 +102,112 @@ func runServer(ctx *cli.Context) error { // TODO: Rate limiting http.HandleFunc("/request", func(w http.ResponseWriter, r *http.Request) { - publicKey := r.PostFormValue("public_key") - // TODO: Ensure public key is new - // Assign an IP address - terribleCounterThatShouldNotExist += 1 - ip := incrementIP(interfIPNet.IP, terribleCounterThatShouldNotExist) - if !interfIPNet.Contains(ip) { - log.Fatal("Ran out of IP addresses to allocate") - } - // Enqueue request into the gate - req := request{ - ip: ip, - publicKey: publicKey, - } - // Wait for flush of configuration - gateQueue <- req - // Produce configuration to client - ipNet := &net.IPNet{ - IP: ip, - Mask: interfIPNet.Mask, - } - resp := struct { - interfaceIP string `json:"interface_ip"` - peerAllowedIPs string `json:"peer_allowed_ips"` - peerPublicKey string `json:"peer_public_key"` - peerEndpoint string `json:"peer_endpoint"` - }{ - ipNet.String(), - interfIPNet.IP.Mask(interfIPNet.Mask), - "", - "", + switch r.Method { + case "POST": + publicKey := r.PostFormValue("PublicKey") + // TODO: Ensure public key is new + if len(publicKey) == 0 { + w.WriteHeader(400) + return + } + + // Assign an IP address + terribleCounterThatShouldNotExist += 1 + ip := incrementIP(interfIPNet.IP, terribleCounterThatShouldNotExist) + if !interfIPNet.Contains(ip) { + log.Println("WARNING: Ran out of addresses to allocate") + w.WriteHeader(500) + return + } + + // Enqueue request into the gate + req := request{ + ip: ip, + publicKey: publicKey, + } + + // Wait for flush of configuration + gateQueue <- req + + // Produce configuration to client + ipNet := &net.IPNet{ + IP: ip, + Mask: interfIPNet.Mask, + } + resp := struct { + InterfaceIPs string + AllowedIPs string + PublicKey string + Endpoint string + PersistentKeepaliveInterval int + }{ + ipNet.String(), + interfIPNet.IP.Mask(interfIPNet.Mask).String(), + serverPublicKey, + endpoint, + 25, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(resp) + default: + w.WriteHeader(405) } }) - http.ListenAndServe(listen, nil) + // Shutdown notifier + go func() { + sigint := make(chan os.Signal, 1) + signal.Notify(sigint, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + <-sigint + close(gateQueue) + close(addQueue) + /* + if err := http.Shutdown(context.Background()); err != nil { + log.Printf("Server shutdown error: %v\n", err) + } + */ + }() - return nil + return http.ListenAndServe(listen, nil) } func adder(queue chan request, inter string, config string) { // Write requests to config and add peer - for req := range queue { - configAddPeer(config, req) - interAddPeer(inter, req, config) + for { + select { + case req, ok := <-queue: + if !ok { + break + } + err := configAddPeer(config, req) + if err != nil { + log.Println(err) + } + err = interAddPeer(inter, req, config) + if err != nil { + log.Println(err) + } + } } } func gater(queue chan request, result chan request) { // Receive requests and prompt the admin - for req := range queue { - // For now, accept all - log.Println(req) - result <- req + for { + select { + case req, ok := <-queue: + if !ok { + break + } + // For now, accept all + log.Println(req.ip.String(), req.publicKey) + result <- req + } } } -func configAddPeer(config string, req 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 @@ -159,36 +221,44 @@ func configAddPeer(config string, req request) { publicKey.SetValue(req.publicKey) allowedIPs := sec.Key("AllowedIPs") allowedHost := ipToIPNetWithHostMask(req.ip) - allowedIPs.AddShadow((&allowedHost).String()) + allowedIPs.SetValue((&allowedHost).String()) f, err := os.OpenFile(config, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { - log.Fatal(err) + return fmt.Errorf("opening %s failed: %w", config, err) } _, err = cfg.WriteTo(f) if err != nil { - log.Fatal(err) + return fmt.Errorf("writing to %s failed: %w", config, err) } + return nil } -func configReadInterfacePublicKey(config string) string { +func configReadInterfacePublicKey(config string) (string, error) { cfg, err := ini.Load(config) if err != nil { - log.Fatal("Failed to read interface public key") + return "", fmt.Errorf("read interface public key failed: %w", err) } - return cfg.Section("Interface").Key("PrivateKey") + b64PrivateKey := cfg.Section("Interface").Key("PrivateKey").String() + wgPrivateKey, err := wgtypes.ParseKey(b64PrivateKey) + if err != nil { + return "", fmt.Errorf("read interface public key failed: %w", err) + } + wgPublicKey := wgPrivateKey.PublicKey() + return wgPublicKey.String(), nil } -func interAddPeer(inter string, req request, config string) { +func interAddPeer(inter string, req request, config string) error { // For every request, we also need to dynamically add the peer to the interface // For now, we simply run one fixed command to reread from the config file cmd := exec.Command("wg", "setconf", inter, config) err := cmd.Run() if err != nil { - log.Println(err) + return fmt.Errorf("wq setconf failed: %w", err) } + return nil } // Helpers @@ -206,11 +276,19 @@ func ipToIPNetWithHostMask(ip net.IP) net.IPNet { } } -func incrementIP(ip net.IP) net.IP { +func incrementIP(ip net.IP, inc int) net.IP { + result := make(net.IP, len(ip)) + copy(result, ip) + for i := len(ip) - 1; i >= 0; i-- { - ip[i]++ - if ip[i] != 0 { - break + remainder := inc % 256 + overflow := int(result[i])+remainder > 255 + + result[i] += byte(remainder) + if overflow { + inc += 256 } + inc /= 256 } + return result } diff --git a/go.mod b/go.mod index b0d71c9..e730cf4 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,6 @@ go 1.13 require ( github.com/urfave/cli/v2 v2.0.0 + golang.zx2c4.com/wireguard/wgctrl v0.0.0-20191219145116-fa6499c8e75f gopkg.in/ini.v1 v1.51.0 ) diff --git a/go.sum b/go.sum index 3e76cf3..b217b8a 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,13 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= +github.com/mdlayher/genetlink v0.0.0-20191205172946-651acf4b47ef/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc= +github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= +github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= +github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -9,6 +16,31 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo= github.com/urfave/cli/v2 v2.0.0 h1:+HU9SCbu8GnEUFtIBfuUNXN39ofWViIEJIp6SURMpCg= github.com/urfave/cli/v2 v2.0.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +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-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191003212358-c178f38b412c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191218084908-4a24b4065292/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.zx2c4.com/wireguard v0.0.20191012 h1:sdX+y3hrHkW8KJkjY7ZgzpT5Tqo8XnBkH55U1klphko= +golang.zx2c4.com/wireguard v0.0.20191012/go.mod h1:P2HsVp8SKwZEufsnezXZA4GRX/T49/HlU7DGuelXsU4= +golang.zx2c4.com/wireguard/wgctrl v0.0.0-20191219145116-fa6499c8e75f h1:xc7xbwx/flY4HCaTpfgGqvsEMELJ5EBF82MP2lvfdbo= +golang.zx2c4.com/wireguard/wgctrl v0.0.0-20191219145116-fa6499c8e75f/go.mod h1:QKgTDEXdhtb9dg1EdxK63hefKjD1e+bSXUbRmZBfCSw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= diff --git a/lib/wgconfig.go b/lib/wgconfig.go new file mode 100644 index 0000000..182083e --- /dev/null +++ b/lib/wgconfig.go @@ -0,0 +1,7 @@ +package lib + +import ( +//"golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +// ReadConfig is yet another INI-like configuration file parser, but for WireGuard diff --git a/lib/wgconfig_test.go b/lib/wgconfig_test.go new file mode 100644 index 0000000..ea0869f --- /dev/null +++ b/lib/wgconfig_test.go @@ -0,0 +1,23 @@ +package lib + +import "testing" + +var testGoodConfiguration1 = ` +# Test = Example comment + +[Interface] +# Test comment 2 +ListenPort = 3333 +PrivateKey = MITUgapB4QfRFF54ITXL3TaiYiSsVYkchqfjAXjxM10= +[Peer] +PublicKey = pjFx72IjbMh84SH1nq8Qfbl7HD5mSScHXCV1eISR7lk= +AllowedIPs = 192.168.10.2/32, 2001:470:ed5d:a::2/128 + +[Peer] +AllowedIPs = 192.168.10.40/32, 2001:470:ed5d:a::28/128 +PublicKey = wXU+vSTdEoIwSi+Tmv35SCOFg17wCAwnmYxeQPpbzDg= +` + +func TestReadConfig(t *testing.T) { + +} diff --git a/main.go b/main.go index b6cd3f9..f41b22b 100644 --- a/main.go +++ b/main.go @@ -13,19 +13,7 @@ func main() { app := &cli.App{ Name: "wireguard-negotiator", Usage: "Exchange WireGuard keys over HTTP(S)", - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "server", - Aliases: []string{"s"}, - Usage: "wireguard-negotiator server URL", - EnvVars: []string{"WGN_SERVER_URL"}, - }, - &cli.BoolFlag{ - Name: "insecure", - Usage: "Disable TLS verification", - EnvVars: []string{"WGN_SERVER_INSECURE"}, - }, - }, + Flags: []cli.Flag{}, Commands: []*cli.Command{ cmd.CmdServer, cmd.CmdList,