Subnet range selector, interface fixes (#481)

This commit is contained in:
0xCA 2023-12-27 13:08:55 +05:00 committed by GitHub
parent e73047b14f
commit a9be53899c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 473 additions and 44 deletions

View file

@ -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 |

View file

@ -18,6 +18,11 @@ function renderClientList(data) {
allowedIpsHtml += `<small class="badge badge-secondary">${obj}</small>&nbsp;`;
})
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 = `<div class="col-sm-6 col-md-6 col-lg-4" id="client_${obj.Client.id}">
<div class="info-box">
@ -59,6 +64,7 @@ function renderClientList(data) {
<hr>
<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-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-clock"></i>
${prettyDateTime(obj.Client.created_at)}</span>

View file

@ -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)
}
}

16
main.go
View file

@ -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)

View file

@ -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"`

View file

@ -58,11 +58,13 @@
</div>
<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;">
<!-- THIS SECTION IS OVERRIDDEN BY JS. SEE updateSearchList() function in clients.html BEFORE EDITING -->
<option value="All">All</option>
<option value="Enabled">Enabled</option>
<option value="Disabled">Disabled</option>
<option value="Connected">Connected</option>
<option value="Disconnected">Disconnected</option>
<!-- THIS SECTION IS OVERRIDDEN BY JS. SEE updateSearchList() function in clients.html BEFORE EDITING -->
</select>
</div>
</form>
@ -209,6 +211,12 @@
<label for="client_email" class="control-label">Email</label>
<input type="text" class="form-control" id="client_email" name="client_email">
</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">
<label for="client_allocated_ips" class="control-label">IP Allocation</label>
<input type="text" data-role="tagsinput" class="form-control" id="client_allocated_ips">
@ -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');
},

View file

@ -100,6 +100,12 @@ Wireguard Clients
<label for="_client_email" class="control-label">Email</label>
<input type="text" class="form-control" id="_client_email" name="client_email">
</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">
<label for="_client_allocated_ips" class="control-label">IP Allocation</label>
<input type="text" data-role="tagsinput" class="form-control" id="_client_allocated_ips">
@ -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(
$("<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>
// load client list
@ -349,7 +444,18 @@ Wireguard Clients
});
break;
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;
}
});
@ -400,6 +506,7 @@ Wireguard Clients
toastr.success('Removed client successfully');
const divElement = document.getElementById('client_' + client_id);
divElement.style.display = "none";
updateApplyConfigVisibility()
},
error: function(jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);
@ -427,7 +534,6 @@ Wireguard Clients
'defaultText': 'Add More',
'removeWithBackspace': true,
'minChars': 0,
'minInputWidth': '100%',
'placeholderColor': '#666666'
});
@ -439,7 +545,6 @@ Wireguard Clients
'defaultText': 'Add More',
'removeWithBackspace': true,
'minChars': 0,
'minInputWidth': '100%',
'placeholderColor': '#666666'
});
@ -450,7 +555,6 @@ Wireguard Clients
'defaultText': 'Add More',
'removeWithBackspace' : true,
'minChars': 0,
'minInputWidth': '100%',
'placeholderColor': '#666666'
})
@ -469,6 +573,13 @@ Wireguard Clients
modal.find("#_client_name").val(client.name);
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('');
client.allocated_ips.forEach(function (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_preshared_key").val(client.preshared_key);
// handle subnet range select
$('#_subnet_ranges').on('select2:select', function (e) {
updateIPAllocationSuggestionExisting();
});
},
error: function (jqXHR, exception) {
const responseJson = jQuery.parseJSON(jqXHR.responseText);

View file

@ -203,7 +203,6 @@ Global Settings
'defaultText': 'Add More',
'removeWithBackspace': true,
'minChars': 0,
'minInputWidth': '100%',
'placeholderColor': '#666666'
});

View file

@ -167,7 +167,6 @@ Wireguard Server Settings
'defaultText': 'Add More',
'removeWithBackspace': true,
'minChars': 0,
'minInputWidth': '100%',
'placeholderColor': '#666666'
});

3
util/cache.go Normal file
View file

@ -0,0 +1,3 @@
package util
var IPToSubnetRange = map[string]uint16{}

View file

@ -1,24 +1,31 @@
package util
import "strings"
import (
"net"
"strings"
"github.com/labstack/gommon/log"
)
// Runtime config
var (
DisableLogin bool
BindAddress string
SmtpHostname string
SmtpPort int
SmtpUsername string
SmtpPassword string
SmtpNoTLSCheck bool
SmtpEncryption string
SmtpAuthType string
SendgridApiKey string
EmailFrom string
EmailFromName string
SessionSecret []byte
WgConfTemplate string
BasePath string
DisableLogin bool
BindAddress string
SmtpHostname string
SmtpPort int
SmtpUsername string
SmtpPassword string
SmtpNoTLSCheck bool
SmtpEncryption string
SmtpAuthType string
SendgridApiKey string
EmailFrom string
EmailFromName string
SessionSecret []byte
WgConfTemplate string
BasePath string
SubnetRanges map[string]([]*net.IPNet)
SubnetRangesOrder []string
)
const (
@ -30,7 +37,7 @@ const (
DefaultDNS = "1.1.1.1"
DefaultMTU = 1450
DefaultPersistentKeepalive = 15
DefaultFirewallMark = "0xca6c" // i.e. 51820
DefaultFirewallMark = "0xca6c" // i.e. 51820
DefaultTable = "auto"
DefaultConfigFilePath = "/etc/wireguard/wg0.conf"
UsernameEnvVar = "WGUI_USERNAME"
@ -66,3 +73,45 @@ func ParseBasePath(basePath string) string {
}
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
}

View file

@ -95,6 +95,15 @@ func ClientDefaultsFromEnv() model.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
func ValidateCIDR(cidr string) bool {
_, _, err := net.ParseCIDR(cidr)
@ -317,15 +326,32 @@ func GetBroadcastIP(n *net.IPNet) net.IP {
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
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)
if err != nil {
return "", err
}
broadcastAddr := GetBroadcastIP(net).String()
networkAddr := net.IP.String()
unavailableIPs := GetBroadcastAndNetworkAddrsLookup(interfaceAddresses)
for ip := ip.Mask(net.Mask); net.Contains(ip); inc(ip) {
available := true
@ -336,7 +362,7 @@ func GetAvailableIP(cidr string, allocatedList []string) (string, error) {
break
}
}
if available && suggestedAddr != networkAddr && suggestedAddr != broadcastAddr {
if available && !unavailableIPs[suggestedAddr] {
return suggestedAddr, nil
}
}
@ -384,6 +410,126 @@ func ValidateIPAllocation(serverAddresses []string, ipAllocatedList []string, ip
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
func WriteWireGuardServerConfig(tmplDir fs.FS, serverConfig model.Server, clientDataList []model.ClientData, usersList []model.User, globalSettings model.GlobalSetting) error {
var tmplWireguardConf string