Add autogen for peers

This commit is contained in:
Michael Crosby 2019-07-11 11:46:36 +00:00
parent 99b6b1577b
commit 1bd241e745
6 changed files with 1061 additions and 89 deletions

102
README.md Normal file
View file

@ -0,0 +1,102 @@
# guard
A GRPC server for managing wireguard tunnels.
## Status: alpha
## Requirements
Wireguard and it's untilities, `wg`, `wg-quick`, and `wg-quick@.service` must be installed on the system hosting
the `guard` server.
### Create a new tunnel
To create a new tunnel specify the address and the endpoint for the tunnel.
The last argument is used as the tunnel ID and interface name on the server.
```bash
> guard create --address 192.168.5.1/32 --endpoint 127.0.0.1:31000 wg0
{
"id": "wg0",
"private_key": "+EymZwYNHxGVe5T1gmTbwKmQgWksDlZzldTwoZi5lnQ=",
"listen_port": "31000",
"address": "192.168.5.1/32",
"public_key": "irDV3wkkNe6f1GLAPFNGjj0xsQsoxPCNko4Lf3igcjM=",
"endpoint": "127.0.0.1"
}
```
### Delete a tunnel
Delete a tunnel using the tunnel ID
```bash
> guard delete wg0
```
### Create a new peer
To create a new peer and have all the keys generated for you use the `peers new` command.
The peer configuration will be output to `stdout` that you can copy and paste into your client.
```bash
> guard peers --tunnel wg0 new --ip 192.168.5.2/32 --dns 192.168.5.1 --ips 192.168.5.0/24 --ips 192.168.0.1/24 mypeer
[Interface]
PrivateKey = kFJ6VSq+l6sBPaI2DUbEWSVI83Kcfz/yo7WfVheT+FI=
Address = 192.168.5.2/32
DNS = 192.168.5.1
# wg0
[Peer]
PublicKey = irDV3wkkNe6f1GLAPFNGjj0xsQsoxPCNko4Lf3igcjM=
AllowedIPs = 192.168.5.0/24, 192.168.0.1/24
Endpoint = 127.0.0.1:31000
```
### List all tunnels
```bash
> guard list
[
{
"id": "wg0",
"private_key": "+EymZwYNHxGVe5T1gmTbwKmQgWksDlZzldTwoZi5lnQ=",
"listen_port": "31000",
"address": "192.168.5.1/32",
"peers": [
{
"id": "mypeer",
"public_key": "u/eGf6olYeFSH4XoPvOSZJb9swA/qWPAlfSxRBi6Uw8=",
"allowed_ips": [
"192.168.5.2/32"
],
"private_key": "kFJ6VSq+l6sBPaI2DUbEWSVI83Kcfz/yo7WfVheT+FI="
}
],
"public_key": "irDV3wkkNe6f1GLAPFNGjj0xsQsoxPCNko4Lf3igcjM=",
"endpoint": "127.0.0.1"
}
]
```
### Delete a peer by ID
You can remove and update peers using the `peers` commands.
```bash
> guard peers --tunnel wg0 delete mypeer
{
"id": "wg0",
"private_key": "+EymZwYNHxGVe5T1gmTbwKmQgWksDlZzldTwoZi5lnQ=",
"listen_port": "31000",
"address": "192.168.5.1/32",
"public_key": "irDV3wkkNe6f1GLAPFNGjj0xsQsoxPCNko4Lf3igcjM=",
"endpoint": "127.0.0.1"
}
```

File diff suppressed because it is too large Load diff

View file

