Add autogen for peers
This commit is contained in:
parent
99b6b1577b
commit
1bd241e745
6 changed files with 1061 additions and 89 deletions
102
README.md
Normal file
102
README.md
Normal 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
|
@ -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 {
|
||||||
|
|
|
@ -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]
|
||||||
|
|
64
main.go
64
main.go
|
@ -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 {
|
||||||
|
@ -156,7 +157,7 @@ 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",
|
||||||
|
|
73
server.go
73
server.go
|
@ -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()
|
||||||
|
|
Loading…
Reference in a new issue