From a9be53899c292ce3d66361145522016105e2cd01 Mon Sep 17 00:00:00 2001 From: 0xCA <0xCA@users.noreply.github.com> Date: Wed, 27 Dec 2023 13:08:55 +0500 Subject: [PATCH] Subnet range selector, interface fixes (#481) --- README.md | 1 + custom/js/helper.js | 6 ++ handler/routes.go | 55 ++++++++++-- main.go | 16 ++++ model/client.go | 1 + templates/base.html | 72 +++++++++++++-- templates/clients.html | 124 +++++++++++++++++++++++++- templates/global_settings.html | 1 - templates/server.html | 1 - util/cache.go | 3 + util/config.go | 83 ++++++++++++++---- util/util.go | 154 ++++++++++++++++++++++++++++++++- 12 files changed, 473 insertions(+), 44 deletions(-) create mode 100644 util/cache.go diff --git a/README.md b/README.md index d585868..c43adc3 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ docker-compose up | `BIND_ADDRESS` | The addresses that can access to the web interface and the port, use unix:///abspath/to/file.socket for unix domain socket. | 0.0.0.0:80 | | `SESSION_SECRET` | The secret key used to encrypt the session cookies. Set this to a random value | N/A | | `SESSION_SECRET_FILE` | Optional filepath for the secret key used to encrypt the session cookies. Leave `SESSION_SECRET` blank to take effect | N/A | +| `SUBNET_RANGES` | The list of address subdivision ranges. Format: `SR Name:10.0.1.0/24; SR2:10.0.2.0/24,10.0.3.0/24` Each CIDR must be inside one of the server interfaces. | N/A | | `WGUI_USERNAME` | The username for the login page. Used for db initialization only | `admin` | | `WGUI_PASSWORD` | The password for the user on the login page. Will be hashed automatically. Used for db initialization only | `admin` | | `WGUI_PASSWORD_FILE` | Optional filepath for the user login password. Will be hashed automatically. Used for db initialization only. Leave `WGUI_PASSWORD` blank to take effect | N/A | diff --git a/custom/js/helper.js b/custom/js/helper.js index 39bc1fa..4f21c1c 100644 --- a/custom/js/helper.js +++ b/custom/js/helper.js @@ -18,6 +18,11 @@ function renderClientList(data) { allowedIpsHtml += `${obj} `; }) + let subnetRangesString = ""; + if (obj.Client.subnet_ranges && obj.Client.subnet_ranges.length > 0) { + subnetRangesString = obj.Client.subnet_ranges.join(',') + } + // render client html content let html = `
@@ -59,6 +64,7 @@ function renderClientList(data) {
${obj.Client.name} + ${obj.Client.email} ${prettyDateTime(obj.Client.created_at)} diff --git a/handler/routes.go b/handler/routes.go index aa5461b..0358750 100644 --- a/handler/routes.go +++ b/handler/routes.go @@ -366,6 +366,10 @@ func GetClients(db store.IStore) echo.HandlerFunc { }) } + for i, clientData := range clientDataList { + clientDataList[i] = util.FillClientSubnetRange(clientData) + } + return c.JSON(http.StatusOK, clientDataList) } } @@ -391,7 +395,7 @@ func GetClient(db store.IStore) echo.HandlerFunc { return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "Client not found"}) } - return c.JSON(http.StatusOK, clientData) + return c.JSON(http.StatusOK, util.FillClientSubnetRange(clientData)) } } @@ -988,6 +992,13 @@ func MachineIPAddresses() echo.HandlerFunc { } } +// GetOrderedSubnetRanges handler to get the ordered list of subnet ranges +func GetOrderedSubnetRanges() echo.HandlerFunc { + return func(c echo.Context) error { + return c.JSON(http.StatusOK, util.SubnetRangesOrder) + } +} + // SuggestIPAllocation handler to get the list of ip address for client func SuggestIPAllocation(db store.IStore) echo.HandlerFunc { return func(c echo.Context) error { @@ -1009,22 +1020,48 @@ func SuggestIPAllocation(db store.IStore) echo.HandlerFunc { false, "Cannot suggest ip allocation: failed to get list of allocated ip addresses", }) } - for _, cidr := range server.Interface.Addresses { - ip, err := util.GetAvailableIP(cidr, allocatedIPs) + + sr := c.QueryParam("sr") + searchCIDRList := make([]string, 0) + found := false + + // Use subnet range or default to interface addresses + if util.SubnetRanges[sr] != nil { + for _, cidr := range util.SubnetRanges[sr] { + searchCIDRList = append(searchCIDRList, cidr.String()) + } + } else { + searchCIDRList = append(searchCIDRList, server.Interface.Addresses...) + } + + // Save only unique IPs + ipSet := make(map[string]struct{}) + + for _, cidr := range searchCIDRList { + ip, err := util.GetAvailableIP(cidr, allocatedIPs, server.Interface.Addresses) if err != nil { log.Error("Failed to get available ip from a CIDR: ", err) - return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ - false, - fmt.Sprintf("Cannot suggest ip allocation: failed to get available ip from network %s", cidr), - }) + continue } + found = true if strings.Contains(ip, ":") { - suggestedIPs = append(suggestedIPs, fmt.Sprintf("%s/128", ip)) + ipSet[fmt.Sprintf("%s/128", ip)] = struct{}{} } else { - suggestedIPs = append(suggestedIPs, fmt.Sprintf("%s/32", ip)) + ipSet[fmt.Sprintf("%s/32", ip)] = struct{}{} } } + if !found { + return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ + false, + "Cannot suggest ip allocation: failed to get available ip. Try a different subnet or deallocate some ips.", + }) + } + + for ip := range ipSet { + suggestedIPs = append(suggestedIPs, ip) + } + return c.JSON(http.StatusOK, suggestedIPs) } } diff --git a/main.go b/main.go index fd4bc90..5f6e341 100644 --- a/main.go +++ b/main.go @@ -45,6 +45,7 @@ var ( flagSessionSecret string = util.RandomString(32) flagWgConfTemplate string flagBasePath string + flagSubnetRanges string ) const ( @@ -81,6 +82,7 @@ func init() { flag.StringVar(&flagEmailFromName, "email-from-name", util.LookupEnvOrString("EMAIL_FROM_NAME", flagEmailFromName), "'From' email name.") flag.StringVar(&flagWgConfTemplate, "wg-conf-template", util.LookupEnvOrString("WG_CONF_TEMPLATE", flagWgConfTemplate), "Path to custom wg.conf template.") flag.StringVar(&flagBasePath, "base-path", util.LookupEnvOrString("BASE_PATH", flagBasePath), "The base path of the URL") + flag.StringVar(&flagSubnetRanges, "subnet-ranges", util.LookupEnvOrString("SUBNET_RANGES", flagSubnetRanges), "IP ranges to choose from when assigning an IP for a client.") var ( smtpPasswordLookup = util.LookupEnvOrString("SMTP_PASSWORD", flagSmtpPassword) @@ -127,6 +129,7 @@ func init() { util.SessionSecret = []byte(flagSessionSecret) util.WgConfTemplate = flagWgConfTemplate util.BasePath = util.ParseBasePath(flagBasePath) + util.SubnetRanges = util.ParseSubnetRanges(flagSubnetRanges) // print only if log level is INFO or lower if lvl, _ := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO")); lvl <= log.INFO { @@ -145,6 +148,7 @@ func init() { //fmt.Println("Session secret\t:", util.SessionSecret) fmt.Println("Custom wg.conf\t:", util.WgConfTemplate) fmt.Println("Base path\t:", util.BasePath+"/") + fmt.Println("Subnet ranges\t:", util.GetSubnetRangesString()) } } @@ -170,6 +174,17 @@ func main() { // create the wireguard config on start, if it doesn't exist initServerConfig(db, tmplDir) + // Check if subnet ranges are valid for the server configuration + // Remove any non-valid CIDRs + if err := util.ValidateAndFixSubnetRanges(db); err != nil { + panic(err) + } + + // Print valid ranges + if lvl, _ := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO")); lvl <= log.INFO { + fmt.Println("Valid subnet ranges:", util.GetSubnetRangesString()) + } + // register routes app := router.New(tmplDir, extraData, util.SessionSecret) @@ -218,6 +233,7 @@ func main() { app.GET(util.BasePath+"/api/clients", handler.GetClients(db), handler.ValidSession) app.GET(util.BasePath+"/api/client/:id", handler.GetClient(db), handler.ValidSession) app.GET(util.BasePath+"/api/machine-ips", handler.MachineIPAddresses(), handler.ValidSession) + app.GET(util.BasePath+"/api/subnet-ranges", handler.GetOrderedSubnetRanges(), handler.ValidSession) app.GET(util.BasePath+"/api/suggest-client-ips", handler.SuggestIPAllocation(db), handler.ValidSession) app.POST(util.BasePath+"/api/apply-wg-config", handler.ApplyServerConfig(db, tmplDir), handler.ValidSession, handler.ContentTypeJson) app.GET(util.BasePath+"/wake_on_lan_hosts", handler.GetWakeOnLanHosts(db), handler.ValidSession) diff --git a/model/client.go b/model/client.go index 95342f0..187ec72 100644 --- a/model/client.go +++ b/model/client.go @@ -12,6 +12,7 @@ type Client struct { PresharedKey string `json:"preshared_key"` Name string `json:"name"` Email string `json:"email"` + SubnetRanges []string `json:"subnet_ranges,omitempty"` AllocatedIPs []string `json:"allocated_ips"` AllowedIPs []string `json:"allowed_ips"` ExtraAllowedIPs []string `json:"extra_allowed_ips"` diff --git a/templates/base.html b/templates/base.html index c2fa367..3f34140 100644 --- a/templates/base.html +++ b/templates/base.html @@ -58,11 +58,13 @@
@@ -209,6 +211,12 @@
+
+ + +
@@ -368,6 +376,36 @@ $(document).ready(function () { + addGlobalStyle(` +.toast-top-right-fix { + top: 67px; + right: 12px; +} + `, 'toastrToastStyleFix') + + toastr.options.closeDuration = 100; + // toastr.options.timeOut = 10000; + toastr.options.positionClass = 'toast-top-right-fix'; + + updateApplyConfigVisibility() + // from clients.html + updateSearchList() + + }); + + function addGlobalStyle(css, id) { + if (!document.querySelector('#' + id)) { + let head = document.head + if (!head) { return } + let style = document.createElement('style') + style.type = 'text/css' + style.id = id + style.innerHTML = css + head.appendChild(style) + } + } + + function updateApplyConfigVisibility() { $.ajax({ cache: false, method: 'GET', @@ -388,8 +426,7 @@ toastr.error(responseJson['message']); } }); - - }); + } // populateClient function for render new client info @@ -456,6 +493,7 @@ if (window.location.pathname === "{{.basePath}}/") { populateClient(resp.id); } + updateApplyConfigVisibility() }, error: function(jqXHR, exception) { const responseJson = jQuery.parseJSON(jqXHR.responseText); @@ -466,19 +504,32 @@ // updateIPAllocationSuggestion function for automatically fill // the IP Allocation input with suggested ip addresses - function updateIPAllocationSuggestion() { + function updateIPAllocationSuggestion(forceDefault = false) { + let subnetRange = $("#subnet_ranges").select2('val'); + + if (forceDefault || !subnetRange || subnetRange.length === 0) { + subnetRange = '__default_any__' + } $.ajax({ cache: false, method: 'GET', - url: '{{.basePath}}/api/suggest-client-ips', + url: `{{.basePath}}/api/suggest-client-ips?sr=${subnetRange}`, dataType: 'json', contentType: "application/json", success: function(data) { + const allocated_ips = $("#client_allocated_ips").val().split(","); + allocated_ips.forEach(function (item, index) { + $('#client_allocated_ips').removeTag(escape(item)); + }) data.forEach(function (item, index) { $('#client_allocated_ips').addTag(item); }) }, error: function(jqXHR, exception) { + const allocated_ips = $("#client_allocated_ips").val().split(","); + allocated_ips.forEach(function (item, index) { + $('#client_allocated_ips').removeTag(escape(item)); + }) const responseJson = jQuery.parseJSON(jqXHR.responseText); toastr.error(responseJson['message']); } @@ -497,7 +548,6 @@ 'defaultText': 'Add More', 'removeWithBackspace': true, 'minChars': 0, - 'minInputWidth': '100%', 'placeholderColor': '#666666' }); @@ -509,7 +559,6 @@ 'defaultText': 'Add More', 'removeWithBackspace': true, 'minChars': 0, - 'minInputWidth': '100%', 'placeholderColor': '#666666' }); @@ -520,7 +569,6 @@ 'defaultText': 'Add More', 'removeWithBackspace': true, 'minChars': 0, - 'minInputWidth': '100%', 'placeholderColor': '#666666' }); @@ -565,10 +613,17 @@ $("#client_preshared_key").val(""); $("#client_allocated_ips").importTags(''); $("#client_extra_allowed_ips").importTags(''); - updateIPAllocationSuggestion(); + updateSubnetRangesList("#subnet_ranges"); + updateIPAllocationSuggestion(true); }); }); + // handle subnet range select + $('#subnet_ranges').on('select2:select', function (e) { + // console.log('Selected Option: ', $("#subnet_ranges").select2('val')); + updateIPAllocationSuggestion(); + }); + // apply_config_confirm button event $(document).ready(function () { $("#apply_config_confirm").click(function () { @@ -579,6 +634,7 @@ dataType: 'json', contentType: "application/json", success: function(data) { + updateApplyConfigVisibility() $("#modal_apply_config").modal('hide'); toastr.success('Applied config successfully'); }, diff --git a/templates/clients.html b/templates/clients.html index 8b4e4ab..1f8f26d 100644 --- a/templates/clients.html +++ b/templates/clients.html @@ -100,6 +100,12 @@ Wireguard Clients
+
+ + +
@@ -253,13 +259,102 @@ Wireguard Clients setClientStatus(clientID, true); const divElement = document.getElementById("paused_" + clientID); divElement.style.visibility = "hidden"; + updateApplyConfigVisibility() } function pauseClient(clientID) { setClientStatus(clientID, false); const divElement = document.getElementById("paused_" + clientID); divElement.style.visibility = "visible"; + updateApplyConfigVisibility() } + + // updateIPAllocationSuggestion function for automatically fill + // the IP Allocation input with suggested ip addresses + // FOR CHANGING A SUBNET OF AN EXISTING CLIENT + function updateIPAllocationSuggestionExisting() { + let subnetRange = $("#_subnet_ranges").select2('val'); + + if (!subnetRange || subnetRange.length === 0) { + subnetRange = '__default_any__' + } + $.ajax({ + cache: false, + method: 'GET', + url: `{{.basePath}}/api/suggest-client-ips?sr=${subnetRange}`, + dataType: 'json', + contentType: "application/json", + success: function(data) { + const allocated_ips = $("#_client_allocated_ips").val().split(","); + allocated_ips.forEach(function (item, index) { + $('#_client_allocated_ips').removeTag(escape(item)); + }) + data.forEach(function (item, index) { + $('#_client_allocated_ips').addTag(item); + }) + }, + error: function(jqXHR, exception) { + const allocated_ips = $("#_client_allocated_ips").val().split(","); + allocated_ips.forEach(function (item, index) { + $('#_client_allocated_ips').removeTag(escape(item)); + }) + const responseJson = jQuery.parseJSON(jqXHR.responseText); + toastr.error(responseJson['message']); + } + }); + } + + function updateSubnetRangesList(elementID, preselectedVal) { + $.getJSON("{{.basePath}}/api/subnet-ranges", null, function(data) { + $(`${elementID} option`).remove(); + $(elementID).append( + $("") + .text("Any") + .val("__default_any__") + ); + $.each(data, function(index, item) { + $(elementID).append( + $("") + .text(item) + .val(item) + ); + if (item === preselectedVal) { + console.log(preselectedVal); + $(elementID).val(preselectedVal).trigger('change') + } + }); + }); + } + + function updateSearchList() { + $.getJSON("{{.basePath}}/api/subnet-ranges", null, function(data) { + $("#status-selector option").remove(); + $("#status-selector").append( + $("") + .text("All") + .val("All"), + $("") + .text("Enabled") + .val("Enabled"), + $("") + .text("Disabled") + .val("Disabled"), + $("") + .text("Connected") + .val("Connected"), + $("") + .text("Disconnected") + .val("Disconnected") + ); + $.each(data, function(index, item) { + $("#status-selector").append( + $("") + .text(item) + .val(item) + ); + }); + }); +}