feat: Add, Check, Get, and Delete peers

This commit is contained in:
HeshamTB 2024-03-14 01:46:05 +03:00
parent 4a1039e5b1
commit eb97d49d1f
8 changed files with 291 additions and 67 deletions

View File

@ -6,6 +6,7 @@ import (
"net" "net"
"net/http" "net/http"
"net/netip" "net/netip"
"net/url"
"os" "os"
"os/signal" "os/signal"
"time" "time"
@ -58,8 +59,9 @@ func run(ctx *cli.Context) {
slog.Debug("Starting run()") slog.Debug("Starting run()")
apiMux := http.NewServeMux() apiMux := http.NewServeMux()
apiMux.HandleFunc("GET /peer", hvpnnode3.HandleGetPeer(wgLink)) apiMux.HandleFunc("GET /peer/{pubkey}", hvpnnode3.HandleGetPeer(wgLink))
apiMux.HandleFunc("POST /peer", hvpnnode3.HandlePostPeer(IPPool, wgLink)) apiMux.HandleFunc("POST /peer", hvpnnode3.HandlePostPeer(wgLink))
apiMux.HandleFunc("DELETE /peer/{pubkey}", hvpnnode3.HandleDeletePeer(wgLink))
apiMux.HandleFunc("GET /peers", hvpnnode3.HandleGetPeers(wgLink)) apiMux.HandleFunc("GET /peers", hvpnnode3.HandleGetPeers(wgLink))
var handler http.Handler = apiMux var handler http.Handler = apiMux
@ -262,6 +264,7 @@ func setup() error {
slog.Debug("main.testVip: Test IP Freed") slog.Debug("main.testVip: Test IP Freed")
IPPool = ipPool IPPool = ipPool
wgLink.IPPool = ipPool
//defer wgLink.Close() //defer wgLink.Close()
c := make(chan os.Signal, 1) c := make(chan os.Signal, 1)
@ -289,7 +292,10 @@ func testWgPeerAdd(wgLink *hvpnnode3.WGLink) error {
} }
publicKey := privateKey.PublicKey() publicKey := privateKey.PublicKey()
ip, err := IPPool.Allocate() urlsafe := url.QueryEscape(publicKey.String())
slog.Debug(urlsafe)
ip, err := wgLink.Allocate()
if err != nil { if err != nil {
return err return err
} }
@ -323,7 +329,7 @@ func testWgPeerAdd(wgLink *hvpnnode3.WGLink) error {
return err return err
} }
slog.Debug("Removed test peer") slog.Debug("Removed test peer")
IPPool.Free(ip) wgLink.Free(ip)
slog.Debug("Freed test peer ip") slog.Debug("Freed test peer ip")

1
go.mod
View File

@ -4,6 +4,7 @@ go 1.22.0
require ( require (
github.com/felixge/httpsnoop v1.0.4 github.com/felixge/httpsnoop v1.0.4
github.com/google/uuid v1.6.0
github.com/urfave/cli/v2 v2.27.1 github.com/urfave/cli/v2 v2.27.1
github.com/vishvananda/netlink v1.1.0 github.com/vishvananda/netlink v1.1.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6

2
go.sum
View File

@ -4,6 +4,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=

View File

@ -1,29 +1,51 @@
package hvpnnode3 package hvpnnode3
import ( import (
"context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"net"
"net/http" "net/http"
"github.com/felixge/httpsnoop" "github.com/felixge/httpsnoop"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/google/uuid"
"gitea.hbanafa.com/HeshamTB/hvpn-node3/proto" "gitea.hbanafa.com/HeshamTB/hvpn-node3/proto"
) )
type CtxKey string
const CtxReqID CtxKey = "request_id"
func HandleGetPeer(wgLink *WGLink) http.HandlerFunc { func HandleGetPeer(wgLink *WGLink) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotImplemented) reqID := r.Context().Value(CtxReqID).(uuid.UUID)
slog.Info("GET Peer is not implemented") pubkey := r.PathValue("pubkey")
debug(pubkey, reqID)
peer, err := wgLink.GetPeer(pubkey)
if err != nil {
if errors.Is(err, proto.PeerDoesNotExist){
json.NewEncoder(w).Encode(
proto.ErrJSONResponse{Message: "Peer does not exist"},
)
w.WriteHeader(http.StatusNotFound)
return
}
w.WriteHeader(http.StatusInternalServerError)
return
}
err = json.NewEncoder(w).Encode(proto.WgPeerToPeer(*peer))
if err != nil {
slog.Error(err.Error())
}
} }
} }
func HandleGetPeers(wgLink *WGLink) http.HandlerFunc { func HandleGetPeers(wgLink *WGLink) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
reqID := r.Context().Value(CtxReqID).(uuid.UUID)
debug("GET Peers for", reqID)
dev, err := wgLink.Device(wgLink.Name) dev, err := wgLink.Device(wgLink.Name)
if err != nil { if err != nil {
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
@ -32,13 +54,7 @@ func HandleGetPeers(wgLink *WGLink) http.HandlerFunc {
} }
var peers []proto.Peer var peers []proto.Peer
for _, peer := range dev.Peers { for _, peer := range dev.Peers {
p := proto.Peer{ p := proto.WgPeerToPeer(peer)
Address: peer.AllowedIPs[0].IP,
PublicKey: peer.PublicKey.String(),
PersistentKeepalive: peer.PersistentKeepaliveInterval,
TX: peer.TransmitBytes,
RX: peer.ReceiveBytes,
}
peers = append(peers, p) peers = append(peers, p)
} }
err = json.NewEncoder(w).Encode(peers) err = json.NewEncoder(w).Encode(peers)
@ -48,8 +64,11 @@ func HandleGetPeers(wgLink *WGLink) http.HandlerFunc {
return return
} }
} }
func HandlePostPeer(IPPool IPPool, wgLink *WGLink) http.HandlerFunc {
func HandlePostPeer(wgLink *WGLink) http.HandlerFunc {
return func(w http.ResponseWriter, r* http.Request) { return func(w http.ResponseWriter, r* http.Request) {
reqID := r.Context().Value(CtxReqID).(uuid.UUID)
debug("POST Peer for", reqID)
peerRequest := proto.CreatePeerRequest{} peerRequest := proto.CreatePeerRequest{}
err := json.NewDecoder(r.Body).Decode(&peerRequest) err := json.NewDecoder(r.Body).Decode(&peerRequest)
if err != nil { if err != nil {
@ -59,79 +78,105 @@ func HandlePostPeer(IPPool IPPool, wgLink *WGLink) http.HandlerFunc {
) )
return return
} }
// TODO: Check if pubkey is already registered
dev, err := wgLink.Device(wgLink.Name) if valid := ValidKey(peerRequest.PublicKey); !valid {
if err != nil { w.WriteHeader(http.StatusBadRequest)
slog.Error(err.Error()) json.NewEncoder(w).Encode(
w.WriteHeader(http.StatusInternalServerError) proto.ErrJSONResponse{Message: "Invalid public key"},
)
return return
} }
for _, peer := range dev.Peers { exists, err := wgLink.Exists(peerRequest.PublicKey)
if peer.PublicKey.String() == peerRequest.PublicKey { if err != nil {
w.WriteHeader(http.StatusAccepted) w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(
proto.ErrJSONResponse{Message: "Error while checking peer existance"},
)
return
}
if exists {
debugf("Peer %s already exists", reqID, peerRequest.PublicKey)
w.WriteHeader(http.StatusFound)
peer, err := wgLink.GetPeer(peerRequest.PublicKey)
if err != nil {
slog.Error(err.Error())
return return
} }
} json.NewEncoder(w).Encode(
proto.WgPeerToPeer(*peer),
newIP, err := IPPool.Allocate() )
if err != nil {
slog.Error(err.Error())
w.WriteHeader(http.StatusInternalServerError)
return return
} }
// TODO: Add peer to wg device peer, err := wgLink.AddPeer(peerRequest.PublicKey)
// TODO: Declare subnet mask for the service
pkey, _ := wgtypes.GeneratePrivateKey()
newPeer := wgtypes.PeerConfig{
PublicKey: pkey.PublicKey(),
ReplaceAllowedIPs: true,
AllowedIPs: []net.IPNet{
{
IP: newIP,
Mask: net.IPv4Mask(255, 255, 255, 255),
},
},
}
err = wgLink.ConfigureDevice(
wgLink.Name,
wgtypes.Config{
Peers: []wgtypes.PeerConfig{
newPeer,
},
ReplacePeers: false,
},
)
if err != nil { if err != nil {
slog.Error(err.Error()) slog.Error(err.Error())
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(
proto.ErrJSONResponse{Message: "Error while adding peer"},
)
return return
} }
debugf("Allocated IP: %s", reqID, peer.AllowedIPs[0].IP.String())
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode( json.NewEncoder(w).Encode(
&proto.Peer{ proto.WgPeerToPeer(*peer),
Address: newIP,
MTU: 1384,
PublicKey: peerRequest.PublicKey,
Endpoint: "vpn.test.com:8487",
AllowedIPs: net.ParseIP("0.0.0.0/0"),
PersistentKeepalive: 25,
},
) )
} }
} }
func HandleDeletePeer(wg *WGLink) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
reqId := r.Context().Value(CtxReqID).(uuid.UUID)
pubkey := r.PathValue("pubkey")
debugf("DELETE Peer %s", reqId, pubkey)
exists, err := wg.Exists(pubkey)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
slog.Error(err.Error())
return
}
if !exists {
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(
proto.ErrJSONResponse{Message: "Peer does not exist"},
)
return
}
err = wg.DeletePeer(pubkey)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(
proto.ErrJSONResponse{Message: "Error while deleting peer"},
)
slog.Error(err.Error())
}
}
}
func HttpLogHandler2(h http.Handler) http.Handler { func HttpLogHandler2(h http.Handler) http.Handler {
// https://blog.kowalczyk.info/article/e00e89c3841e4f8c8c769a78b8a90b47/logging-http-requests-in-go.html // https://blog.kowalczyk.info/article/e00e89c3841e4f8c8c769a78b8a90b47/logging-http-requests-in-go.html
fn := func(w http.ResponseWriter, r* http.Request) { fn := func(w http.ResponseWriter, r* http.Request) {
reqID, _ := uuid.NewRandom()
rCtx := context.WithValue(r.Context(), CtxReqID, reqID)
r = r.WithContext(rCtx)
slog.Info(fmt.Sprintf("Starting request with ID: %s", reqID.String()))
m := httpsnoop.CaptureMetrics(h, w, r) m := httpsnoop.CaptureMetrics(h, w, r)
msg := fmt.Sprintf( msg := fmt.Sprintf(
"%s %s %s %d %s", r.RemoteAddr, r.Method, "[HTTP] %s %s %s %d %s %s", r.RemoteAddr, r.Method,
r.URL.String(),m.Code, m.Duration) r.URL.String(),m.Code, m.Duration, reqID.String())
slog.Info(msg) slog.Info(msg)
} }
return http.HandlerFunc(fn) return http.HandlerFunc(fn)
} }
func debugf(format string, reqID uuid.UUID, args ...any) {
format = format + " " + reqID.String()
slog.Debug(fmt.Sprintf(format, args...))
}
func debug(msg string, reqID uuid.UUID) {
msg = msg + " " + reqID.String()
debugf("%s", reqID, msg)
}

