Subnet range selector, interface fixes (#481)
This commit is contained in:
		
							parent
							
								
									e73047b14f
								
							
						
					
					
						commit
						a9be53899c
					
				|  | @ -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                         | | | `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`            | 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                                | | | `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_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`             | 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                                | | | `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                                | | ||||||
|  |  | ||||||
|  | @ -18,6 +18,11 @@ function renderClientList(data) { | ||||||
|             allowedIpsHtml += `<small class="badge badge-secondary">${obj}</small> `; |             allowedIpsHtml += `<small class="badge badge-secondary">${obj}</small> `; | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|  |         let subnetRangesString = ""; | ||||||
|  |         if (obj.Client.subnet_ranges && obj.Client.subnet_ranges.length > 0) { | ||||||
|  |             subnetRangesString = obj.Client.subnet_ranges.join(',') | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         // render client html content
 |         // render client html content
 | ||||||
|         let html = `<div class="col-sm-6 col-md-6 col-lg-4" id="client_${obj.Client.id}">
 |         let html = `<div class="col-sm-6 col-md-6 col-lg-4" id="client_${obj.Client.id}">
 | ||||||
|                         <div class="info-box"> |                         <div class="info-box"> | ||||||
|  | @ -59,6 +64,7 @@ function renderClientList(data) { | ||||||
|                                 <hr> |                                 <hr> | ||||||
|                                 <span class="info-box-text"><i class="fas fa-user"></i> ${obj.Client.name}</span> |                                 <span class="info-box-text"><i class="fas fa-user"></i> ${obj.Client.name}</span> | ||||||
|                                 <span class="info-box-text" style="display: none"><i class="fas fa-key"></i> ${obj.Client.public_key}</span> |                                 <span class="info-box-text" style="display: none"><i class="fas fa-key"></i> ${obj.Client.public_key}</span> | ||||||
|  |                                 <span class="info-box-text" style="display: none"><i class="fas fa-subnetrange"></i>${subnetRangesString}</span> | ||||||
|                                 <span class="info-box-text"><i class="fas fa-envelope"></i> ${obj.Client.email}</span> |                                 <span class="info-box-text"><i class="fas fa-envelope"></i> ${obj.Client.email}</span> | ||||||
|                                 <span class="info-box-text"><i class="fas fa-clock"></i> |                                 <span class="info-box-text"><i class="fas fa-clock"></i> | ||||||
|                                     ${prettyDateTime(obj.Client.created_at)}</span> |                                     ${prettyDateTime(obj.Client.created_at)}</span> | ||||||
|  |  | ||||||
|  | @ -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) | 		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.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
 | // SuggestIPAllocation handler to get the list of ip address for client
 | ||||||
| func SuggestIPAllocation(db store.IStore) echo.HandlerFunc { | func SuggestIPAllocation(db store.IStore) echo.HandlerFunc { | ||||||
| 	return func(c echo.Context) error { | 	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", | 				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 { | 			if err != nil { | ||||||
| 				log.Error("Failed to get available ip from a CIDR: ", err) | 				log.Error("Failed to get available ip from a CIDR: ", err) | ||||||
| 				return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{ | 				continue | ||||||
| 					false, |  | ||||||
| 					fmt.Sprintf("Cannot suggest ip allocation: failed to get available ip from network %s", cidr), |  | ||||||
| 				}) |  | ||||||
| 			} | 			} | ||||||
|  | 			found = true | ||||||
| 			if strings.Contains(ip, ":") { | 			if strings.Contains(ip, ":") { | ||||||
| 				suggestedIPs = append(suggestedIPs, fmt.Sprintf("%s/128", ip)) | 				ipSet[fmt.Sprintf("%s/128", ip)] = struct{}{} | ||||||
| 			} else { | 			} 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) | 		return c.JSON(http.StatusOK, suggestedIPs) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										16
									
								
								main.go
								
								
								
								
							
							
						
						
									
										16
									
								
								main.go
								
								
								
								
							|  | @ -45,6 +45,7 @@ var ( | ||||||
| 	flagSessionSecret  string = util.RandomString(32) | 	flagSessionSecret  string = util.RandomString(32) | ||||||
| 	flagWgConfTemplate string | 	flagWgConfTemplate string | ||||||
| 	flagBasePath       string | 	flagBasePath       string | ||||||
|  | 	flagSubnetRanges   string | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
|  | @ -81,6 +82,7 @@ func init() { | ||||||
| 	flag.StringVar(&flagEmailFromName, "email-from-name", util.LookupEnvOrString("EMAIL_FROM_NAME", flagEmailFromName), "'From' email name.") | 	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(&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(&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 ( | 	var ( | ||||||
| 		smtpPasswordLookup  = util.LookupEnvOrString("SMTP_PASSWORD", flagSmtpPassword) | 		smtpPasswordLookup  = util.LookupEnvOrString("SMTP_PASSWORD", flagSmtpPassword) | ||||||
|  | @ -127,6 +129,7 @@ func init() { | ||||||
| 	util.SessionSecret = []byte(flagSessionSecret) | 	util.SessionSecret = []byte(flagSessionSecret) | ||||||
| 	util.WgConfTemplate = flagWgConfTemplate | 	util.WgConfTemplate = flagWgConfTemplate | ||||||
| 	util.BasePath = util.ParseBasePath(flagBasePath) | 	util.BasePath = util.ParseBasePath(flagBasePath) | ||||||
|  | 	util.SubnetRanges = util.ParseSubnetRanges(flagSubnetRanges) | ||||||
| 
 | 
 | ||||||
| 	// print only if log level is INFO or lower
 | 	// print only if log level is INFO or lower
 | ||||||
| 	if lvl, _ := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO")); lvl <= log.INFO { | 	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("Session secret\t:", util.SessionSecret)
 | ||||||
| 		fmt.Println("Custom wg.conf\t:", util.WgConfTemplate) | 		fmt.Println("Custom wg.conf\t:", util.WgConfTemplate) | ||||||
| 		fmt.Println("Base path\t:", util.BasePath+"/") | 		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
 | 	// create the wireguard config on start, if it doesn't exist
 | ||||||
| 	initServerConfig(db, tmplDir) | 	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
 | 	// register routes
 | ||||||
| 	app := router.New(tmplDir, extraData, util.SessionSecret) | 	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/clients", handler.GetClients(db), handler.ValidSession) | ||||||
| 	app.GET(util.BasePath+"/api/client/:id", handler.GetClient(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/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.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.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) | 	app.GET(util.BasePath+"/wake_on_lan_hosts", handler.GetWakeOnLanHosts(db), handler.ValidSession) | ||||||
|  |  | ||||||
|  | @ -12,6 +12,7 @@ type Client struct { | ||||||
| 	PresharedKey    string    `json:"preshared_key"` | 	PresharedKey    string    `json:"preshared_key"` | ||||||
| 	Name            string    `json:"name"` | 	Name            string    `json:"name"` | ||||||
| 	Email           string    `json:"email"` | 	Email           string    `json:"email"` | ||||||
|  | 	SubnetRanges    []string  `json:"subnet_ranges,omitempty"` | ||||||
| 	AllocatedIPs    []string  `json:"allocated_ips"` | 	AllocatedIPs    []string  `json:"allocated_ips"` | ||||||
| 	AllowedIPs      []string  `json:"allowed_ips"` | 	AllowedIPs      []string  `json:"allowed_ips"` | ||||||
| 	ExtraAllowedIPs []string  `json:"extra_allowed_ips"` | 	ExtraAllowedIPs []string  `json:"extra_allowed_ips"` | ||||||
|  |  | ||||||
|  | @ -58,11 +58,13 @@ | ||||||
|                 </div> |                 </div> | ||||||
|                 <div class="form-group form-group-sm"> |                 <div class="form-group form-group-sm"> | ||||||
|                     <select name="status-selector" id="status-selector" class="custom-select form-control-navbar" style="margin-left: 0.5em; height: 90%; font-size: 14px;"> |                     <select name="status-selector" id="status-selector" class="custom-select form-control-navbar" style="margin-left: 0.5em; height: 90%; font-size: 14px;"> | ||||||
|  |                         <!-- THIS SECTION IS OVERRIDDEN BY JS. SEE updateSearchList() function in clients.html BEFORE EDITING --> | ||||||
|                         <option value="All">All</option> |                         <option value="All">All</option> | ||||||
|                         <option value="Enabled">Enabled</option> |                         <option value="Enabled">Enabled</option> | ||||||
|                         <option value="Disabled">Disabled</option> |                         <option value="Disabled">Disabled</option> | ||||||
|                         <option value="Connected">Connected</option> |                         <option value="Connected">Connected</option> | ||||||
|                         <option value="Disconnected">Disconnected</option> |                         <option value="Disconnected">Disconnected</option> | ||||||
|  |                         <!-- THIS SECTION IS OVERRIDDEN BY JS. SEE updateSearchList() function in clients.html BEFORE EDITING --> | ||||||
|                     </select> |                     </select> | ||||||
|                 </div> |                 </div> | ||||||
|             </form> |             </form> | ||||||
|  | @ -209,6 +211,12 @@ | ||||||
|                                 <label for="client_email" class="control-label">Email</label> |                                 <label for="client_email" class="control-label">Email</label> | ||||||
|                                 <input type="text" class="form-control" id="client_email" name="client_email"> |                                 <input type="text" class="form-control" id="client_email" name="client_email"> | ||||||
|                             </div> |                             </div> | ||||||
|  |                             <div class="form-group"> | ||||||
|  |                                 <label for="subnet_ranges" class="control-label">Subnet range</label> | ||||||
|  |                                 <select id="subnet_ranges" class="select2" | ||||||
|  |                                     data-placeholder="Select a subnet range" style="width: 100%;"> | ||||||
|  |                                 </select> | ||||||
|  |                             </div> | ||||||
|                             <div class="form-group"> |                             <div class="form-group"> | ||||||
|                                 <label for="client_allocated_ips" class="control-label">IP Allocation</label> |                                 <label for="client_allocated_ips" class="control-label">IP Allocation</label> | ||||||
|                                 <input type="text" data-role="tagsinput" class="form-control" id="client_allocated_ips"> |                                 <input type="text" data-role="tagsinput" class="form-control" id="client_allocated_ips"> | ||||||
|  | @ -368,6 +376,36 @@ | ||||||
| 
 | 
 | ||||||
|         $(document).ready(function () { |         $(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({ |                 $.ajax({ | ||||||
|                     cache: false, |                     cache: false, | ||||||
|                     method: 'GET', |                     method: 'GET', | ||||||
|  | @ -388,8 +426,7 @@ | ||||||
|                         toastr.error(responseJson['message']); |                         toastr.error(responseJson['message']); | ||||||
|                     } |                     } | ||||||
|                 }); |                 }); | ||||||
| 
 |         } | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|          |          | ||||||
|         // populateClient function for render new client info |         // populateClient function for render new client info | ||||||
|  | @ -456,6 +493,7 @@ | ||||||
|                     if (window.location.pathname === "{{.basePath}}/") { |                     if (window.location.pathname === "{{.basePath}}/") { | ||||||
|                         populateClient(resp.id); |                         populateClient(resp.id); | ||||||
|                     } |                     } | ||||||
|  |                     updateApplyConfigVisibility() | ||||||
|                 }, |                 }, | ||||||
|                 error: function(jqXHR, exception) { |                 error: function(jqXHR, exception) { | ||||||
|                     const responseJson = jQuery.parseJSON(jqXHR.responseText); |                     const responseJson = jQuery.parseJSON(jqXHR.responseText); | ||||||
|  | @ -466,19 +504,32 @@ | ||||||
| 
 | 
 | ||||||
|         // updateIPAllocationSuggestion function for automatically fill |         // updateIPAllocationSuggestion function for automatically fill | ||||||
|         // the IP Allocation input with suggested ip addresses |         // 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({ |             $.ajax({ | ||||||
|                 cache: false, |                 cache: false, | ||||||
|                 method: 'GET', |                 method: 'GET', | ||||||
|                 url: '{{.basePath}}/api/suggest-client-ips', |                 url: `{{.basePath}}/api/suggest-client-ips?sr=${subnetRange}`, | ||||||
|                 dataType: 'json', |                 dataType: 'json', | ||||||
|                 contentType: "application/json", |                 contentType: "application/json", | ||||||
|                 success: function(data) { |                 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) { |                     data.forEach(function (item, index) { | ||||||
|                         $('#client_allocated_ips').addTag(item); |                         $('#client_allocated_ips').addTag(item); | ||||||
|                     }) |                     }) | ||||||
|                 }, |                 }, | ||||||
|                 error: function(jqXHR, exception) { |                 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); |                     const responseJson = jQuery.parseJSON(jqXHR.responseText); | ||||||
|                     toastr.error(responseJson['message']); |                     toastr.error(responseJson['message']); | ||||||
|                 } |                 } | ||||||
|  | @ -497,7 +548,6 @@ | ||||||
|             'defaultText': 'Add More', |             'defaultText': 'Add More', | ||||||
|             'removeWithBackspace': true, |             'removeWithBackspace': true, | ||||||
|             'minChars': 0, |             'minChars': 0, | ||||||
|             'minInputWidth': '100%', |  | ||||||
|             'placeholderColor': '#666666' |             'placeholderColor': '#666666' | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  | @ -509,7 +559,6 @@ | ||||||
|             'defaultText': 'Add More', |             'defaultText': 'Add More', | ||||||
|             'removeWithBackspace': true, |             'removeWithBackspace': true, | ||||||
|             'minChars': 0, |             'minChars': 0, | ||||||
|             'minInputWidth': '100%', |  | ||||||
|             'placeholderColor': '#666666' |             'placeholderColor': '#666666' | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  | @ -520,7 +569,6 @@ | ||||||
|             'defaultText': 'Add More', |             'defaultText': 'Add More', | ||||||
|             'removeWithBackspace': true, |             'removeWithBackspace': true, | ||||||
|             'minChars': 0, |             'minChars': 0, | ||||||
|             'minInputWidth': '100%', |  | ||||||
|             'placeholderColor': '#666666' |             'placeholderColor': '#666666' | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  | @ -565,10 +613,17 @@ | ||||||
|                 $("#client_preshared_key").val(""); |                 $("#client_preshared_key").val(""); | ||||||
|                 $("#client_allocated_ips").importTags(''); |                 $("#client_allocated_ips").importTags(''); | ||||||
|                 $("#client_extra_allowed_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 |         // apply_config_confirm button event | ||||||
|         $(document).ready(function () { |         $(document).ready(function () { | ||||||
|             $("#apply_config_confirm").click(function () { |             $("#apply_config_confirm").click(function () { | ||||||
|  | @ -579,6 +634,7 @@ | ||||||
|                     dataType: 'json', |                     dataType: 'json', | ||||||
|                     contentType: "application/json", |                     contentType: "application/json", | ||||||
|                     success: function(data) { |                     success: function(data) { | ||||||
|  |                         updateApplyConfigVisibility() | ||||||
|                         $("#modal_apply_config").modal('hide'); |                         $("#modal_apply_config").modal('hide'); | ||||||
|                         toastr.success('Applied config successfully'); |                         toastr.success('Applied config successfully'); | ||||||
|                     }, |                     }, | ||||||
|  |  | ||||||
|  | @ -100,6 +100,12 @@ Wireguard Clients | ||||||
|                         <label for="_client_email" class="control-label">Email</label> |                         <label for="_client_email" class="control-label">Email</label> | ||||||
|                         <input type="text" class="form-control" id="_client_email" name="client_email"> |                         <input type="text" class="form-control" id="_client_email" name="client_email"> | ||||||
|                     </div> |                     </div> | ||||||
|  |                     <div class="form-group"> | ||||||
|  |                         <label for="_subnet_ranges" class="control-label">Subnet range</label> | ||||||
|  |                         <select id="_subnet_ranges" class="select2" | ||||||
|  |                             data-placeholder="Select a subnet range" style="width: 100%;"> | ||||||
|  |                         </select> | ||||||
|  |                     </div> | ||||||
|                     <div class="form-group"> |                     <div class="form-group"> | ||||||
|                         <label for="_client_allocated_ips" class="control-label">IP Allocation</label> |                         <label for="_client_allocated_ips" class="control-label">IP Allocation</label> | ||||||
|                         <input type="text" data-role="tagsinput" class="form-control" id="_client_allocated_ips"> |                         <input type="text" data-role="tagsinput" class="form-control" id="_client_allocated_ips"> | ||||||
|  | @ -253,13 +259,102 @@ Wireguard Clients | ||||||
|             setClientStatus(clientID, true); |             setClientStatus(clientID, true); | ||||||
|             const divElement = document.getElementById("paused_" + clientID); |             const divElement = document.getElementById("paused_" + clientID); | ||||||
|             divElement.style.visibility = "hidden"; |             divElement.style.visibility = "hidden"; | ||||||
|  |             updateApplyConfigVisibility() | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         function pauseClient(clientID) { |         function pauseClient(clientID) { | ||||||
|             setClientStatus(clientID, false); |             setClientStatus(clientID, false); | ||||||
|             const divElement = document.getElementById("paused_" + clientID); |             const divElement = document.getElementById("paused_" + clientID); | ||||||
|             divElement.style.visibility = "visible"; |             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( | ||||||
|  |                     $("<option></option>") | ||||||
|  |                         .text("Any") | ||||||
|  |                         .val("__default_any__") | ||||||
|  |                 ); | ||||||
|  |                 $.each(data, function(index, item) { | ||||||
|  |                     $(elementID).append( | ||||||
|  |                         $("<option></option>") | ||||||
|  |                             .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( | ||||||
|  |                     $("<option></option>") | ||||||
|  |                         .text("All") | ||||||
|  |                         .val("All"), | ||||||
|  |                     $("<option></option>") | ||||||
|  |                         .text("Enabled") | ||||||
|  |                         .val("Enabled"), | ||||||
|  |                     $("<option></option>") | ||||||
|  |                         .text("Disabled") | ||||||
|  |                         .val("Disabled"), | ||||||
|  |                     $("<option></option>") | ||||||
|  |                         .text("Connected") | ||||||
|  |                         .val("Connected"), | ||||||
|  |                     $("<option></option>") | ||||||
|  |                         .text("Disconnected") | ||||||
|  |                         .val("Disconnected") | ||||||
|  |                 ); | ||||||
|  |                 $.each(data, function(index, item) { | ||||||
|  |                     $("#status-selector").append( | ||||||
|  |                         $("<option></option>") | ||||||
|  |                             .text(item) | ||||||
|  |                             .val(item) | ||||||
|  |                     ); | ||||||
|  |                 }); | ||||||
|  |             }); | ||||||
|  | } | ||||||
|     </script> |     </script> | ||||||
|     <script> |     <script> | ||||||
|         // load client list |         // load client list | ||||||
|  | @ -349,7 +444,18 @@ Wireguard Clients | ||||||
|                     }); |                     }); | ||||||
|                     break; |                     break; | ||||||
|                 default: |                 default: | ||||||
|                     $('.col-lg-4').show(); |                     $('.col-lg-4').hide(); | ||||||
|  |                     const selectedSR = $("#status-selector").val() | ||||||
|  |                     $(".fa-subnetrange").each(function () { | ||||||
|  |                         const srs = $(this).parent().text().trim().split(',') | ||||||
|  |                         for (const sr of srs) { | ||||||
|  |                             if (sr === selectedSR) { | ||||||
|  |                                 $(this).closest('.col-lg-4').show(); | ||||||
|  |                                 break | ||||||
|  |                             }                             | ||||||
|  |                         } | ||||||
|  |                     }) | ||||||
|  |                     // $('.col-lg-4').show(); | ||||||
|                     break; |                     break; | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|  | @ -400,6 +506,7 @@ Wireguard Clients | ||||||
|                         toastr.success('Removed client successfully'); |                         toastr.success('Removed client successfully'); | ||||||
|                         const divElement = document.getElementById('client_' + client_id); |                         const divElement = document.getElementById('client_' + client_id); | ||||||
|                         divElement.style.display = "none"; |                         divElement.style.display = "none"; | ||||||
|  |                         updateApplyConfigVisibility() | ||||||
|                     }, |                     }, | ||||||
|                     error: function(jqXHR, exception) { |                     error: function(jqXHR, exception) { | ||||||
|                         const responseJson = jQuery.parseJSON(jqXHR.responseText); |                         const responseJson = jQuery.parseJSON(jqXHR.responseText); | ||||||
|  | @ -427,7 +534,6 @@ Wireguard Clients | ||||||
|                     'defaultText': 'Add More', |                     'defaultText': 'Add More', | ||||||
|                     'removeWithBackspace': true, |                     'removeWithBackspace': true, | ||||||
|                     'minChars': 0, |                     'minChars': 0, | ||||||
|                     'minInputWidth': '100%', |  | ||||||
|                     'placeholderColor': '#666666' |                     'placeholderColor': '#666666' | ||||||
|                 }); |                 }); | ||||||
| 
 | 
 | ||||||
|  | @ -439,7 +545,6 @@ Wireguard Clients | ||||||
|                     'defaultText': 'Add More', |                     'defaultText': 'Add More', | ||||||
|                     'removeWithBackspace': true, |                     'removeWithBackspace': true, | ||||||
|                     'minChars': 0, |                     'minChars': 0, | ||||||
|                     'minInputWidth': '100%', |  | ||||||
|                     'placeholderColor': '#666666' |                     'placeholderColor': '#666666' | ||||||
|                 }); |                 }); | ||||||
| 
 | 
 | ||||||
|  | @ -450,7 +555,6 @@ Wireguard Clients | ||||||
|                     'defaultText': 'Add More', |                     'defaultText': 'Add More', | ||||||
|                     'removeWithBackspace' : true, |                     'removeWithBackspace' : true, | ||||||
|                     'minChars': 0, |                     'minChars': 0, | ||||||
|                     'minInputWidth': '100%', |  | ||||||
|                     'placeholderColor': '#666666' |                     'placeholderColor': '#666666' | ||||||
|                 }) |                 }) | ||||||
| 
 | 
 | ||||||
|  | @ -469,6 +573,13 @@ Wireguard Clients | ||||||
|                         modal.find("#_client_name").val(client.name); |                         modal.find("#_client_name").val(client.name); | ||||||
|                         modal.find("#_client_email").val(client.email); |                         modal.find("#_client_email").val(client.email); | ||||||
| 
 | 
 | ||||||
|  |                         let preselectedEl | ||||||
|  |                         if (client.subnet_ranges && client.subnet_ranges.length > 0) { | ||||||
|  |                             preselectedEl = client.subnet_ranges[0] | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         updateSubnetRangesList("#_subnet_ranges", preselectedEl); | ||||||
|  | 
 | ||||||
|                         modal.find("#_client_allocated_ips").importTags(''); |                         modal.find("#_client_allocated_ips").importTags(''); | ||||||
|                         client.allocated_ips.forEach(function (obj) { |                         client.allocated_ips.forEach(function (obj) { | ||||||
|                             modal.find("#_client_allocated_ips").addTag(obj); |                             modal.find("#_client_allocated_ips").addTag(obj); | ||||||
|  | @ -491,6 +602,11 @@ Wireguard Clients | ||||||
| 
 | 
 | ||||||
|                         modal.find("#_client_public_key").val(client.public_key); |                         modal.find("#_client_public_key").val(client.public_key); | ||||||
|                         modal.find("#_client_preshared_key").val(client.preshared_key); |                         modal.find("#_client_preshared_key").val(client.preshared_key); | ||||||
|  | 
 | ||||||
|  |                         // handle subnet range select | ||||||
|  |                         $('#_subnet_ranges').on('select2:select', function (e) { | ||||||
|  |                             updateIPAllocationSuggestionExisting(); | ||||||
|  |                         }); | ||||||
|                     }, |                     }, | ||||||
|                     error: function (jqXHR, exception) { |                     error: function (jqXHR, exception) { | ||||||
|                         const responseJson = jQuery.parseJSON(jqXHR.responseText); |                         const responseJson = jQuery.parseJSON(jqXHR.responseText); | ||||||
|  |  | ||||||
|  | @ -203,7 +203,6 @@ Global Settings | ||||||
|             'defaultText': 'Add More', |             'defaultText': 'Add More', | ||||||
|             'removeWithBackspace': true, |             'removeWithBackspace': true, | ||||||
|             'minChars': 0, |             'minChars': 0, | ||||||
|             'minInputWidth': '100%', |  | ||||||
|             'placeholderColor': '#666666' |             'placeholderColor': '#666666' | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -167,7 +167,6 @@ Wireguard Server Settings | ||||||
|             'defaultText': 'Add More', |             'defaultText': 'Add More', | ||||||
|             'removeWithBackspace': true, |             'removeWithBackspace': true, | ||||||
|             'minChars': 0, |             'minChars': 0, | ||||||
|             'minInputWidth': '100%', |  | ||||||
|             'placeholderColor': '#666666' |             'placeholderColor': '#666666' | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,3 @@ | ||||||
|  | package util | ||||||
|  | 
 | ||||||
|  | var IPToSubnetRange = map[string]uint16{} | ||||||
|  | @ -1,24 +1,31 @@ | ||||||
| package util | package util | ||||||
| 
 | 
 | ||||||
| import "strings" | import ( | ||||||
|  | 	"net" | ||||||
|  | 	"strings" | ||||||
|  | 
 | ||||||
|  | 	"github.com/labstack/gommon/log" | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| // Runtime config
 | // Runtime config
 | ||||||
| var ( | var ( | ||||||
| 	DisableLogin   bool | 	DisableLogin      bool | ||||||
| 	BindAddress    string | 	BindAddress       string | ||||||
| 	SmtpHostname   string | 	SmtpHostname      string | ||||||
| 	SmtpPort       int | 	SmtpPort          int | ||||||
| 	SmtpUsername   string | 	SmtpUsername      string | ||||||
| 	SmtpPassword   string | 	SmtpPassword      string | ||||||
| 	SmtpNoTLSCheck bool | 	SmtpNoTLSCheck    bool | ||||||
| 	SmtpEncryption string | 	SmtpEncryption    string | ||||||
| 	SmtpAuthType   string | 	SmtpAuthType      string | ||||||
| 	SendgridApiKey string | 	SendgridApiKey    string | ||||||
| 	EmailFrom      string | 	EmailFrom         string | ||||||
| 	EmailFromName  string | 	EmailFromName     string | ||||||
| 	SessionSecret  []byte | 	SessionSecret     []byte | ||||||
| 	WgConfTemplate string | 	WgConfTemplate    string | ||||||
| 	BasePath       string | 	BasePath          string | ||||||
|  | 	SubnetRanges      map[string]([]*net.IPNet) | ||||||
|  | 	SubnetRangesOrder []string | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
|  | @ -30,7 +37,7 @@ const ( | ||||||
| 	DefaultDNS                             = "1.1.1.1" | 	DefaultDNS                             = "1.1.1.1" | ||||||
| 	DefaultMTU                             = 1450 | 	DefaultMTU                             = 1450 | ||||||
| 	DefaultPersistentKeepalive             = 15 | 	DefaultPersistentKeepalive             = 15 | ||||||
| 	DefaultFirewallMark                    = "0xca6c" // i.e. 51820
 | 	DefaultFirewallMark                    = "0xca6c"  // i.e. 51820
 | ||||||
| 	DefaultTable                           = "auto" | 	DefaultTable                           = "auto" | ||||||
| 	DefaultConfigFilePath                  = "/etc/wireguard/wg0.conf" | 	DefaultConfigFilePath                  = "/etc/wireguard/wg0.conf" | ||||||
| 	UsernameEnvVar                         = "WGUI_USERNAME" | 	UsernameEnvVar                         = "WGUI_USERNAME" | ||||||
|  | @ -66,3 +73,45 @@ func ParseBasePath(basePath string) string { | ||||||
| 	} | 	} | ||||||
| 	return basePath | 	return basePath | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func ParseSubnetRanges(subnetRangesStr string) map[string]([]*net.IPNet) { | ||||||
|  | 	subnetRanges := map[string]([]*net.IPNet){} | ||||||
|  | 	if subnetRangesStr == "" { | ||||||
|  | 		return subnetRanges | ||||||
|  | 	} | ||||||
|  | 	cidrSet := map[string]bool{} | ||||||
|  | 	subnetRangesStr = strings.TrimSpace(subnetRangesStr) | ||||||
|  | 	subnetRangesStr = strings.Trim(subnetRangesStr, ";:,") | ||||||
|  | 	ranges := strings.Split(subnetRangesStr, ";") | ||||||
|  | 	for _, rng := range ranges { | ||||||
|  | 		rng = strings.TrimSpace(rng) | ||||||
|  | 		rngSpl := strings.Split(rng, ":") | ||||||
|  | 		if len(rngSpl) != 2 { | ||||||
|  | 			log.Warnf("Unable to parse subnet range: %v. Skipped.", rng) | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		rngName := strings.TrimSpace(rngSpl[0]) | ||||||
|  | 		subnetRanges[rngName] = make([]*net.IPNet, 0) | ||||||
|  | 		cidrs := strings.Split(rngSpl[1], ",") | ||||||
|  | 		for _, cidr := range cidrs { | ||||||
|  | 			cidr = strings.TrimSpace(cidr) | ||||||
|  | 			_, net, err := net.ParseCIDR(cidr) | ||||||
|  | 			if err != nil { | ||||||
|  | 				log.Warnf("[%v] Unable to parse CIDR: %v. Skipped.", rngName, cidr) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			if cidrSet[net.String()] { | ||||||
|  | 				log.Warnf("[%v] CIDR already exists: %v. Skipped.", rngName, net.String()) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
|  | 			cidrSet[net.String()] = true | ||||||
|  | 			subnetRanges[rngName] = append(subnetRanges[rngName], net) | ||||||
|  | 		} | ||||||
|  | 		if len(subnetRanges[rngName]) == 0 { | ||||||
|  | 			delete(subnetRanges, rngName) | ||||||
|  | 		} else { | ||||||
|  | 			SubnetRangesOrder = append(SubnetRangesOrder, rngName) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return subnetRanges | ||||||
|  | } | ||||||
|  |  | ||||||
							
								
								
									
										154
									
								
								util/util.go
								
								
								
								
							
							
						
						
									
										154
									
								
								util/util.go
								
								
								
								
							|  | @ -95,6 +95,15 @@ func ClientDefaultsFromEnv() model.ClientDefaults { | ||||||
| 	return clientDefaults | 	return clientDefaults | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // ContainsCIDR to check if ipnet1 contains ipnet2
 | ||||||
|  | // https://stackoverflow.com/a/40406619/6111641
 | ||||||
|  | // https://go.dev/play/p/Q4J-JEN3sF
 | ||||||
|  | func ContainsCIDR(ipnet1, ipnet2 *net.IPNet) bool { | ||||||
|  | 	ones1, _ := ipnet1.Mask.Size() | ||||||
|  | 	ones2, _ := ipnet2.Mask.Size() | ||||||
|  | 	return ones1 <= ones2 && ipnet1.Contains(ipnet2.IP) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // ValidateCIDR to validate a network CIDR
 | // ValidateCIDR to validate a network CIDR
 | ||||||
| func ValidateCIDR(cidr string) bool { | func ValidateCIDR(cidr string) bool { | ||||||
| 	_, _, err := net.ParseCIDR(cidr) | 	_, _, err := net.ParseCIDR(cidr) | ||||||
|  | @ -317,15 +326,32 @@ func GetBroadcastIP(n *net.IPNet) net.IP { | ||||||
| 	return broadcast | 	return broadcast | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // GetBroadcastAndNetworkAddrsLookup get the ip address that can't be used with current server interfaces
 | ||||||
|  | func GetBroadcastAndNetworkAddrsLookup(interfaceAddresses []string) map[string]bool { | ||||||
|  | 	list := make(map[string]bool, 0) | ||||||
|  | 	for _, ifa := range interfaceAddresses { | ||||||
|  | 		_, net, err := net.ParseCIDR(ifa) | ||||||
|  | 		if err != nil { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		broadcastAddr := GetBroadcastIP(net).String() | ||||||
|  | 		networkAddr := net.IP.String() | ||||||
|  | 		list[broadcastAddr] = true | ||||||
|  | 		list[networkAddr] = true | ||||||
|  | 	} | ||||||
|  | 	return list | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // GetAvailableIP get the ip address that can be allocated from an CIDR
 | // GetAvailableIP get the ip address that can be allocated from an CIDR
 | ||||||
| func GetAvailableIP(cidr string, allocatedList []string) (string, error) { | // We need interfaceAddresses to find real broadcast and network addresses
 | ||||||
|  | func GetAvailableIP(cidr string, allocatedList, interfaceAddresses []string) (string, error) { | ||||||
| 	ip, net, err := net.ParseCIDR(cidr) | 	ip, net, err := net.ParseCIDR(cidr) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", err | 		return "", err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	broadcastAddr := GetBroadcastIP(net).String() | 	unavailableIPs := GetBroadcastAndNetworkAddrsLookup(interfaceAddresses) | ||||||
| 	networkAddr := net.IP.String() |  | ||||||
| 
 | 
 | ||||||
| 	for ip := ip.Mask(net.Mask); net.Contains(ip); inc(ip) { | 	for ip := ip.Mask(net.Mask); net.Contains(ip); inc(ip) { | ||||||
| 		available := true | 		available := true | ||||||
|  | @ -336,7 +362,7 @@ func GetAvailableIP(cidr string, allocatedList []string) (string, error) { | ||||||
| 				break | 				break | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		if available && suggestedAddr != networkAddr && suggestedAddr != broadcastAddr { | 		if available && !unavailableIPs[suggestedAddr] { | ||||||
| 			return suggestedAddr, nil | 			return suggestedAddr, nil | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | @ -384,6 +410,126 @@ func ValidateIPAllocation(serverAddresses []string, ipAllocatedList []string, ip | ||||||
| 	return true, nil | 	return true, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // findSubnetRangeForIP to find first SR for IP, and cache the match
 | ||||||
|  | func findSubnetRangeForIP(cidr string) (uint16, error) { | ||||||
|  | 	ip, _, err := net.ParseCIDR(cidr) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if srName, ok := IPToSubnetRange[ip.String()]; ok { | ||||||
|  | 		return srName, nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for srIndex, sr := range SubnetRangesOrder { | ||||||
|  | 		for _, srCIDR := range SubnetRanges[sr] { | ||||||
|  | 			if srCIDR.Contains(ip) { | ||||||
|  | 				IPToSubnetRange[ip.String()] = uint16(srIndex) | ||||||
|  | 				return uint16(srIndex), nil | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return 0, fmt.Errorf("Subnet range not found for this IP") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // FillClientSubnetRange to fill subnet ranges client belongs to, does nothing if SRs are not found
 | ||||||
|  | func FillClientSubnetRange(client model.ClientData) model.ClientData { | ||||||
|  | 	cl := *client.Client | ||||||
|  | 	for _, ip := range cl.AllocatedIPs { | ||||||
|  | 		sr, err := findSubnetRangeForIP(ip) | ||||||
|  | 		if err != nil { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		cl.SubnetRanges = append(cl.SubnetRanges, SubnetRangesOrder[sr]) | ||||||
|  | 	} | ||||||
|  | 	return model.ClientData{ | ||||||
|  | 		Client: &cl, | ||||||
|  | 		QRCode: client.QRCode, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // ValidateAndFixSubnetRanges to check if subnet ranges are valid for the server configuration
 | ||||||
|  | // Removes all non-valid CIDRs
 | ||||||
|  | func ValidateAndFixSubnetRanges(db store.IStore) error { | ||||||
|  | 	if len(SubnetRangesOrder) == 0 { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	server, err := db.GetServer() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	var serverSubnets []*net.IPNet | ||||||
|  | 	for _, addr := range server.Interface.Addresses { | ||||||
|  | 		addr = strings.TrimSpace(addr) | ||||||
|  | 		_, net, err := net.ParseCIDR(addr) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		serverSubnets = append(serverSubnets, net) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, rng := range SubnetRangesOrder { | ||||||
|  | 		cidrs := SubnetRanges[rng] | ||||||
|  | 		if len(cidrs) > 0 { | ||||||
|  | 			newCIDRs := make([]*net.IPNet, 0) | ||||||
|  | 			for _, cidr := range cidrs { | ||||||
|  | 				valid := false | ||||||
|  | 
 | ||||||
|  | 				for _, serverSubnet := range serverSubnets { | ||||||
|  | 					if ContainsCIDR(serverSubnet, cidr) { | ||||||
|  | 						valid = true | ||||||
|  | 						break | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				if valid { | ||||||
|  | 					newCIDRs = append(newCIDRs, cidr) | ||||||
|  | 				} else { | ||||||
|  | 					log.Warnf("[%v] CIDR is outside of all server subnets: %v. Removed.", rng, cidr) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			if len(newCIDRs) > 0 { | ||||||
|  | 				SubnetRanges[rng] = newCIDRs | ||||||
|  | 			} else { | ||||||
|  | 				delete(SubnetRanges, rng) | ||||||
|  | 				log.Warnf("[%v] No valid CIDRs in this subnet range. Removed.", rng) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetSubnetRangesString to get a formatted string, representing active subnet ranges
 | ||||||
|  | func GetSubnetRangesString() string { | ||||||
|  | 	if len(SubnetRangesOrder) == 0 { | ||||||
|  | 		return "" | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	strB := strings.Builder{} | ||||||
|  | 
 | ||||||
|  | 	for _, rng := range SubnetRangesOrder { | ||||||
|  | 		cidrs := SubnetRanges[rng] | ||||||
|  | 		if len(cidrs) > 0 { | ||||||
|  | 			strB.WriteString(rng) | ||||||
|  | 			strB.WriteString(":[") | ||||||
|  | 			first := true | ||||||
|  | 			for _, cidr := range cidrs { | ||||||
|  | 				if !first { | ||||||
|  | 					strB.WriteString(", ") | ||||||
|  | 				} | ||||||
|  | 				strB.WriteString(cidr.String()) | ||||||
|  | 				first = false | ||||||
|  | 			} | ||||||
|  | 			strB.WriteString("]  ") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return strings.TrimSpace(strB.String()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // WriteWireGuardServerConfig to write Wireguard server config. e.g. wg0.conf
 | // WriteWireGuardServerConfig to write Wireguard server config. e.g. wg0.conf
 | ||||||
| func WriteWireGuardServerConfig(tmplDir fs.FS, serverConfig model.Server, clientDataList []model.ClientData, usersList []model.User, globalSettings model.GlobalSetting) error { | func WriteWireGuardServerConfig(tmplDir fs.FS, serverConfig model.Server, clientDataList []model.ClientData, usersList []model.User, globalSettings model.GlobalSetting) error { | ||||||
| 	var tmplWireguardConf string | 	var tmplWireguardConf string | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue