init:
- Working basic wg controls - Working ip_pool allocation - Working basic HTTP API Signed-off-by: HeshamTB <hishaminv@gmail.com>
This commit is contained in:
commit
1a611616bd
2
cmd/hvpn-node/.gitignore
vendored
Normal file
2
cmd/hvpn-node/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
hvpn-node
|
||||||
|
|
257
cmd/hvpn-node/hvpn-node.go
Normal file
257
cmd/hvpn-node/hvpn-node.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
|
27
go.mod
Normal file
27
go.mod
Normal file
@ -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
|
||||||
|
)
|
39
go.sum
Normal file
39
go.sum
Normal file
@ -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=
|
137
handlers.go
Normal file
137
handlers.go
Normal file
@ -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)
|
||||||
|
}
|
28
init/isolate.sh
Executable file
28
init/isolate.sh
Executable file
@ -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 <wg_interface>"
|
||||||
|
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
|
21
init/systemd/hvpn.service
Normal file
21
init/systemd/hvpn.service
Normal file
@ -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
|
102
ip_pool.go
Normal file
102
ip_pool.go
Normal file
@ -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()...)
|
||||||
|
}
|
||||||
|
|
69
ip_pool_test.go
Normal file
69
ip_pool_test.go
Normal file
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
77
link.go
Normal file
77
link.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
5
proto/error.go
Normal file
5
proto/error.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package proto
|
||||||
|
|
||||||
|
type ErrJSONResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
26
proto/peer.go
Normal file
26
proto/peer.go
Normal file
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user