commit 1a611616bddb38c166b3fd418b9a83fc695ffbd5 Author: HeshamTB Date: Mon Mar 11 17:34:06 2024 +0300 init: - Working basic wg controls - Working ip_pool allocation - Working basic HTTP API Signed-off-by: HeshamTB diff --git a/cmd/hvpn-node/.gitignore b/cmd/hvpn-node/.gitignore new file mode 100644 index 0000000..7c07f9f --- /dev/null +++ b/cmd/hvpn-node/.gitignore @@ -0,0 +1,2 @@ +hvpn-node + diff --git a/cmd/hvpn-node/hvpn-node.go b/cmd/hvpn-node/hvpn-node.go new file mode 100644 index 0000000..faec3a4 --- /dev/null +++ b/cmd/hvpn-node/hvpn-node.go @@ -0,0 +1,257 @@ +package main + +import ( + "errors" + "fmt" + "log/slog" + "net/http" + "os" + "os/signal" + "time" + + "github.com/urfave/cli/v2" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + + hvpnnode3 "gitea.hbanafa.com/HeshamTB/hvpn-node3" +) + +var IPPool *hvpnnode3.IPPool +var VPNIPCIDR string +var PrivateKeyPath cli.Path +var InterfaceName string +var WgPort int +var wgLink *hvpnnode3.WGLink + +var httpPort int + +func main() { + + + // TODO: Define error exit codes + slog.SetLogLoggerLevel(slog.LevelDebug) + + app := createCliApp() + err := app.Run(os.Args) + if err != nil { + slog.Error(err.Error()) + os.Exit(1) + } + + +} + +func run() { + IPPool, err := hvpnnode3.NewPool(VPNIPCIDR) + if err != nil { + slog.Error(fmt.Sprintf("main.IPPool: %s", err)) + os.Exit(1) + } + slog.Info(fmt.Sprintf("Init ip pool %s", VPNIPCIDR)) + + testVip, err := IPPool.Allocate() + if err != nil { + slog.Error("main.testVip: ", err) + os.Exit(1) + } + + slog.Info(fmt.Sprintf("main.testVip: IP Pool Test IP: %s", testVip.String())) + err = IPPool.Free(testVip) + if err != nil { + slog.Error("main.testVip: Could not free test Vip from IPPool!", err) + os.Exit(1) + } + slog.Info("main.testVip: Test IP Freed") + + apiMux := http.NewServeMux() + apiMux.HandleFunc("GET /peer", hvpnnode3.HandleGetPeer(wgLink)) + apiMux.HandleFunc("POST /peer", hvpnnode3.HandlePostPeer(IPPool, wgLink)) + apiMux.HandleFunc("GET /peers", hvpnnode3.HandleGetPeers(wgLink)) + + var handler http.Handler = apiMux + handler = hvpnnode3.HttpLogHandler2(handler) + + port := fmt.Sprintf("%d", httpPort) + host := "0.0.0.0" + hostPort := fmt.Sprintf("%s:%s", host, port) + + slog.Info(fmt.Sprintf("Starting HVPN node on %s", hostPort)) + srv := &http.Server{ + ReadTimeout: 120 * time.Second, + WriteTimeout: 120 * time.Second, + IdleTimeout: 120 * time.Second, + Handler: handler, + Addr: hostPort, + } + + defer wgLink.Close() + slog.Warn(srv.ListenAndServe().Error()) +} + +func createCliApp() *cli.App { + app := cli.NewApp() + app.Name = os.Args[0] + app.HideHelpCommand = true + app.Usage = "HVPN node API server" + app.Args = true + + author1 := cli.Author{ + Name: "Hesham T. Banafa", + Email: "hishaminv@gmail.com"} + app.Authors = append(app.Authors, &author1) + + logLevel := cli.StringFlag{ + Name: "log-level", + Value: "INFO", + EnvVars: []string{"LOG_LEVEL"}, + Action: func(ctx *cli.Context, s string) error { + switch s { + case "INFO": + slog.SetLogLoggerLevel(slog.LevelInfo) + case "DEBUG": + slog.SetLogLoggerLevel(slog.LevelDebug) + case "WARN": + slog.SetLogLoggerLevel(slog.LevelWarn) + case "ERROR": + slog.SetLogLoggerLevel(slog.LevelError) + default: + return cli.Exit(fmt.Sprintf("Undefined log level: %s", s), 1) + } + return nil + }, + } + app.Flags = append(app.Flags, &logLevel) + + privateKeyFileFlag := cli.PathFlag{ + Name: "private-key", + Required: true, + Usage: "Path to file with private key", + Destination: &PrivateKeyPath, + } + app.Flags = append(app.Flags, &privateKeyFileFlag) + + vpnIpCIDR := cli.StringFlag{ + Name: "cidr", + Usage: "The network subnet used for the internal IP Pool", + Value: "10.42.0.0/16", + Aliases: []string{"n"}, + Destination: &VPNIPCIDR, + } + app.Flags = append(app.Flags, &vpnIpCIDR) + + wgInterfaceName := cli.StringFlag{ + Name: "interface", + Usage: "Name of the Wireguard interface to be created and managed", + Value: "hvpn0", + Aliases: []string{"i"}, + Destination: &InterfaceName, + } + app.Flags = append(app.Flags, &wgInterfaceName) + + wgPort := cli.IntFlag{ + Name: "port", + Usage: "UDP Port for wireguard device", + Value: 6416, + Aliases: []string{"p"}, + Destination: &WgPort, + } + app.Flags = append(app.Flags, &wgPort) + + httpPort := cli.IntFlag{ + Name: "http-port", + Usage: "TCP Port for HTTP API", + Value: 8080, + Destination: &httpPort, + } + app.Flags = append(app.Flags, &httpPort) + + + app.Action = func(ctx *cli.Context) error { + err := setup() + if err != nil { + return err + } + run() + return nil + } + + return app +} + +func setup() error { + privKeyFile, err := os.Open(PrivateKeyPath) + if err != nil { + return cli.Exit(err, 1) + } + defer privKeyFile.Close() + slog.Debug("Keyfile opened for reading") + + + privateKeyStr := make([]byte, 45) + n, err := privKeyFile.Read(privateKeyStr) + if err != nil { + return cli.Exit(err, 1) + } + if n != 45 { + slog.Warn("Private key length did not math the expected 45!") + } + slog.Debug(fmt.Sprintf("Read %d bytes from keyfile", n)) + + privateKey, err := wgtypes.ParseKey(string(privateKeyStr)) + if err != nil { + return cli.Exit(err, 1) + } + slog.Debug("Private key parsed and is correct") + + wg, err := hvpnnode3.InitWGLink( + InterfaceName, + &privateKey, + WgPort, + ) + if err != nil { + return cli.Exit(err, 1) + } + + wgLink = wg + + // this is done to recover from the next call to wgLink.Device call + // idk a better way to recover or prevent the panic when running user + // does not have root or CAP_NET_ADMIN. + defer func() { + if r := recover(); r != nil { + slog.Error(fmt.Sprint(r)) + slog.Error("Recovered from panic. Ensure to run as root or with CAP_NET_ADMIN") + cli.Exit(errors.New("Recovered from panic. Ensure to run as root or with CAP_NET_ADMIN"),1) + os.Exit(-1) + } + }() + + slog.Info("Wireguard device is setup and running") + + + dev, err := wgLink.Device(InterfaceName) + if err != nil { + return cli.Exit(err, 1) + } + slog.Info("Initialized Wireguard device using wgctrl") + + slog.Info( + fmt.Sprintf( + "Device name: %s, UDP port: %d, Type: %s", + dev.Name, dev.ListenPort, dev.Type.String(), + ), + ) + + //defer wgLink.Close() + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, os.Kill) + go func() { + slog.Debug("Listening for SIGINT, SIGKILL") + <- c + slog.Warn("Recived SIGINT! Closing Wireguard Device") + wgLink.Close() + os.Exit(0) + }() + + return nil +} + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d0faed0 --- /dev/null +++ b/go.mod @@ -0,0 +1,27 @@ +module gitea.hbanafa.com/HeshamTB/hvpn-node3 + +go 1.22.0 + +require ( + github.com/felixge/httpsnoop v1.0.4 + 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 +) + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/josharian/native v1.1.0 // indirect + github.com/mdlayher/genetlink v1.3.2 // indirect + github.com/mdlayher/netlink v1.7.2 // indirect + github.com/mdlayher/socket v0.4.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + golang.org/x/crypto v0.8.0 // indirect + golang.org/x/net v0.9.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.7.0 // indirect + golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..97a3f45 --- /dev/null +++ b/go.sum @@ -0,0 +1,39 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +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/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= +github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= +github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= +github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= +github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= +github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= +github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws= +github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= +github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= +golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= +golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= +golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= +golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b h1:J1CaxgLerRR5lgx3wnr6L04cJFbWoceSK9JWBdglINo= +golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b/go.mod h1:tqur9LnfstdR9ep2LaJT4lFUl0EjlHtge+gAjmsHUG4= +golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE= +golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80= diff --git a/handlers.go b/handlers.go new file mode 100644 index 0000000..6464abe --- /dev/null +++ b/handlers.go @@ -0,0 +1,137 @@ +package hvpnnode3 + +import ( + "encoding/json" + "fmt" + "log/slog" + "net" + "net/http" + + "github.com/felixge/httpsnoop" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + + "gitea.hbanafa.com/HeshamTB/hvpn-node3/proto" +) + + +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") + + } +} + +func HandleGetPeers(wgLink *WGLink) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + dev, err := wgLink.Device(wgLink.Name) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + slog.Error(err.Error()) + return + } + 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, + } + peers = append(peers, p) + } + err = json.NewEncoder(w).Encode(peers) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + } + return + } +} +func HandlePostPeer(IPPool IPPool, wgLink *WGLink) http.HandlerFunc { + return func(w http.ResponseWriter, r* http.Request) { + peerRequest := proto.CreatePeerRequest{} + err := json.NewDecoder(r.Body).Decode(&peerRequest) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode( + &proto.ErrJSONResponse{Message: "Invalid request JSON"}, + ) + 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) + return + } + + for _, peer := range dev.Peers { + if peer.PublicKey.String() == peerRequest.PublicKey { + w.WriteHeader(http.StatusAccepted) + return + } + } + + newIP, err := IPPool.Allocate() + if err != nil { + slog.Error(err.Error()) + w.WriteHeader(http.StatusInternalServerError) + 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, + }, + ) + if err != nil { + slog.Error(err.Error()) + w.WriteHeader(http.StatusInternalServerError) + return + } + + 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, + }, + ) + + } +} + + +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) { + 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) + slog.Info(msg) + } + return http.HandlerFunc(fn) +} diff --git a/init/isolate.sh b/init/isolate.sh new file mode 100755 index 0000000..287e31b --- /dev/null +++ b/init/isolate.sh @@ -0,0 +1,28 @@ +#!/bin/env bash + +# A POSIX variable +OPTIND=1 # Reset in case getopts has been used previously in the shell. + +# Initialize our own variables: +wg_interface="" + +while getopts "h?i:" opt; do + case "$opt" in + h|\?) + #show_help + echo "Usage: isolate.sh -i " + exit 0 + ;; + i) wg_interface=$OPTARG + ;; + esac +done + +shift $((OPTIND-1)) + +[ "${1:-}" = "--" ] && shift + + +set -x +iptables -I FORWARD -i $wg_interface -o $wg_interface -j DROP +set +x diff --git a/init/systemd/hvpn.service b/init/systemd/hvpn.service new file mode 100644 index 0000000..383b8a0 --- /dev/null +++ b/init/systemd/hvpn.service @@ -0,0 +1,21 @@ +[Unit] +Description=HVPN node service + +[Service] +Type=exec +User=hvpnnode +Group=hvpn +ExecStart=/opt/hvpn-node/hvpn-node +AmbientCapabilities=CAP_NET_ADMIN +RemainAfterExit=true +NoNewPrivileges=true +PrivateTmp=true +ProtectSystem=full +PrivateDevices=true +ProtectKernelTunables=true + +#Nice=19 +#IOSchedulingClass=idle +#IOSchedulingPriority=7 +#PrivateDevices=true +#PrivateNetwork=true diff --git a/ip_pool.go b/ip_pool.go new file mode 100644 index 0000000..3b5b27a --- /dev/null +++ b/ip_pool.go @@ -0,0 +1,102 @@ +package hvpnnode3 + +import ( + "encoding/binary" + "errors" + "net" + "sync" +) + +// IPPool knows how to allocate IPs and free previously allocated ones. +type IPPool interface { + Allocate() (net.IP, error) + Free(net.IP) error + Remove(...net.IP) error +} + +// Pool is a pool of available IP numbers for allocation. +type Pool struct { + network *net.IPNet + available []net.IP + allocMu sync.Mutex +} + +// NewPool creates a new pool with a CIDR. +func NewPool(cidr string) (*Pool, error) { + networkip, network, err := net.ParseCIDR(cidr) + if err != nil { + return nil, err + } + + mask, size := network.Mask.Size() + maskbits := binary.BigEndian.Uint32(network.Mask) + max := uint32(1 << uint(size-mask)) + // Remove unusable host ranges + max -= 2 + + available := make([]net.IP, max) + networkipbits := maskbits & binary.BigEndian.Uint32(networkip.To4()) + for ; max > 0; max-- { + ip := make([]byte, 4) + binary.BigEndian.PutUint32(ip, uint32(networkipbits|max)) + available[max-1] = net.IP(ip) + } + + return &Pool{ + network: network, + available: available, + }, nil +} + +// Remove selected IPs from the available pool. +func (p *Pool) Remove(ips ...net.IP) error { + p.allocMu.Lock() + defer p.allocMu.Unlock() + + all := p.available + for _, ip := range ips { + if !p.network.Contains(ip) { + return errors.New("IP is not part of this pool") + } + for n, a := range p.available { + if a.Equal(ip) { + all = append(all[:n], all[n+1:]...) + } + } + } + p.available = all + return nil +} + +// Allocate assigns a new IP to the pool for use. +func (p *Pool) Allocate() (ip net.IP, err error) { + p.allocMu.Lock() + defer p.allocMu.Unlock() + + if len(p.available) > 0 { + ip = p.available[0] + p.available = p.available[1:] + } else { + err = errors.New("No more IPs currently available") + } + return +} + +// Free returns the IP to the pool to be used by other allocations. +func (p *Pool) Free(ip net.IP) error { + if !p.network.Contains(ip) { + return errors.New("IP is not part of this pool") + } + p.allocMu.Lock() + defer p.allocMu.Unlock() + + p.available = append([]net.IP{ip}, p.available...) + return nil +} + +// ip4To6 will prefix IPv4 with the IPv6 network to create an IPv6 address. +func ip4To6(ip4 net.IP, ip6prefix *net.IPNet) (ip6 net.IP) { + b6 := ip6prefix.IP.To16() + return append(b6[:12], ip4.To4()...) +} + diff --git a/ip_pool_test.go b/ip_pool_test.go new file mode 100644 index 0000000..37e1298 --- /dev/null +++ b/ip_pool_test.go @@ -0,0 +1,69 @@ +package hvpnnode3 + +import ( + "net" + "testing" +) + +func TestPoolIP4(t *testing.T) { + pool, err := NewPool("10.2.0.0/16") + if err != nil { + t.Fatal(err) + } + if l := len(pool.available); l != 65534 { + t.Error("Expected 65534 available addresses, got", l) + } + + ip, err := pool.Allocate() + if err != nil { + t.Fatal(err) + } + + if l := len(pool.available); l != 65533 { + t.Error("Expected 65533 available addresses, got", l) + } + + if err := pool.Free(ip); err != nil { + t.Fatal(err) + } + if l := len(pool.available); l != 65534 { + t.Error("Expected 65534 available addresses, got", l) + } + + if ip.String() != "10.2.0.1" { + t.Error("Wrong ip", ip) + } + + if err = pool.Free(net.ParseIP("1.2.3.4")); err == nil { + t.Error("expected error trying to free invalid IP") + } + + if l := len(pool.available); l != 65534 { + t.Error("Expected 65534 available addresses, got", l) + } + if err := pool.Remove(net.ParseIP("10.2.0.100"), net.ParseIP("10.2.0.200")); err != nil { + t.Fatal(err) + } + if l := len(pool.available); l != 65532 { + t.Error("invalid number of available addresses, ", l) + } + for _, ip := range pool.available { + if ip.Equal(net.ParseIP("10.2.0.100")) { + t.Error("IP was not removed") + break + } + } +} + +func TestIP4To6(t *testing.T) { + _, prefix, err := net.ParseCIDR("fdad:b10c:a::/48") + if err != nil { + t.Fatal(err) + } + ip6 := ip4To6(net.IP{10, 1, 2, 3}, prefix) + if s := net.IP(ip6).String(); s != "fdad:b10c:a::a01:203" { + t.Log(s) + t.Error("invalid IP") + } +} + diff --git a/link.go b/link.go new file mode 100644 index 0000000..62e5534 --- /dev/null +++ b/link.go @@ -0,0 +1,77 @@ +package hvpnnode3 + +import ( + "errors" + + "github.com/vishvananda/netlink" + "golang.zx2c4.com/wireguard/wgctrl" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" +) + +type WGLink struct { + *netlink.LinkAttrs + *wgctrl.Client +} + +// Retruns an existing or create a WGLink +func InitWGLink(ifName string, privateKey *wgtypes.Key, port int) (*WGLink, error){ + attrs := netlink.NewLinkAttrs() + attrs.Name = ifName + wg := WGLink{LinkAttrs: &attrs} + link, err := netlink.LinkByName(ifName) + if err != nil { + switch err.(type) { + case netlink.LinkNotFoundError: + if err := netlink.LinkAdd(&wg); err != nil { + return nil, err + } + default: + return nil, err + } + } else { + wg.LinkAttrs = link.Attrs() + } + + err = wg.initClient() + if err != nil { + return nil, err + } + + err = wg.ConfigureDevice( + ifName, + wgtypes.Config{ + PrivateKey: privateKey, + ListenPort: &port, + }, + ) + if err != nil { + return nil, err + } + + return &wg, netlink.LinkSetUp(&wg) +} + +func (WGLink) Type() string { + return "wireguard" +} + +func (wg *WGLink) Attrs() *netlink.LinkAttrs { + return wg.LinkAttrs +} + +func (wg *WGLink) Close() error { + return netlink.LinkDel(wg) +} + +func (wg *WGLink) initClient() error { + client, err := wgctrl.New() + if client == nil { + return errors.New("Could not initialize new Wireguard Client") + } + wg.Client = client + return err +} + + + + diff --git a/proto/error.go b/proto/error.go new file mode 100644 index 0000000..ad845af --- /dev/null +++ b/proto/error.go @@ -0,0 +1,5 @@ +package proto + +type ErrJSONResponse struct { + Message string `json:"message"` +} diff --git a/proto/peer.go b/proto/peer.go new file mode 100644 index 0000000..802a329 --- /dev/null +++ b/proto/peer.go @@ -0,0 +1,26 @@ +package proto + +import ( + "net" + "time" +) + +type CreatePeerRequest struct { + UUID string `json:"uuid"` + PublicKey string `json:"public_key"` +} + +/* 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"` + Endpoint string `json:"endpoint"` + AllowedIPs net.IP `json:"allowed_ips"` + PersistentKeepalive time.Duration `json:"presistent_keepalive"` + TX int64 + RX int64 +} + +