138
link.go
View File

@ -2,15 +2,26 @@ package hvpnnode3
import ( import (
"errors" "errors"
"fmt"
"log/slog"
"net"
"sync"
"time"
"gitea.hbanafa.com/HeshamTB/hvpn-node3/proto"
"github.com/vishvananda/netlink" "github.com/vishvananda/netlink"
"golang.zx2c4.com/wireguard/wgctrl" "golang.zx2c4.com/wireguard/wgctrl"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes" "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
) )
var SINGLE_IP_MASK net.IPMask = net.IPv4Mask(255, 255, 255, 255)
var PEER_KEEP_ALIVE_INTERVAL time.Duration = time.Second * 25
type WGLink struct { type WGLink struct {
*netlink.LinkAttrs *netlink.LinkAttrs
*wgctrl.Client *wgctrl.Client
IPPool
lock *sync.Mutex
} }
// Retruns an existing or create a WGLink // Retruns an existing or create a WGLink
@ -48,6 +59,8 @@ func InitWGLink(ifName string, privateKey *wgtypes.Key, port int) (*WGLink, erro
return nil, err return nil, err
} }
wg.lock = &sync.Mutex{}
return &wg, netlink.LinkSetUp(&wg) return &wg, netlink.LinkSetUp(&wg)
} }
@ -72,6 +85,131 @@ func (wg *WGLink) initClient() error {
return err return err
} }
// Adds a peer to the wireguard netlink.
func (wg *WGLink) AddPeer(publicKey string) (*wgtypes.Peer, error) {
slog.Debug(fmt.Sprintf("Trying to add peer %s", publicKey))
pubKey, err := wgtypes.ParseKey(publicKey)
if err != nil {
return nil, err
}
exists, err := wg.exists(pubKey)
if err != nil {
return nil, err
}
if exists {
return nil, proto.PeerExistsErr{PublicKey: publicKey}
}
peerIp, err := wg.Allocate()
if err != nil {
return nil, err
}
peerConfig := wgtypes.PeerConfig{
PublicKey: pubKey,
AllowedIPs: []net.IPNet{
{
IP: peerIp,
Mask: SINGLE_IP_MASK,
},
},
PersistentKeepaliveInterval: &PEER_KEEP_ALIVE_INTERVAL,
ReplaceAllowedIPs: true,
}
wgConf := wgtypes.Config{
ReplacePeers: false,
Peers: []wgtypes.PeerConfig{peerConfig},
}
err = wg.ApplyConfig(wgConf)
if err != nil {
return nil, err
}
return wg.getPeer(pubKey)
}
func (wg *WGLink) DeletePeer(publickey string) error {
pkey, err := wgtypes.ParseKey(publickey)
if err != nil {
return err
}
return wg.deletePeer(pkey)
}
func (wg *WGLink) deletePeer(publickey wgtypes.Key) error {
rmCfg := createARemovePeerCfg(publickey)
return wg.ApplyConfig(rmCfg)
}
func (wg *WGLink) Exists(publicKey string) (bool, error) {
pubkey, err := wgtypes.ParseKey(publicKey)
if err != nil {
return false, err
}
return wg.exists(pubkey)
}
func (wg *WGLink) exists(pubkey wgtypes.Key) (bool, error) {
dev, err := wg.Client.Device(wg.Name)
if err != nil {
return false, err
}
for _, peer := range dev.Peers {
if peer.PublicKey == pubkey {
return true, nil
}
}
return false, nil
}
// Calls wgctrl.ConfigureDevice wrapped with a Mutex to ensure sync
func (wg *WGLink) ApplyConfig(cfg wgtypes.Config) error {
wg.lock.Lock()
defer wg.lock.Unlock()
return wg.ConfigureDevice(wg.Name, cfg)
}
func (wg *WGLink) GetPeer(publickey string) (*wgtypes.Peer, error) {
k, err := wgtypes.ParseKey(publickey)
if err != nil {
return nil, err
}
return wg.getPeer(k)
}
func (wg *WGLink) getPeer(pubkey wgtypes.Key) (*wgtypes.Peer, error) {
dev, err := wg.Device(wg.Name)
if err != nil {
return nil, err
}
for _, peer := range dev.Peers {
if peer.PublicKey == pubkey {
return &peer, nil
}
}
return nil, proto.PeerDoesNotExist
}
func createARemovePeerCfg(publickey wgtypes.Key) wgtypes.Config {
rmPeerCfg := wgtypes.PeerConfig{
Remove: true,
PublicKey: publickey,
}
return wgtypes.Config{
Peers: []wgtypes.PeerConfig{rmPeerCfg},
ReplacePeers: false,
}
}
func ValidKey(key string) bool {
_, err := wgtypes.ParseKey(key)
if err != nil { return false }
return true
}