@ -8,16 +8,24 @@ import "google/protobuf/empty.proto";
option go_package = "github.com/crosbymichael/guard/api/v1;v1"; option go_package = "github.com/crosbymichael/guard/api/v1;v1";
service Wireguard { service Wireguard {
// Create a new tunnel
rpc Create(CreateRequest) returns (TunnelResponse); rpc Create(CreateRequest) returns (TunnelResponse);
// Delete a tunnel
rpc Delete(DeleteRequest) returns (google.protobuf.Empty); rpc Delete(DeleteRequest) returns (google.protobuf.Empty);
// List all tunnels
rpc List(google.protobuf.Empty) returns (ListResponse); rpc List(google.protobuf.Empty) returns (ListResponse);
// NewPeer to the tunnel with gernerated keys
rpc NewPeer(NewPeerRequest) returns (PeerResponse);
// AddPeer to the tunnel
rpc AddPeer(AddPeerRequest) returns (TunnelResponse); rpc AddPeer(AddPeerRequest) returns (TunnelResponse);
// DeletePeer from a tunnel
rpc DeletePeer(DeletePeerRequest) returns (TunnelResponse); rpc DeletePeer(DeletePeerRequest) returns (TunnelResponse);
} }
message CreateRequest { message CreateRequest {
// id of the tunnel
string id = 1 [(gogoproto.customname) = "ID"]; string id = 1 [(gogoproto.customname) = "ID"];
uint32 listen_port = 2; string endpoint = 2;
string address = 3; string address = 3;
} }
@ -25,17 +33,32 @@ message TunnelResponse {
Tunnel tunnel = 1; Tunnel tunnel = 1;
} }
message PeerResponse {
Tunnel tunnel = 1;
Peer peer = 2;
}
message NewPeerRequest {
// id of the tunnel
string id = 1 [(gogoproto.customname) = "ID"];
string peer_id = 2 [(gogoproto.customname) = "PeerID"];
string address = 3;
}
message AddPeerRequest { message AddPeerRequest {
// id of the tunnel
string id = 1 [(gogoproto.customname) = "ID"]; string id = 1 [(gogoproto.customname) = "ID"];
Peer peer = 2; Peer peer = 2;
} }
message DeletePeerRequest { message DeletePeerRequest {
// id of the tunnel
string id = 1 [(gogoproto.customname) = "ID"]; string id = 1 [(gogoproto.customname) = "ID"];
string peer_id = 2 [(gogoproto.customname) = "PeerID"]; string peer_id = 2 [(gogoproto.customname) = "PeerID"];
} }
message DeleteRequest { message DeleteRequest {
// id of the tunnel
string id = 1 [(gogoproto.customname) = "ID"]; string id = 1 [(gogoproto.customname) = "ID"];
} }
@ -46,11 +69,13 @@ message ListResponse {
message Tunnel { message Tunnel {
string id = 1 [(gogoproto.customname) = "ID"]; string id = 1 [(gogoproto.customname) = "ID"];
string private_key = 2; string private_key = 2;
uint32 listen_port = 3; string listen_port = 3;
string address = 4; string address = 4;
string dns = 5 [(gogoproto.customname) = "DNS"]; string dns = 5 [(gogoproto.customname) = "DNS"];
repeated Peer peers = 6; repeated Peer peers = 6;
Masquerade masquerade = 7; Masquerade masquerade = 7;
string public_key = 8;
string endpoint = 9;
} }
message Peer { message Peer {
@ -59,6 +84,7 @@ message Peer {
repeated string allowed_ips = 3 [(gogoproto.customname) = "AllowedIPs"]; repeated string allowed_ips = 3 [(gogoproto.customname) = "AllowedIPs"];
string endpoint = 4; string endpoint = 4;
uint32 persistent_keepalive = 5; uint32 persistent_keepalive = 5;
string private_key = 6;
} }
message Masquerade { message Masquerade {

View file

@ -37,15 +37,13 @@ import (
const confFmt = `[Interface] const confFmt = `[Interface]
PrivateKey = {{.PrivateKey}} PrivateKey = {{.PrivateKey}}
{{if gt .ListenPort 0}}ListenPort = {{.ListenPort}}{{end}} {{if .ListenPort}}ListenPort = {{.ListenPort}}{{end}}
Address = {{.Address}} Address = {{.Address}}
{{if .DNS }}DNS = {{.DNS}}{{end}} {{if .DNS }}DNS = {{.DNS}}{{end}}
{{if .Masquerade}} {{if .Masquerade}}
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o {{.Masquerade.Interface}} -j MASQUERADE PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o {{.Masquerade.Interface}} -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o {{.Masquerade.Interface}} -j MASQUERADE PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o {{.Masquerade.Interface}} -j MASQUERADE
{{end}} {{end}}
{{range $peer := .Peers -}} {{range $peer := .Peers -}}
# {{$peer.ID}} # {{$peer.ID}}
[Peer] [Peer]

68
main.go
View file

@ -136,9 +136,10 @@ var createCommand = cli.Command{
Name: "address,a", Name: "address,a",
Usage: "cidr for the tunnel address", Usage: "cidr for the tunnel address",
}, },
cli.UintFlag{ cli.StringFlag{
Name: "port,p", Name: "endpoint,e",
Usage: "listen port for the tunnel", Usage: "set the endpoint for the tunnel",
Value: "127.0.0.1:31000",
}, },
}, },
Action: func(clix *cli.Context) error { Action: func(clix *cli.Context) error {
@ -154,9 +155,9 @@ var createCommand = cli.Command{
) )
r, err := client.Create(ctx, &v1.CreateRequest{ r, err := client.Create(ctx, &v1.CreateRequest{
ID: clix.Args().First(), ID: clix.Args().First(),
Address: clix.String("address"), Address: clix.String("address"),
ListenPort: uint32(clix.Uint("port")), Endpoint: clix.String("endpoint"),
}) })
if err != nil { if err != nil {
return err return err
@ -204,6 +205,61 @@ var peersCommand = cli.Command{
}, },
}, },
Subcommands: []cli.Command{ Subcommands: []cli.Command{
{
Name: "new",
Description: "create a new peer",
Flags: []cli.Flag{
cli.StringFlag{
Name: "ip,i",
Usage: "ip cidr for the peer",
},
cli.StringFlag{
Name: "dns",
Usage: "dns for the peer",
},
cli.StringSliceFlag{
Name: "ips",
Usage: "allowed ips",
Value: &cli.StringSlice{},
},
},
Action: func(clix *cli.Context) error {
conn, err := grpc.Dial(clix.GlobalString("address"), grpc.WithInsecure())
if err != nil {
return errors.Wrap(err, "dial server")
}
defer conn.Close()
var (
ctx = cancelContext()
client = v1.NewWireguardClient(conn)
)
r, err := client.NewPeer(ctx, &v1.NewPeerRequest{
ID: clix.GlobalString("tunnel"),
PeerID: clix.Args().First(),
Address: clix.String("ip"),
})
if err != nil {
return err
}
// generate peer tunnel config
t := &v1.Tunnel{
PrivateKey: r.Peer.PrivateKey,
Address: r.Peer.AllowedIPs[0],
DNS: clix.String("dns"),
Peers: []*v1.Peer{
{
ID: r.Tunnel.ID,
PublicKey: r.Tunnel.PublicKey,
Endpoint: net.JoinHostPort(r.Tunnel.Endpoint, r.Tunnel.ListenPort),
AllowedIPs: clix.StringSlice("ips"),
},
},
}
return t.Render(os.Stdout)
},
},
{ {
Name: "add", Name: "add",
Description: "add a peer", Description: "add a peer",

View file

@ -31,7 +31,9 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"net"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@ -69,8 +71,12 @@ func (s *server) Create(ctx context.Context, r *v1.CreateRequest) (*v1.TunnelRes
if r.Address == "" { if r.Address == "" {
return nil, errors.New("address cannot be empty") return nil, errors.New("address cannot be empty")
} }
if r.ListenPort == 0 { if r.Endpoint == "" {
return nil, errors.New("listen port cannot be 0") return nil, errors.New("endpoint cannot be empty")
}
host, port, err := net.SplitHostPort(r.Endpoint)
if err != nil {
return nil, errors.Wrap(err, "cannot split endpoint into host and port")
} }
path := filepath.Join(s.dir, r.ID) path := filepath.Join(s.dir, r.ID)
if err := os.Mkdir(path, 0700); err != nil { if err := os.Mkdir(path, 0700); err != nil {
@ -83,11 +89,17 @@ func (s *server) Create(ctx context.Context, r *v1.CreateRequest) (*v1.TunnelRes
if err != nil { if err != nil {
return nil, err return nil, err
} }
pub, err := publicKey(ctx, key)
if err != nil {
return nil, err
}
t := v1.Tunnel{ t := v1.Tunnel{
ID: r.ID, ID: r.ID,
ListenPort: r.ListenPort, ListenPort: port,
Address: r.Address, Address: r.Address,
PrivateKey: key, PrivateKey: key,
PublicKey: pub,
Endpoint: host,
} }
if err := s.saveTunnel(&t); err != nil { if err := s.saveTunnel(&t); err != nil {
@ -109,6 +121,46 @@ func (s *server) Create(ctx context.Context, r *v1.CreateRequest) (*v1.TunnelRes
}, nil }, nil
} }
func (s *server) NewPeer(ctx context.Context, r *v1.NewPeerRequest) (*v1.PeerResponse, error) {
if r.ID == "" {
return nil, errors.New("tunnel id cannot be empty")
}
t, err := s.loadTunnel(r.ID)
if err != nil {
return nil, err
}
peerKey, err := newPrivateKey(ctx)
if err != nil {
return nil, err
}
publicKey, err := publicKey(ctx, peerKey)
if err != nil {
return nil, err
}
peer := &v1.Peer{
ID: r.PeerID,
PublicKey: publicKey,
PrivateKey: peerKey,
AllowedIPs: []string{
r.Address,
},
}
t.Peers = append(t.Peers, peer)
if err := s.saveTunnel(t); err != nil {
return nil, err
}
if err := s.saveConf(t); err != nil {
return nil, err
}
if err := wgquick(ctx, "restart", t.ID); err != nil {
return nil, errors.Wrap(err, "restart tunnel")
}
return &v1.PeerResponse{
Tunnel: t,
Peer: peer,
}, nil
}
func (s *server) AddPeer(ctx context.Context, r *v1.AddPeerRequest) (*v1.TunnelResponse, error) { func (s *server) AddPeer(ctx context.Context, r *v1.AddPeerRequest) (*v1.TunnelResponse, error) {
if r.ID == "" { if r.ID == "" {
return nil, errors.New("tunnel id cannot be empty") return nil, errors.New("tunnel id cannot be empty")
@ -250,11 +302,26 @@ func newPrivateKey(ctx context.Context) (string, error) {
return strings.TrimSuffix(string(data), "\n"), nil return strings.TrimSuffix(string(data), "\n"), nil
} }
func publicKey(ctx context.Context, privateKey string) (string, error) {
r := strings.NewReader(privateKey)
data, err := wireguardData(ctx, r, "pubkey")
if err != nil {
return "", errors.Wrapf(err, "%s", data)
}
return strings.TrimSuffix(string(data), "\n"), nil
}
func wireguard(ctx context.Context, args ...string) ([]byte, error) { func wireguard(ctx context.Context, args ...string) ([]byte, error) {
cmd := exec.CommandContext(ctx, "wg", args...) cmd := exec.CommandContext(ctx, "wg", args...)
return cmd.CombinedOutput() return cmd.CombinedOutput()
} }
func wireguardData(ctx context.Context, r io.Reader, args ...string) ([]byte, error) {
cmd := exec.CommandContext(ctx, "wg", args...)
cmd.Stdin = r
return cmd.CombinedOutput()
}
func wgquick(ctx context.Context, action, name string) error { func wgquick(ctx context.Context, action, name string) error {
cmd := exec.CommandContext(ctx, "systemctl", action, fmt.Sprintf("wg-quick@%s", name)) cmd := exec.CommandContext(ctx, "systemctl", action, fmt.Sprintf("wg-quick@%s", name))
out, err := cmd.CombinedOutput() out, err := cmd.CombinedOutput()