diff --git a/geoip/geoip.go b/geoip/geoip.go new file mode 100644 index 0000000..41748ac --- /dev/null +++ b/geoip/geoip.go @@ -0,0 +1,225 @@ +// Package geoip provides GeoLite2 (MaxMind) city geolocation with a locally +// stored database that can be installed/updated on demand or automatically. +package geoip + +import ( + "archive/tar" + "compress/gzip" + "errors" + "fmt" + "io" + "net" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/oschwald/geoip2-golang" +) + +// download endpoint for the GeoLite2-City edition (tar.gz archive) +const downloadURLTemplate = "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=%s&suffix=tar.gz" + +// guard against decompression bombs; the GeoLite2-City db is ~70MB +const maxDBSize = 1 << 30 // 1 GiB + +var ( + mu sync.RWMutex + reader *geoip2.Reader + dbPath = "./db/GeoLite2-City.mmdb" +) + +// Location holds the resolved geolocation of an IP address. +type Location struct { + City string + Country string + CountryISO string + Flag string +} + +// Display renders the location as "🇬🇷 Athens, Greece", omitting empty parts. +func (l Location) Display() string { + var parts []string + if l.City != "" { + parts = append(parts, l.City) + } + if l.Country != "" { + parts = append(parts, l.Country) + } + text := strings.Join(parts, ", ") + switch { + case l.Flag != "" && text != "": + return l.Flag + " " + text + case l.Flag != "": + return l.Flag + default: + return text + } +} + +// SetDBPath configures where the GeoLite2 database is stored/read. +func SetDBPath(p string) { + if strings.TrimSpace(p) != "" { + dbPath = p + } +} + +// DBPath returns the configured database path. +func DBPath() string { + return dbPath +} + +// Load opens the local database (if present) into the reader. +func Load() error { + if _, err := os.Stat(dbPath); err != nil { + return err + } + r, err := geoip2.Open(dbPath) + if err != nil { + return err + } + mu.Lock() + old := reader + reader = r + mu.Unlock() + if old != nil { + old.Close() + } + return nil +} + +// Available reports whether a database is loaded and ready for lookups. +func Available() bool { + mu.RLock() + defer mu.RUnlock() + return reader != nil +} + +// LastUpdated returns the database file's modification time. +func LastUpdated() (time.Time, bool) { + fi, err := os.Stat(dbPath) + if err != nil { + return time.Time{}, false + } + return fi.ModTime(), true +} + +// NeedsUpdate reports whether the database is missing or older than maxAge. +func NeedsUpdate(maxAge time.Duration) bool { + t, ok := LastUpdated() + if !ok { + return true + } + return time.Since(t) > maxAge +} + +// Lookup geolocates an IP address (host only, without port). +func Lookup(ipStr string) (Location, error) { + mu.RLock() + r := reader + mu.RUnlock() + if r == nil { + return Location{}, errors.New("geoip database not loaded") + } + ip := net.ParseIP(ipStr) + if ip == nil { + return Location{}, fmt.Errorf("invalid ip address: %q", ipStr) + } + record, err := r.City(ip) + if err != nil { + return Location{}, err + } + loc := Location{ + City: record.City.Names["en"], + Country: record.Country.Names["en"], + CountryISO: record.Country.IsoCode, + } + loc.Flag = flagEmoji(loc.CountryISO) + return loc, nil +} + +// flagEmoji converts a two-letter ISO country code into its flag emoji using +// Unicode regional indicator symbols. +func flagEmoji(iso string) string { + if len(iso) != 2 { + return "" + } + iso = strings.ToUpper(iso) + runes := make([]rune, 0, 2) + for _, c := range iso { + if c < 'A' || c > 'Z' { + return "" + } + runes = append(runes, rune(0x1F1E6+(c-'A'))) + } + return string(runes) +} + +// Update downloads the latest GeoLite2-City database with the given MaxMind +// license key, atomically replaces the local file, and reloads the reader. +func Update(licenseKey string) error { + licenseKey = strings.TrimSpace(licenseKey) + if licenseKey == "" { + return errors.New("MaxMind license key is not set") + } + + client := &http.Client{Timeout: 5 * time.Minute} + resp, err := client.Get(fmt.Sprintf(downloadURLTemplate, licenseKey)) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("download failed with HTTP %d (check your MaxMind license key)", resp.StatusCode) + } + + gz, err := gzip.NewReader(resp.Body) + if err != nil { + return err + } + defer gz.Close() + + if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil { + return err + } + + tmpPath := dbPath + ".tmp" + tr := tar.NewReader(gz) + found := false + for { + hdr, err := tr.Next() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return err + } + if !strings.HasSuffix(hdr.Name, ".mmdb") { + continue + } + out, err := os.Create(tmpPath) + if err != nil { + return err + } + if _, err := io.Copy(out, io.LimitReader(tr, maxDBSize)); err != nil { + out.Close() + os.Remove(tmpPath) + return err + } + out.Close() + found = true + break + } + if !found { + return errors.New("no .mmdb file found in the downloaded archive") + } + + _ = os.Chmod(tmpPath, 0600) + if err := os.Rename(tmpPath, dbPath); err != nil { + os.Remove(tmpPath) + return err + } + return Load() +} diff --git a/geoip/geoip_test.go b/geoip/geoip_test.go new file mode 100644 index 0000000..eaa61a6 --- /dev/null +++ b/geoip/geoip_test.go @@ -0,0 +1,52 @@ +package geoip + +import "testing" + +func TestFlagEmoji(t *testing.T) { + us := string([]rune{0x1F1FA, 0x1F1F8}) + gr := string([]rune{0x1F1EC, 0x1F1F7}) + gb := string([]rune{0x1F1EC, 0x1F1E7}) + + cases := map[string]string{ + "US": us, + "GR": gr, + "gb": gb, // lowercase is normalized + "": "", + "U": "", // too short + "USA": "", // too long + "1A": "", // non-letter + } + for in, want := range cases { + if got := flagEmoji(in); got != want { + t.Errorf("flagEmoji(%q) = %q, want %q", in, got, want) + } + } +} + +func TestLocationDisplay(t *testing.T) { + flag := string([]rune{0x1F1EC, 0x1F1F7}) + cases := []struct { + loc Location + want string + }{ + {Location{City: "Athens", Country: "Greece", Flag: flag}, flag + " Athens, Greece"}, + {Location{Country: "Greece", Flag: flag}, flag + " Greece"}, + {Location{Flag: flag}, flag}, + {Location{City: "Athens", Country: "Greece"}, "Athens, Greece"}, + {Location{}, ""}, + } + for _, c := range cases { + if got := c.loc.Display(); got != c.want { + t.Errorf("Display(%+v) = %q, want %q", c.loc, got, c.want) + } + } +} + +func TestLookupNotLoaded(t *testing.T) { + mu.Lock() + reader = nil + mu.Unlock() + if _, err := Lookup("8.8.8.8"); err == nil { + t.Error("expected error when database not loaded, got nil") + } +} diff --git a/go.mod b/go.mod index e9647ca..e8fe2cf 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,8 @@ require ( gopkg.in/go-playground/validator.v9 v9.31.0 ) +require github.com/oschwald/geoip2-golang v1.11.0 + require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect @@ -38,13 +40,14 @@ require ( github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/socket v0.5.0 // indirect + github.com/oschwald/maxminddb-golang v1.13.0 // indirect github.com/sendgrid/rest v2.6.9+incompatible // indirect github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/sync v0.5.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/sys v0.20.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b // indirect diff --git a/go.sum b/go.sum index 3aa2ceb..743145c 100644 --- a/go.sum +++ b/go.sum @@ -78,6 +78,10 @@ github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= 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/oschwald/geoip2-golang v1.11.0 h1:hNENhCn1Uyzhf9PTmquXENiWS6AlxAEnBII6r8krA3w= +github.com/oschwald/geoip2-golang v1.11.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo= +github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU= +github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o= 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/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= @@ -100,8 +104,8 @@ github.com/stretchr/testify v0.0.0-20150929183540-2b15294402a8/go.mod h1:a8OnRci github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM= github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -160,8 +164,8 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/handler/routes.go b/handler/routes.go index 7034106..c9aa381 100644 --- a/handler/routes.go +++ b/handler/routes.go @@ -25,6 +25,7 @@ import ( "golang.zx2c4.com/wireguard/wgctrl/wgtypes" "github.com/ngoduykhanh/wireguard-ui/emailer" + "github.com/ngoduykhanh/wireguard-ui/geoip" "github.com/ngoduykhanh/wireguard-ui/model" "github.com/ngoduykhanh/wireguard-ui/store" "github.com/ngoduykhanh/wireguard-ui/telegram" @@ -909,9 +910,16 @@ func GlobalSettings(db store.IStore) echo.HandlerFunc { log.Error("Cannot get global settings: ", err) } + geoLiteLastUpdated := "" + if t, ok := geoip.LastUpdated(); ok { + geoLiteLastUpdated = t.UTC().Format("2006-01-02 15:04:05 MST") + } + return c.Render(http.StatusOK, "global_settings.html", map[string]interface{}{ - "baseData": model.BaseData{Active: "global-settings", CurrentUser: currentUser(c), Admin: isAdmin(c)}, - "globalSettings": globalSettings, + "baseData": model.BaseData{Active: "global-settings", CurrentUser: currentUser(c), Admin: isAdmin(c)}, + "globalSettings": globalSettings, + "geoLiteAvailable": geoip.Available(), + "geoLiteLastUpdated": geoLiteLastUpdated, }) } } @@ -939,6 +947,7 @@ func Status(db store.IStore) echo.HandlerFunc { Connected bool AllocatedIP string Endpoint string + Location string } type DeviceVM struct { @@ -1007,6 +1016,11 @@ func Status(db store.IStore) echo.HandlerFunc { if isAdmin(c) { pVm.Endpoint = devices[i].Peers[j].Endpoint.String() + if endpoint := devices[i].Peers[j].Endpoint; endpoint != nil && endpoint.IP != nil && geoip.Available() { + if loc, err := geoip.Lookup(endpoint.IP.String()); err == nil { + pVm.Location = loc.Display() + } + } } if _client, ok := m[pVm.PublicKey]; ok { @@ -1055,6 +1069,28 @@ func GlobalSettingSubmit(db store.IStore) echo.HandlerFunc { } } +// GeoLiteUpdate handler downloads/updates the GeoLite2 database using the +// MaxMind license key configured in the global settings. +func GeoLiteUpdate(db store.IStore) echo.HandlerFunc { + return func(c echo.Context) error { + globalSettings, err := db.GetGlobalSettings() + if err != nil { + return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Cannot get global settings"}) + } + + if err := geoip.Update(globalSettings.MaxmindLicenseKey); err != nil { + log.Error("Cannot update GeoLite2 database: ", err) + return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, err.Error()}) + } + + msg := "GeoLite2 database updated successfully" + if t, ok := geoip.LastUpdated(); ok { + msg = fmt.Sprintf("GeoLite2 database updated successfully (%s)", t.UTC().Format("2006-01-02 15:04:05 MST")) + } + return c.JSON(http.StatusOK, jsonHTTPResponse{true, msg}) + } +} + // MachineIPAddresses handler to get local interface ip addresses func MachineIPAddresses() echo.HandlerFunc { return func(c echo.Context) error { diff --git a/main.go b/main.go index 9e78b75..a95e78e 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,7 @@ import ( "github.com/labstack/echo/v4" "github.com/labstack/gommon/log" + "github.com/ngoduykhanh/wireguard-ui/geoip" "github.com/ngoduykhanh/wireguard-ui/store" "github.com/ngoduykhanh/wireguard-ui/telegram" @@ -56,6 +57,7 @@ var ( flagBrandText = "WireGuard UI" flagAccentColor = "#343a40" flagPageTitlePrefix string + flagGeoLite2DBPath = "./db/GeoLite2-City.mmdb" ) const ( @@ -101,6 +103,7 @@ func init() { flag.StringVar(&flagBrandText, "brand-text", util.LookupEnvOrString("WGUI_BRAND_TEXT", flagBrandText), "The UI brand text or name") flag.StringVar(&flagAccentColor, "accent-color", util.LookupEnvOrString("WGUI_ACCENT_COLOR", flagAccentColor), "The UI accent color") flag.StringVar(&flagPageTitlePrefix, "page-title-prefix", util.LookupEnvOrString("WGUI_PAGE_TITLE_PREFIX", flagPageTitlePrefix), "The prefix of the page title") + flag.StringVar(&flagGeoLite2DBPath, "geolite-db-path", util.LookupEnvOrString(util.GeoLite2DBPathEnvVar, flagGeoLite2DBPath), "Path to the local GeoLite2-City database file") var ( smtpPasswordLookup = util.LookupEnvOrString("SMTP_PASSWORD", flagSmtpPassword) @@ -153,6 +156,7 @@ func init() { util.BrandText = flagBrandText util.AccentColor = flagAccentColor util.PageTitlePrefix = flagPageTitlePrefix + util.GeoLite2DBPath = flagGeoLite2DBPath lvl, _ := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO")) @@ -207,6 +211,9 @@ func main() { // create the wireguard config on start, if it doesn't exist initServerConfig(db, tmplDir) + // load the GeoLite2 database and auto-update it if stale + initGeoLite2(db) + // Check if subnet ranges are valid for the server configuration // Remove any non-valid CIDRs if err := util.ValidateAndFixSubnetRanges(db); err != nil { @@ -265,6 +272,7 @@ func main() { app.POST(util.BasePath+"/wg-server/keypair", handler.WireGuardServerKeyPair(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) app.GET(util.BasePath+"/global-settings", handler.GlobalSettings(db), handler.ValidSession, handler.RefreshSession, handler.NeedsAdmin) app.POST(util.BasePath+"/global-settings", handler.GlobalSettingSubmit(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) + app.POST(util.BasePath+"/api/geolite/update", handler.GeoLiteUpdate(db), handler.ValidSession, handler.ContentTypeJson, handler.NeedsAdmin) app.GET(util.BasePath+"/status", handler.Status(db), handler.ValidSession, handler.RefreshSession) app.GET(util.BasePath+"/api/clients", handler.GetClients(db), handler.ValidSession) app.GET(util.BasePath+"/api/client/:id", handler.GetClient(db), handler.ValidSession) @@ -343,6 +351,38 @@ func initServerConfig(db store.IStore, tmplDir fs.FS) { } } +// initGeoLite2 loads the local GeoLite2 database and, if a MaxMind license key +// is configured and the database is missing or older than 7 days, updates it in +// the background so it does not delay startup. +func initGeoLite2(db store.IStore) { + geoip.SetDBPath(util.GeoLite2DBPath) + + if err := geoip.Load(); err != nil { + log.Infof("GeoLite2 database not loaded yet: %v", err) + } + + settings, err := db.GetGlobalSettings() + if err != nil { + log.Warnf("Cannot get global settings for GeoLite2: %v", err) + return + } + if strings.TrimSpace(settings.MaxmindLicenseKey) == "" { + return + } + if !geoip.NeedsUpdate(7 * 24 * time.Hour) { + return + } + + go func() { + log.Info("GeoLite2 database is missing or older than 7 days, updating...") + if err := geoip.Update(settings.MaxmindLicenseKey); err != nil { + log.Warnf("GeoLite2 auto-update failed: %v", err) + return + } + log.Info("GeoLite2 database updated successfully") + }() +} + func initTelegram(initDeps telegram.TgBotInitDependencies) { go func() { for { diff --git a/model/setting.go b/model/setting.go index c9e152c..4810b70 100644 --- a/model/setting.go +++ b/model/setting.go @@ -13,5 +13,6 @@ type GlobalSetting struct { FirewallMark string `json:"firewall_mark"` Table string `json:"table"` ConfigFilePath string `json:"config_file_path"` + MaxmindLicenseKey string `json:"maxmind_license_key"` UpdatedAt time.Time `json:"updated_at"` } diff --git a/store/jsondb/jsondb.go b/store/jsondb/jsondb.go index 1cd0a43..162e001 100644 --- a/store/jsondb/jsondb.go +++ b/store/jsondb/jsondb.go @@ -111,6 +111,7 @@ func (o *JsonDB) Init() error { globalSetting.FirewallMark = util.LookupEnvOrString(util.FirewallMarkEnvVar, util.DefaultFirewallMark) globalSetting.Table = util.LookupEnvOrString(util.TableEnvVar, util.DefaultTable) globalSetting.ConfigFilePath = util.LookupEnvOrString(util.ConfigFilePathEnvVar, util.DefaultConfigFilePath) + globalSetting.MaxmindLicenseKey = util.LookupEnvOrString(util.MaxmindLicenseKeyEnvVar, "") globalSetting.UpdatedAt = time.Now().UTC() o.conn.Write("server", "global_settings", globalSetting) err := util.ManagePerms(globalSettingPath) diff --git a/templates/global_settings.html b/templates/global_settings.html index 1c667b8..46d73ea 100644 --- a/templates/global_settings.html +++ b/templates/global_settings.html @@ -73,6 +73,24 @@ Global Settings name="config_file_path" placeholder="E.g. /etc/wireguard/wg0.conf" value="{{ .globalSettings.ConfigFilePath }}"> +
+ + +
+
+ +
+ + + + +
+
@@ -113,6 +131,12 @@ Global Settings
7. WireGuard Config File Path
The path of your WireGuard server config file. Please make sure the parent directory exists and is writable.
+
8. MaxMind License Key
+
Your MaxMind license key, used to download the GeoLite2-City database. Create a free + account at maxmind.com to generate one. Click + Install / Update to download it locally; it also auto-updates on startup + when older than 7 days. Once installed, a Location column + (flag, city, country) is shown next to each peer's endpoint on the Status page.
@@ -152,23 +176,27 @@ Global Settings {{define "bottom_js"}} diff --git a/util/config.go b/util/config.go index 54a566d..49f22c9 100644 --- a/util/config.go +++ b/util/config.go @@ -31,6 +31,7 @@ var ( BrandText string AccentColor string PageTitlePrefix string + GeoLite2DBPath string ) const ( @@ -45,6 +46,7 @@ const ( DefaultFirewallMark = "0xca6c" // i.e. 51820 DefaultTable = "auto" DefaultConfigFilePath = "/etc/wireguard/wg0.conf" + DefaultGeoLite2DBPath = "./db/GeoLite2-City.mmdb" UsernameEnvVar = "WGUI_USERNAME" PasswordEnvVar = "WGUI_PASSWORD" PasswordFileEnvVar = "WGUI_PASSWORD_FILE" @@ -71,6 +73,8 @@ const ( AccentColorEnvVar = "WGUI_ACCENT_COLOR" PageTitlePrefixEnvVar = "WGUI_PAGE_TITLE_PREFIX" LogoFilePathEnvVar = "WGUI_LOGO_FILE_PATH" + MaxmindLicenseKeyEnvVar = "WGUI_MAXMIND_LICENSE_KEY" + GeoLite2DBPathEnvVar = "WGUI_GEOLITE_DB_PATH" ) func ParseBasePath(basePath string) string {