20
proto/conv.go Normal file
View File

@ -0,0 +1,20 @@
package proto
import (
"net/url"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
func WgPeerToPeer(peer wgtypes.Peer) Peer {
return Peer{
PublicKey: peer.PublicKey.String(),
PublicKeyUrlSafe: url.QueryEscape(peer.PublicKey.String()),
MTU: 1380,
TX: peer.TransmitBytes,
RX: peer.ReceiveBytes,
AllowedIPs: peer.AllowedIPs[0].IP,
PersistentKeepalive: peer.PersistentKeepaliveInterval,
}
}

View File

@ -1,5 +1,18 @@
package proto package proto
import "errors"
type ErrJSONResponse struct { type ErrJSONResponse struct {
Message string `json:"message"` Message string `json:"message"`
} }
type PeerExistsErr struct {
PublicKey string
}
func (e PeerExistsErr) Error() string {
return e.PublicKey
}
var PeerDoesNotExist = errors.New("Peer does not exist")

View File

@ -13,9 +13,9 @@ type CreatePeerRequest struct {
/* JSON returned for user to use. This means that the peer can connect with /* JSON returned for user to use. This means that the peer can connect with
these parameters as a wireguard peer */ these parameters as a wireguard peer */
type Peer struct { type Peer struct {
Address net.IP `json:"address"`
MTU uint16 `json:"mtu"` MTU uint16 `json:"mtu"`
PublicKey string `json:"public_key"` PublicKey string `json:"public_key"`
PublicKeyUrlSafe string `json:"public_key_url_safe"`
Endpoint string `json:"endpoint"` Endpoint string `json:"endpoint"`
AllowedIPs net.IP `json:"allowed_ips"` AllowedIPs net.IP `json:"allowed_ips"`
PersistentKeepalive time.Duration `json:"presistent_keepalive"` PersistentKeepalive time.Duration `json:"presistent_keepalive"`
@ -23,4 +23,3 @@ type Peer struct {
RX int64 RX int64
} }