diff --git a/cmd/hvpn-node/hvpn-node.go b/cmd/hvpn-node/hvpn-node.go index 76afe4e..34b64ce 100644 --- a/cmd/hvpn-node/hvpn-node.go +++ b/cmd/hvpn-node/hvpn-node.go @@ -6,6 +6,7 @@ import ( "net" "net/http" "net/netip" + "net/url" "os" "os/signal" "time" @@ -58,8 +59,9 @@ func run(ctx *cli.Context) { slog.Debug("Starting run()") apiMux := http.NewServeMux() - apiMux.HandleFunc("GET /peer", hvpnnode3.HandleGetPeer(wgLink)) - apiMux.HandleFunc("POST /peer", hvpnnode3.HandlePostPeer(IPPool, wgLink)) + apiMux.HandleFunc("GET /peer/{pubkey}", hvpnnode3.HandleGetPeer(wgLink)) + apiMux.HandleFunc("POST /peer", hvpnnode3.HandlePostPeer(wgLink)) + apiMux.HandleFunc("DELETE /peer/{pubkey}", hvpnnode3.HandleDeletePeer(wgLink)) apiMux.HandleFunc("GET /peers", hvpnnode3.HandleGetPeers(wgLink)) var handler http.Handler = apiMux @@ -262,6 +264,7 @@ func setup() error { slog.Debug("main.testVip: Test IP Freed") IPPool = ipPool + wgLink.IPPool = ipPool //defer wgLink.Close() c := make(chan os.Signal, 1) @@ -289,7 +292,10 @@ func testWgPeerAdd(wgLink *hvpnnode3.WGLink) error { } publicKey := privateKey.PublicKey() - ip, err := IPPool.Allocate() + urlsafe := url.QueryEscape(publicKey.String()) + slog.Debug(urlsafe) + + ip, err := wgLink.Allocate() if err != nil { return err } @@ -323,7 +329,7 @@ func testWgPeerAdd(wgLink *hvpnnode3.WGLink) error { return err } slog.Debug("Removed test peer") - IPPool.Free(ip) + wgLink.Free(ip) slog.Debug("Freed test peer ip") diff --git a/go.mod b/go.mod index d0faed0..e9fc852 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.22.0 require ( github.com/felixge/httpsnoop v1.0.4 + github.com/google/uuid v1.6.0 github.com/urfave/cli/v2 v2.27.1 github.com/vishvananda/netlink v1.1.0 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 diff --git a/go.sum b/go.sum index 97a3f45..09e5313 100644 --- a/go.sum +++ b/go.sum @@ -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/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 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/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= diff --git a/handlers.go b/handlers.go index 6464abe..5c26223 100644 --- a/handlers.go +++ b/handlers.go @@ -1,29 +1,51 @@ package hvpnnode3 import ( + "context" "encoding/json" + "errors" "fmt" "log/slog" - "net" "net/http" "github.com/felixge/httpsnoop" - "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + "github.com/google/uuid" "gitea.hbanafa.com/HeshamTB/hvpn-node3/proto" ) +type CtxKey string +const CtxReqID CtxKey = "request_id" func HandleGetPeer(wgLink *WGLink) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotImplemented) - slog.Info("GET Peer is not implemented") + reqID := r.Context().Value(CtxReqID).(uuid.UUID) + 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 { 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) if err != nil { w.WriteHeader(http.StatusInternalServerError) @@ -32,13 +54,7 @@ func HandleGetPeers(wgLink *WGLink) http.HandlerFunc { } var peers []proto.Peer for _, peer := range dev.Peers { - p := proto.Peer{ - Address: peer.AllowedIPs[0].IP, - PublicKey: peer.PublicKey.String(), - PersistentKeepalive: peer.PersistentKeepaliveInterval, - TX: peer.TransmitBytes, - RX: peer.ReceiveBytes, - } + p := proto.WgPeerToPeer(peer) peers = append(peers, p) } err = json.NewEncoder(w).Encode(peers) @@ -48,8 +64,11 @@ func HandleGetPeers(wgLink *WGLink) http.HandlerFunc { return } } -func HandlePostPeer(IPPool IPPool, wgLink *WGLink) http.HandlerFunc { + +func HandlePostPeer(wgLink *WGLink) http.HandlerFunc { return func(w http.ResponseWriter, r* http.Request) { + reqID := r.Context().Value(CtxReqID).(uuid.UUID) + debug("POST Peer for", reqID) peerRequest := proto.CreatePeerRequest{} err := json.NewDecoder(r.Body).Decode(&peerRequest) if err != nil { @@ -59,79 +78,105 @@ func HandlePostPeer(IPPool IPPool, wgLink *WGLink) http.HandlerFunc { ) return } - // TODO: Check if pubkey is already registered - dev, err := wgLink.Device(wgLink.Name) - if err != nil { - slog.Error(err.Error()) - w.WriteHeader(http.StatusInternalServerError) + + if valid := ValidKey(peerRequest.PublicKey); !valid { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode( + proto.ErrJSONResponse{Message: "Invalid public key"}, + ) return } - - for _, peer := range dev.Peers { - if peer.PublicKey.String() == peerRequest.PublicKey { - w.WriteHeader(http.StatusAccepted) + + exists, err := wgLink.Exists(peerRequest.PublicKey) + if err != nil { + 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 } - } - - newIP, err := IPPool.Allocate() - if err != nil { - slog.Error(err.Error()) - w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode( + proto.WgPeerToPeer(*peer), + ) return } - // TODO: Add peer to wg device - // 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, - }, - ) + peer, err := wgLink.AddPeer(peerRequest.PublicKey) if err != nil { slog.Error(err.Error()) w.WriteHeader(http.StatusInternalServerError) + json.NewEncoder(w).Encode( + proto.ErrJSONResponse{Message: "Error while adding peer"}, + ) return } - + debugf("Allocated IP: %s", reqID, peer.AllowedIPs[0].IP.String()) + w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode( - &proto.Peer{ - Address: newIP, - MTU: 1384, - PublicKey: peerRequest.PublicKey, - Endpoint: "vpn.test.com:8487", - AllowedIPs: net.ParseIP("0.0.0.0/0"), - PersistentKeepalive: 25, - }, + proto.WgPeerToPeer(*peer), ) - } } +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 { // https://blog.kowalczyk.info/article/e00e89c3841e4f8c8c769a78b8a90b47/logging-http-requests-in-go.html 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) msg := fmt.Sprintf( - "%s %s %s %d %s", r.RemoteAddr, r.Method, - r.URL.String(),m.Code, m.Duration) + "[HTTP] %s %s %s %d %s %s", r.RemoteAddr, r.Method, + r.URL.String(),m.Code, m.Duration, reqID.String()) slog.Info(msg) } 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) +} diff --git a/link.go b/link.go index 62e5534..7298379 100644 --- a/link.go +++ b/link.go @@ -2,15 +2,26 @@ package hvpnnode3 import ( "errors" + "fmt" + "log/slog" + "net" + "sync" + "time" + "gitea.hbanafa.com/HeshamTB/hvpn-node3/proto" "github.com/vishvananda/netlink" "golang.zx2c4.com/wireguard/wgctrl" "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 { *netlink.LinkAttrs *wgctrl.Client + IPPool + lock *sync.Mutex } // 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 } + wg.lock = &sync.Mutex{} + return &wg, netlink.LinkSetUp(&wg) } @@ -72,6 +85,131 @@ func (wg *WGLink) initClient() error { 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 +} diff --git a/proto/conv.go b/proto/conv.go new file mode 100644 index 0000000..0f3d347 --- /dev/null +++ b/proto/conv.go @@ -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, + } +} diff --git a/proto/error.go b/proto/error.go index ad845af..c935784 100644 --- a/proto/error.go +++ b/proto/error.go @@ -1,5 +1,18 @@ package proto +import "errors" + type ErrJSONResponse struct { 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") + diff --git a/proto/peer.go b/proto/peer.go index 802a329..44fd790 100644 --- a/proto/peer.go +++ b/proto/peer.go @@ -13,9 +13,9 @@ type CreatePeerRequest struct { /* JSON returned for user to use. This means that the peer can connect with these parameters as a wireguard peer */ type Peer struct { - Address net.IP `json:"address"` MTU uint16 `json:"mtu"` PublicKey string `json:"public_key"` + PublicKeyUrlSafe string `json:"public_key_url_safe"` Endpoint string `json:"endpoint"` AllowedIPs net.IP `json:"allowed_ips"` PersistentKeepalive time.Duration `json:"presistent_keepalive"` @@ -23,4 +23,3 @@ type Peer struct { RX int64 } -