From c5dc5da9a08fb8d19df30f7ceb32a8d55df1893c Mon Sep 17 00:00:00 2001 From: James Batt Date: Sat, 17 Oct 2020 17:09:33 +1100 Subject: [PATCH] lots of updates to config - also added a data migration tool for moving between storage backends --- .vscode/launch.json | 20 +++ Dockerfile | 20 +-- README.md | 11 +- cmd/migrate/main.go | 58 +++++++ cmd/serve/main.go | 309 ++++++++++++++++++++++++++++++++++++++ codegen.sh | 4 +- docker-compose.yml | 4 + go.sum | 2 + internal/config/config.go | 142 ------------------ internal/storage/file.go | 2 +- main.go | 172 ++++----------------- 11 files changed, 449 insertions(+), 295 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 cmd/migrate/main.go create mode 100644 cmd/serve/main.go diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..53d7904 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Launch", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${workspaceFolder}/main.go", + "env": {}, + "args": [ + "serve", + "--config=config.yaml" + ] + } + ] +} diff --git a/Dockerfile b/Dockerfile index 2b3f33f..fc92099 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,13 +12,14 @@ RUN npm run codegen RUN npm run build ### Build stage for the website backend server -FROM golang:1.13.8 as server -RUN apt-get update -RUN apt-get install -y protobuf-compiler libprotobuf-dev +FROM golang:1.13.8-alpine as server +RUN apk add gcc musl-dev +RUN apk add protobuf +RUN apk add protobuf-dev WORKDIR /code ENV GOOS=linux ENV GARCH=amd64 -ENV CGO_ENABLED=0 +ENV CGO_ENABLED=1 ENV GO111MODULE=on RUN go get github.com/golang/protobuf/protoc-gen-go@v1.3.5 COPY ./go.mod ./ @@ -28,9 +29,10 @@ COPY ./proto/ ./proto/ COPY ./codegen.sh ./ RUN ./codegen.sh COPY ./main.go ./main.go -COPY ./internal/ ./internal/ +COPY ./cmd/ ./cmd/ COPY ./pkg/ ./pkg/ -RUN go build -o server +COPY ./internal/ ./internal/ +RUN go build -o wg-access-server ### Server FROM alpine:3.10 @@ -38,7 +40,7 @@ RUN apk add iptables RUN apk add wireguard-tools RUN apk add curl ENV CONFIG="/config.yaml" -ENV STORAGE="file:///data" -COPY --from=server /code/server /server +ENV STORAGE="sqlite3:///data/db.sqlite3" +COPY --from=server /code/wg-access-server /usr/local/bin/wg-access-server COPY --from=website /code/build /website/build -CMD /server +CMD ["wg-access-server", "serve"] diff --git a/README.md b/README.md index 528310c..6618bd0 100644 --- a/README.md +++ b/README.md @@ -27,13 +27,17 @@ Quick Links: Here's a quick command to run the server to try it out. ```bash +export ADMIN_PASSWORD="example" +export WIREGUARD_PRIVATE_KEY="$(wg genkey)" + docker run \ -it \ --rm \ --cap-add NET_ADMIN \ --device /dev/net/tun:/dev/net/tun \ -v wg-access-server-data:/data \ - -e "WIREGUARD_PRIVATE_KEY=$(wg genkey)" \ + -e "ADMIN_PASSWORD=$ADMIN_PASSWORD" \ + -e "WIREGUARD_PRIVATE_KEY=$WIREGUARD_PRIVATE_KEY" \ -p 8000:8000/tcp \ -p 51820:51820/udp \ place1/wg-access-server @@ -62,9 +66,12 @@ helm delete my-release ## Running with Docker-Compose -You modify the docker-compose.yml file for you need then run this following command. +Download the the docker-compose.yml file from the repo and run the following command. ```bash +export ADMIN_PASSWORD="example" +export WIREGUARD_PRIVATE_KEY="$(wg genkey)" + docker-compose up ``` diff --git a/cmd/migrate/main.go b/cmd/migrate/main.go new file mode 100644 index 0000000..14a1970 --- /dev/null +++ b/cmd/migrate/main.go @@ -0,0 +1,58 @@ +package migrate + +import ( + "github.com/pkg/errors" + "github.com/place1/wg-access-server/internal/storage" + "github.com/sirupsen/logrus" + "gopkg.in/alecthomas/kingpin.v2" +) + +func RegisterCommand(app *kingpin.Application) *migratecmd { + cmd := &migratecmd{} + cli := app.Command(cmd.Name(), "Migrate your wg-access-server devices between storage backends. This tool is provided on a best effort bases.") + cli.Arg("source", "The source storage URI").Required().StringVar(&cmd.src) + cli.Arg("destination", "The destination storage URI").Required().StringVar(&cmd.dest) + return cmd +} + +type migratecmd struct { + src string + dest string +} + +func (cmd *migratecmd) Name() string { + return "migrate" +} + +func (cmd *migratecmd) Run() { + srcBackend, err := storage.NewStorage(cmd.src) + if err != nil { + logrus.Fatal(errors.Wrap(err, "failed to create src storage backend")) + } + if err := srcBackend.Open(); err != nil { + logrus.Fatal(errors.Wrap(err, "failed to connect/open src storage backend")) + } + defer srcBackend.Close() + + destBackend, err := storage.NewStorage(cmd.dest) + if err != nil { + logrus.Fatal(errors.Wrap(err, "failed to create destination storage backend")) + } + if err := destBackend.Open(); err != nil { + logrus.Fatal(errors.Wrap(err, "failed to connect/open destination storage backend")) + } + defer destBackend.Close() + + srcDevices, err := srcBackend.List("") + if err != nil { + logrus.Fatal(errors.Wrap(err, "failed to list all devices from source storage backend")) + } + + logrus.Infof("copying %v devices from source --> destination backend", len(srcDevices)) + + for _, device := range srcDevices { + if err := destBackend.Save(device); err != nil { + logrus.Fatal(errors.Wrap(err, "failed to write device to destination storage backend")) + } + } +} diff --git a/cmd/serve/main.go b/cmd/serve/main.go new file mode 100644 index 0000000..fe94520 --- /dev/null +++ b/cmd/serve/main.go @@ -0,0 +1,309 @@ +package serve + +import ( + "fmt" + "io/ioutil" + "net/http" + "strings" + + "github.com/place1/wg-access-server/internal/services" + "github.com/place1/wg-access-server/internal/storage" + "github.com/place1/wg-access-server/pkg/authnz" + "github.com/place1/wg-access-server/pkg/authnz/authconfig" + "github.com/place1/wg-access-server/pkg/authnz/authsession" + "github.com/vishvananda/netlink" + "golang.org/x/crypto/bcrypt" + "golang.zx2c4.com/wireguard/wgctrl/wgtypes" + "gopkg.in/alecthomas/kingpin.v2" + "gopkg.in/yaml.v2" + + "github.com/gorilla/mux" + "github.com/place1/wg-embed/pkg/wgembed" + + "github.com/pkg/errors" + "github.com/place1/wg-access-server/internal/config" + "github.com/place1/wg-access-server/internal/devices" + "github.com/place1/wg-access-server/internal/dnsproxy" + "github.com/place1/wg-access-server/internal/network" + "github.com/sirupsen/logrus" +) + +func RegisterCommand(app *kingpin.Application) *servecmd { + cmd := &servecmd{} + cli := app.Command(cmd.Name(), "Run the server") + cli.Flag("config", "Path to a config file").Envar("CONFIG").StringVar(&cmd.configPath) + cli.Flag("web-port", "The port that the web ui server will listen on").Envar("WEB_PORT").Default("8000").IntVar(&cmd.webPort) + cli.Flag("wireguard-port", "The port that the Wireguard server will listen on").Envar("WIREGUARD_PORT").Default("51820").IntVar(&cmd.wireguardPort) + cli.Flag("storage", "The storage backend connection string").Envar("STORAGE").Default("memory://").StringVar(&cmd.storage) + cli.Flag("wireguard-private-key", "Wireguard private key").Envar("WIREGUARD_PRIVATE_KEY").StringVar(&cmd.privateKey) + cli.Flag("disable-metadata", "Disable metadata collection (i.e. metrics)").Envar("DISABLE_METADATA").Default("false").BoolVar(&cmd.disableMetadata) + cli.Flag("admin-username", "Admin username (defaults to admin)").Envar("ADMIN_USERNAME").Default("admin").StringVar(&cmd.adminUsername) + cli.Flag("admin-password", "Admin password (provide plaintext, stored in-memory only)").Envar("ADMIN_PASSWORD").StringVar(&cmd.adminPassword) + cli.Flag("upstream-dns", "An upstream DNS server to proxy DNS traffic to").Envar("UPSTREAM_DNS").StringVar(&cmd.upstreamDNS) + return cmd +} + +type servecmd struct { + configPath string + webPort int + wireguardPort int + storage string + privateKey string + disableMetadata bool + adminUsername string + adminPassword string + upstreamDNS string +} + +func (cmd *servecmd) Name() string { + return "serve" +} + +func (cmd *servecmd) Run() { + conf := cmd.ReadConfig() + + // The server's IP within the VPN virtual network + vpnip := network.ServerVPNIP(conf.VPN.CIDR) + + // WireGuard Server + wg := wgembed.NewNoOpInterface() + if conf.WireGuard.Enabled { + wgimpl, err := wgembed.New(conf.WireGuard.InterfaceName) + if err != nil { + logrus.Fatal(errors.Wrap(err, "failed to create wireguard interface")) + } + defer wgimpl.Close() + wg = wgimpl + + logrus.Infof("starting wireguard server on 0.0.0.0:%d", conf.WireGuard.Port) + + wgconfig := &wgembed.ConfigFile{ + Interface: wgembed.IfaceConfig{ + PrivateKey: conf.WireGuard.PrivateKey, + Address: vpnip.String(), + ListenPort: &conf.WireGuard.Port, + }, + } + + if err := wg.LoadConfig(wgconfig); err != nil { + logrus.Fatal(errors.Wrap(err, "failed to load wireguard config")) + } + + logrus.Infof("wireguard VPN network is %s", conf.VPN.CIDR) + + if err := network.ConfigureForwarding(conf.WireGuard.InterfaceName, conf.VPN.GatewayInterface, conf.VPN.CIDR, conf.VPN.AllowedIPs); err != nil { + logrus.Fatal(err) + } + } + + // DNS Server + if conf.DNS.Enabled { + dns, err := dnsproxy.New(dnsproxy.DNSServerOpts{ + Upstream: conf.DNS.Upstream, + }) + if err != nil { + logrus.Fatal(errors.Wrap(err, "failed to start dns server")) + } + defer dns.Close() + } + + // Storage + storageBackend, err := storage.NewStorage(conf.Storage) + if err != nil { + logrus.Fatal(errors.Wrap(err, "failed to create storage backend")) + } + if err := storageBackend.Open(); err != nil { + logrus.Fatal(errors.Wrap(err, "failed to connect/open storage backend")) + } + defer storageBackend.Close() + + // Services + deviceManager := devices.New(wg, storageBackend, conf.VPN.CIDR) + if err := deviceManager.StartSync(conf.DisableMetadata); err != nil { + logrus.Fatal(errors.Wrap(err, "failed to sync")) + } + + router := mux.NewRouter() + router.Use(services.TracesMiddleware) + router.Use(services.RecoveryMiddleware) + + // Health check endpoint + router.PathPrefix("/health").Handler(services.HealthEndpoint()) + + // Authentication middleware + if conf.Auth.IsEnabled() { + router.Use(authnz.NewMiddleware(conf.Auth, claimsMiddleware(conf))) + } else { + logrus.Warn("[DEPRECATION NOTICE] using wg-access-server without an admin user is deprecated and will be removed in an upcoming minor release.") + router.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next.ServeHTTP(w, r.WithContext(authsession.SetIdentityCtx(r.Context(), &authsession.AuthSession{ + Identity: &authsession.Identity{ + Subject: "", + }, + }))) + }) + }) + } + + // Subrouter for our site (web + api) + site := router.PathPrefix("/").Subrouter() + site.Use(authnz.RequireAuthentication) + + // Grpc api + site.PathPrefix("/api").Handler(services.ApiRouter(&services.ApiServices{ + Config: conf, + DeviceManager: deviceManager, + Wg: wg, + })) + + // Static website + site.PathPrefix("/").Handler(services.WebsiteRouter()) + + // publicRouter.NotFoundHandler = authMiddleware.Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // if authsession.Authenticated(r.Context()) { + // router.ServeHTTP(w, r) + // } else { + // http.Redirect(w, r, "/signin", http.StatusTemporaryRedirect) + // } + // })) + publicRouter := router + + // Listen + address := fmt.Sprintf("0.0.0.0:%d", conf.Port) + srv := &http.Server{ + Addr: address, + Handler: publicRouter, + } + + // Start Web server + logrus.Infof("web ui listening on %v", address) + if err := srv.ListenAndServe(); err != nil { + logrus.Fatal(errors.Wrap(err, "unable to start http server")) + } +} + +func (cmd *servecmd) ReadConfig() *config.AppConfig { + // here we're filling out the config struct + // with values from our flags/defaults. + config := &config.AppConfig{} + config.Port = cmd.webPort + config.WireGuard.InterfaceName = "wg0" + config.WireGuard.Port = cmd.wireguardPort + config.VPN.CIDR = "10.44.0.0/24" + config.DisableMetadata = cmd.disableMetadata + config.WireGuard.Enabled = true + config.WireGuard.PrivateKey = cmd.privateKey + config.Storage = cmd.storage + config.VPN.AllowedIPs = []string{"0.0.0.0/0"} + config.DNS.Enabled = true + config.AdminPassword = cmd.adminPassword + config.AdminSubject = cmd.adminUsername + + if cmd.upstreamDNS != "" { + config.DNS.Upstream = []string{cmd.upstreamDNS} + } + + if cmd.configPath != "" { + if b, err := ioutil.ReadFile(cmd.configPath); err == nil { + if err := yaml.Unmarshal(b, &config); err != nil { + logrus.Fatal(errors.Wrap(err, "failed to bind configuration file")) + } + } + } + + if config.LogLevel != "" { + level, err := logrus.ParseLevel(config.LogLevel) + if err != nil { + logrus.Fatal(errors.Wrap(err, "invalid log level - should be one of fatal, error, warn, info, debug, trace")) + } + logrus.SetLevel(level) + } + + if config.DisableMetadata { + logrus.Info("Metadata collection has been disabled. No metrics or device connectivity information will be recorded or shown") + } + + if config.VPN.GatewayInterface == "" { + iface, err := defaultInterface() + if err != nil { + logrus.Warn(errors.Wrap(err, "failed to set default value for VPN.GatewayInterface")) + } else { + config.VPN.GatewayInterface = iface + } + } + + if config.WireGuard.PrivateKey == "" { + if !strings.HasPrefix(config.Storage, "memory://") { + logrus.Fatal(missingPrivateKey) + } + key, err := wgtypes.GeneratePrivateKey() + if err != nil { + logrus.Fatal(errors.Wrap(err, "failed to generate a server private key")) + } + config.WireGuard.PrivateKey = key.String() + } + + if config.AdminPassword != "" && config.AdminSubject != "" { + if config.Auth.Basic == nil { + config.Auth.Basic = &authconfig.BasicAuthConfig{} + } + // htpasswd.AcceptBcrypt(config.AdminPassword) + pw, err := bcrypt.GenerateFromPassword([]byte(config.AdminPassword), bcrypt.DefaultCost) + if err != nil { + logrus.Fatal(errors.Wrap(err, "failed to generate a bcrypt hash for the provided admin password")) + } + config.Auth.Basic.Users = append(config.Auth.Basic.Users, fmt.Sprintf("%s:%s", config.AdminSubject, string(pw))) + } + + return config +} + +func claimsMiddleware(conf *config.AppConfig) authsession.ClaimsMiddleware { + return func(user *authsession.Identity) error { + if user.Subject == conf.AdminSubject { + user.Claims.Add("admin", "true") + } + return nil + } +} + +func defaultInterface() (string, error) { + links, err := netlink.LinkList() + if err != nil { + return "", errors.Wrap(err, "failed to list network interfaces") + } + for _, link := range links { + routes, err := netlink.RouteList(link, 4) + if err != nil { + return "", errors.Wrapf(err, "failed to list routes for interface %s", link.Attrs().Name) + } + for _, route := range routes { + if route.Dst == nil { + return link.Attrs().Name, nil + } + } + } + return "", errors.New("could not determine the default network interface name") +} + +var missingPrivateKey = `missing wireguard private key: + + create a key: + + $ wg genkey + + configure via environment variable: + + $ export WIREGUARD_PRIVATE_KEY="" + + or configure via flag: + + $ wg-access-server serve --wireguard-private-key="" + + or configure via file: + + wireguard: + privateKey: "" + +` diff --git a/codegen.sh b/codegen.sh index 35c26aa..947c7f3 100755 --- a/codegen.sh +++ b/codegen.sh @@ -1,7 +1,7 @@ -#!/bin/bash +#!/bin/sh set -e -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +SCRIPT_DIR="$(dirname $0)" OUT_DIR="$SCRIPT_DIR/proto/proto" mkdir -p "$OUT_DIR" || true diff --git a/docker-compose.yml b/docker-compose.yml index 21348df..7832676 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,10 @@ services: volumes: - "wg-access-server-data:/data" # - "./config.yaml:/config.yaml" # if you have a custom config file + environment: + - "ADMIN_USERNAME=admin" + - "ADMIN_PASSWORD=${ADMIN_PASSWORD:?\n\nplease set the ADMIN_PASSWORD environment variable:\n export ADMIN_PASSWORD=example\n}" + - "WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY:?\n\nplease set the WIREGUARD_PRIVATE_KEY environment variable:\n export WIREGUARD_PRIVATE_KEY=$(wg genkey)\n}" ports: - "8000:8000/tcp" - "51820:51820/udp" diff --git a/go.sum b/go.sum index 11c4210..d453f63 100644 --- a/go.sum +++ b/go.sum @@ -144,6 +144,8 @@ github.com/place1/wg-embed v0.2.0 h1:JzdDeDDWqzY7w6/JJ/fW+q/Qc7AzI/i1GqyeFNf/HT0 github.com/place1/wg-embed v0.2.0/go.mod h1:i09dm8AEkurC4oATFxjvyH0+e1pdmtZoNk2FfPupROI= github.com/place1/wg-embed v0.3.0 h1:n7piTgnp3MgyceBEAD/A7ZiLA4kH8qkqCTVPLBHj6SE= github.com/place1/wg-embed v0.3.0/go.mod h1:i09dm8AEkurC4oATFxjvyH0+e1pdmtZoNk2FfPupROI= +github.com/place1/wg-embed v0.4.0 h1:rToHj4+TuI2ruv2mz3Y16vvisv280BuzdojsGGNQ/pM= +github.com/place1/wg-embed v0.4.0/go.mod h1:i09dm8AEkurC4oATFxjvyH0+e1pdmtZoNk2FfPupROI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU= diff --git a/internal/config/config.go b/internal/config/config.go index db8c9ab..7d233b4 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,22 +1,7 @@ package config import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "runtime" - - "gopkg.in/yaml.v2" - "github.com/place1/wg-access-server/pkg/authnz/authconfig" - "github.com/vishvananda/netlink" - - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "golang.org/x/crypto/bcrypt" - "golang.zx2c4.com/wireguard/wgctrl/wgtypes" - "gopkg.in/alecthomas/kingpin.v2" ) type AppConfig struct { @@ -93,130 +78,3 @@ type AppConfig struct { // the server will not require any authentication. Auth authconfig.AuthConfig `yaml:"auth"` } - -var ( - app = kingpin.New("wg-access-server", "An all-in-one WireGuard Access Server & VPN solution") - configPath = app.Flag("config", "Path to a config file").Envar("CONFIG").String() - logLevel = app.Flag("log-level", "Log level (debug, info, error)").Envar("LOG_LEVEL").Default("info").String() - webPort = app.Flag("web-port", "The port that the web ui server will listen on").Envar("WEB_PORT").Default("8000").Int() - wireguardPort = app.Flag("wireguard-port", "The port that the Wireguard server will listen on").Envar("WIREGUARD_PORT").Default("51820").Int() - storage = app.Flag("storage", "The storage backend connection string").Envar("STORAGE").Default("memory://").String() - privateKey = app.Flag("wireguard-private-key", "Wireguard private key").Envar("WIREGUARD_PRIVATE_KEY").String() - disableMetadata = app.Flag("disable-metadata", "Disable metadata collection (i.e. metrics)").Envar("DISABLE_METADATA").Default("false").Bool() - adminUsername = app.Flag("admin-username", "Admin username (defaults to admin)").Envar("ADMIN_USERNAME").String() - adminPassword = app.Flag("admin-password", "Admin password (provide plaintext, stored in-memory only)").Envar("ADMIN_PASSWORD").String() - upstreamDNS = app.Flag("upstream-dns", "An upstream DNS server to proxy DNS traffic to").Envar("UPSTREAM_DNS").String() -) - -func Read() *AppConfig { - kingpin.MustParse(app.Parse(os.Args[1:])) - - // here we're filling out the config struct - // with values from our flags/defaults. - config := AppConfig{} - config.LogLevel = *logLevel - config.Port = *webPort - config.WireGuard.InterfaceName = "wg0" - config.WireGuard.Port = *wireguardPort - config.VPN.CIDR = "10.44.0.0/24" - config.DisableMetadata = *disableMetadata - config.WireGuard.Enabled = true - config.WireGuard.PrivateKey = *privateKey - config.Storage = *storage - config.VPN.AllowedIPs = []string{"0.0.0.0/0"} - config.DNS.Enabled = true - config.AdminPassword = *adminPassword - config.AdminSubject = *adminUsername - - if *upstreamDNS != "" { - config.DNS.Upstream = []string{*upstreamDNS} - } - - if *configPath != "" { - if b, err := ioutil.ReadFile(*configPath); err == nil { - if err := yaml.Unmarshal(b, &config); err != nil { - logrus.Fatal(errors.Wrap(err, "failed to bind configuration file")) - } - } - } - - level, err := logrus.ParseLevel(config.LogLevel) - if err != nil { - logrus.Fatal(errors.Wrap(err, "invalid log level - should be one of fatal, error, warn, info, debug, trace")) - } - - logrus.SetLevel(level) - logrus.SetReportCaller(true) - logrus.SetFormatter(&logrus.TextFormatter{ - CallerPrettyfier: func(f *runtime.Frame) (string, string) { - return "", fmt.Sprintf("%s:%d", filepath.Base(f.File), f.Line) - }, - }) - - if config.DisableMetadata { - logrus.Info("Metadata collection has been disabled. No metrics or device connectivity information will be recorded or shown") - } - - if config.VPN.GatewayInterface == "" { - iface, err := defaultInterface() - if err != nil { - logrus.Warn(errors.Wrap(err, "failed to set default value for VPN.GatewayInterface")) - } else { - config.VPN.GatewayInterface = iface - } - } - - if config.WireGuard.PrivateKey == "" { - logrus.Warn("no private key has been configured! using an in-memory private key that will be lost when the process exits!") - key, err := wgtypes.GeneratePrivateKey() - if err != nil { - logrus.Fatal(errors.Wrap(err, "failed to generate a server private key")) - } - config.WireGuard.PrivateKey = key.String() - } - - if config.AdminPassword != "" && config.AdminSubject != "" { - if config.Auth.Basic == nil { - config.Auth.Basic = &authconfig.BasicAuthConfig{} - } - // htpasswd.AcceptBcrypt(config.AdminPassword) - pw, err := bcrypt.GenerateFromPassword([]byte(config.AdminPassword), bcrypt.DefaultCost) - if err != nil { - logrus.Fatal(errors.Wrap(err, "failed to generate a bcrypt hash for the provided admin password")) - } - config.Auth.Basic.Users = append(config.Auth.Basic.Users, fmt.Sprintf("%s:%s", config.AdminSubject, string(pw))) - } - - return &config -} - -func defaultInterface() (string, error) { - links, err := netlink.LinkList() - if err != nil { - return "", errors.Wrap(err, "failed to list network interfaces") - } - for _, link := range links { - routes, err := netlink.RouteList(link, 4) - if err != nil { - return "", errors.Wrapf(err, "failed to list routes for interface %s", link.Attrs().Name) - } - for _, route := range routes { - if route.Dst == nil { - return link.Attrs().Name, nil - } - } - } - return "", errors.New("could not determine the default network interface name") -} - -// func randomPassword() string { -// letterRunes := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") -// length := 12 - -// b := make([]rune, length) -// for i := range b { -// b[i] = letterRunes[rand.Intn(len(letterRunes))] -// } - -// return string(b) -// } diff --git a/internal/storage/file.go b/internal/storage/file.go index acfe434..7feb907 100644 --- a/internal/storage/file.go +++ b/internal/storage/file.go @@ -66,7 +66,7 @@ func (s *FileStorage) List(username string) ([]*Device, error) { } p := strings.TrimPrefix(path, s.directory) p = strings.TrimPrefix(p, string(os.PathSeparator)) - if strings.HasPrefix(p, prefix) { + if strings.HasPrefix(p, prefix) && filepath.Ext(path) == ".json" { files = append(files, path) } return nil diff --git a/main.go b/main.go index 6c06896..a1f97fa 100644 --- a/main.go +++ b/main.go @@ -2,153 +2,47 @@ package main import ( "fmt" - "net/http" - - "github.com/place1/wg-access-server/internal/services" - "github.com/place1/wg-access-server/internal/storage" - "github.com/place1/wg-access-server/pkg/authnz" - "github.com/place1/wg-access-server/pkg/authnz/authsession" - - "github.com/gorilla/mux" - "github.com/place1/wg-embed/pkg/wgembed" + "os" + "path/filepath" + "runtime" "github.com/pkg/errors" - "github.com/place1/wg-access-server/internal/config" - "github.com/place1/wg-access-server/internal/devices" - "github.com/place1/wg-access-server/internal/dnsproxy" - "github.com/place1/wg-access-server/internal/network" + "github.com/place1/wg-access-server/cmd/migrate" + "github.com/place1/wg-access-server/cmd/serve" "github.com/sirupsen/logrus" + "gopkg.in/alecthomas/kingpin.v2" ) -func main() { - conf := config.Read() - - // The server's IP within the VPN virtual network - vpnip := network.ServerVPNIP(conf.VPN.CIDR) - - // WireGuard Server - wg := wgembed.NewNoOpInterface() - if conf.WireGuard.Enabled { - wgimpl, err := wgembed.New(conf.WireGuard.InterfaceName) - if err != nil { - logrus.Fatal(errors.Wrap(err, "failed to create wireguard interface")) - } - defer wgimpl.Close() - wg = wgimpl - - logrus.Infof("starting wireguard server on 0.0.0.0:%d", conf.WireGuard.Port) - - wgconfig := &wgembed.ConfigFile{ - Interface: wgembed.IfaceConfig{ - PrivateKey: conf.WireGuard.PrivateKey, - Address: vpnip.String(), - ListenPort: &conf.WireGuard.Port, - }, - } - - if err := wg.LoadConfig(wgconfig); err != nil { - logrus.Fatal(errors.Wrap(err, "failed to load wireguard config")) - } +var ( + app = kingpin.New("wg-access-server", "An all-in-one WireGuard Access Server & VPN solution") + logLevel = app.Flag("log-level", "Log level (debug, info, error)").Envar("LOG_LEVEL").Default("info").String() - logrus.Infof("wireguard VPN network is %s", conf.VPN.CIDR) - - if err := network.ConfigureForwarding(conf.WireGuard.InterfaceName, conf.VPN.GatewayInterface, conf.VPN.CIDR, conf.VPN.AllowedIPs); err != nil { - logrus.Fatal(err) - } - } + servecmd = serve.RegisterCommand(app) + migratecmd = migrate.RegisterCommand(app) +) - // DNS Server - if conf.DNS.Enabled { - dns, err := dnsproxy.New(dnsproxy.DNSServerOpts{ - Upstream: conf.DNS.Upstream, - }) - if err != nil { - logrus.Fatal(errors.Wrap(err, "failed to start dns server")) - } - defer dns.Close() - } +func main() { + cmd := kingpin.MustParse(app.Parse(os.Args[1:])) - // Storage - storageBackend, err := storage.NewStorage(conf.Storage) + level, err := logrus.ParseLevel(*logLevel) if err != nil { - logrus.Fatal(errors.Wrap(err, "failed to create storage backend")) - } - if err := storageBackend.Open(); err != nil { - logrus.Fatal(errors.Wrap(err, "failed to connect/open storage backend")) - } - defer storageBackend.Close() - - // Services - deviceManager := devices.New(wg, storageBackend, conf.VPN.CIDR) - if err := deviceManager.StartSync(conf.DisableMetadata); err != nil { - logrus.Fatal(errors.Wrap(err, "failed to sync")) - } - - router := mux.NewRouter() - router.Use(services.TracesMiddleware) - router.Use(services.RecoveryMiddleware) - - // Health check endpoint - router.PathPrefix("/health").Handler(services.HealthEndpoint()) - - // Authentication middleware - if conf.Auth.IsEnabled() { - router.Use(authnz.NewMiddleware(conf.Auth, claimsMiddleware(conf))) - } else { - logrus.Warn("[DEPRECATION NOTICE] using wg-access-server without an admin user is deprecated and will be removed in an upcoming minor release.") - router.Use(func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - next.ServeHTTP(w, r.WithContext(authsession.SetIdentityCtx(r.Context(), &authsession.AuthSession{ - Identity: &authsession.Identity{ - Subject: "", - }, - }))) - }) - }) - } - - // Subrouter for our site (web + api) - site := router.PathPrefix("/").Subrouter() - site.Use(authnz.RequireAuthentication) - - // Grpc api - site.PathPrefix("/api").Handler(services.ApiRouter(&services.ApiServices{ - Config: conf, - DeviceManager: deviceManager, - Wg: wg, - })) - - // Static website - site.PathPrefix("/").Handler(services.WebsiteRouter()) - - // publicRouter.NotFoundHandler = authMiddleware.Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // if authsession.Authenticated(r.Context()) { - // router.ServeHTTP(w, r) - // } else { - // http.Redirect(w, r, "/signin", http.StatusTemporaryRedirect) - // } - // })) - publicRouter := router - - // Listen - address := fmt.Sprintf("0.0.0.0:%d", conf.Port) - srv := &http.Server{ - Addr: address, - Handler: publicRouter, - } - - // Start Web server - logrus.Infof("web ui listening on %v", address) - if err := srv.ListenAndServe(); err != nil { - logrus.Fatal(errors.Wrap(err, "unable to start http server")) - } -} - -func claimsMiddleware(conf *config.AppConfig) authsession.ClaimsMiddleware { - return func(user *authsession.Identity) error { - if user.Subject == conf.AdminSubject { - user.Claims.Add("admin", "true") - } - return nil + logrus.Fatal(errors.Wrap(err, "invalid log level - should be one of fatal, error, warn, info, debug, trace")) + } + + logrus.SetLevel(level) + logrus.SetReportCaller(true) + logrus.SetFormatter(&logrus.TextFormatter{ + CallerPrettyfier: func(f *runtime.Frame) (string, string) { + return "", fmt.Sprintf("%s:%d", filepath.Base(f.File), f.Line) + }, + }) + + switch cmd { + case servecmd.Name(): + servecmd.Run() + case migratecmd.Name(): + migratecmd.Run() + default: + logrus.Fatal(fmt.Errorf("unknown command: %s", cmd)) } }