333 lines
8.1 KiB
Go
333 lines
8.1 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
"net/netip"
|
|
"os"
|
|
"os/signal"
|
|
"time"
|
|
|
|
"github.com/urfave/cli/v2"
|
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
|
|
|
hvpnnode3 "gitea.hbanafa.com/HeshamTB/hvpn-node3"
|
|
)
|
|
|
|
/*
|
|
Can change all therse vars to local func vars
|
|
IPPool can be internal to wglink or other abstraction. As well as
|
|
POrt, ifname and CIDR and so on. Use Clie.ctx to get the values
|
|
*/
|
|
|
|
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)
|
|
|
|
uid := os.Getuid()
|
|
if uid == -1 {
|
|
slog.Warn("Running on windows! whatrr u doing?")
|
|
} else if uid == 0 {
|
|
slog.Warn("Running as root! avoid running as root by setting CAP_NET_ADMIN")
|
|
}
|
|
|
|
app := createCliApp()
|
|
err := app.Run(os.Args)
|
|
if err != nil {
|
|
slog.Error(err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
|
|
}
|
|
|
|
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 /peers", hvpnnode3.HandleGetPeers(wgLink))
|
|
|
|
var handler http.Handler = apiMux
|
|
handler = hvpnnode3.HttpLogHandler2(handler)
|
|
|
|
port := fmt.Sprintf("%d", httpPort)
|
|
host := ctx.String("host")
|
|
if host == "" {
|
|
slog.Info("Host is not set. Using 0.0.0.0")
|
|
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 {
|
|
lvl := new(slog.LevelVar)
|
|
err := lvl.UnmarshalText([]byte(s))
|
|
if err != nil {
|
|
slog.Debug("Error on unmarshal log level flag")
|
|
return err
|
|
}
|
|
slog.SetLogLoggerLevel(lvl.Level())
|
|
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)
|
|
|
|
httpListenAddr := cli.StringFlag{
|
|
Name: "host",
|
|
Usage: "IP address to listen on for HTTP API requests",
|
|
Value: "0.0.0.0",
|
|
Action: func(ctx *cli.Context, s string) error {
|
|
_, err := netip.ParseAddr(s)
|
|
if err != nil {
|
|
return cli.Exit(fmt.Sprintf("Can not parse %s as a network IP", s), 1)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
app.Flags = append(app.Flags, &httpListenAddr)
|
|
|
|
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(ctx)
|
|
return nil
|
|
}
|
|
|
|
return app
|
|
}
|
|
|
|
func setup() error {
|
|
slog.Debug("Starting setup()")
|
|
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 {
|
|
slog.Warn("Error while initlizing Wireguard netlink and device!")
|
|
slog.Warn("Ensure to run as root or with CAP_NET_ADMIN")
|
|
return cli.Exit(err, 1)
|
|
}
|
|
|
|
wgLink = wg
|
|
|
|
slog.Info("Wireguard netlink 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(),
|
|
),
|
|
)
|
|
|
|
ipPool, err := hvpnnode3.NewPool(VPNIPCIDR)
|
|
if err != nil {
|
|
slog.Error(fmt.Sprintf("main.IPPool: %s", err))
|
|
os.Exit(1)
|
|
}
|
|
slog.Debug(fmt.Sprintf("Init ip pool %s", VPNIPCIDR))
|
|
|
|
testVip, err := ipPool.Allocate()
|
|
if err != nil {
|
|
slog.Error("main.testVip: ", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
slog.Debug(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.Debug("main.testVip: Test IP Freed")
|
|
|
|
IPPool = ipPool
|
|
|
|
//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)
|
|
}()
|
|
|
|
err = testWgPeerAdd(wgLink)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func testWgPeerAdd(wgLink *hvpnnode3.WGLink) error {
|
|
privateKey, err := wgtypes.GeneratePrivateKey()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
publicKey := privateKey.PublicKey()
|
|
|
|
ip, err := IPPool.Allocate()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
peerConfig := wgtypes.PeerConfig{
|
|
PublicKey: publicKey,
|
|
AllowedIPs: []net.IPNet{
|
|
{
|
|
IP: ip,
|
|
Mask: net.IPv4Mask(255, 255, 255, 255),
|
|
},
|
|
},
|
|
}
|
|
|
|
wgConfig := wgtypes.Config{
|
|
Peers: []wgtypes.PeerConfig{peerConfig},
|
|
}
|
|
|
|
err = wgLink.ConfigureDevice(wgLink.Name, wgConfig)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
slog.Debug(fmt.Sprintf("Added test peer %v", peerConfig.PublicKey))
|
|
|
|
wgConfig.ReplacePeers = true
|
|
wgConfig.Peers = []wgtypes.PeerConfig{}
|
|
|
|
err = wgLink.ConfigureDevice(wgLink.Name, wgConfig)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
slog.Debug("Removed test peer")
|
|
IPPool.Free(ip)
|
|
slog.Debug("Freed test peer ip")
|
|
|
|
|
|
return nil
|
|
}
|
|
|