Merge branch 'ngoduykhanh:master' into fix

This commit is contained in:
kevin 2023-12-27 18:36:42 +08:00 committed by GitHub
commit 1e589126ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 541 additions and 45 deletions

67
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,67 @@
# Contributing Guidelines
Thank you for your interest in contributing to my project. Whether it's a bug report, new feature, correction, or additional
documentation, I greatly value feedback and contributions from my community.
Please read through this document before submitting any issues or pull requests to ensure I have all the necessary
information to effectively respond to your bug report or contribution.
## Reporting Bugs/Feature Requests
I welcome you to use the GitHub issue tracker to report bugs or suggest features.
When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already
reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:
- A reproducible test case or series of steps
- The version of my code being used
- Any modifications you've made relevant to the bug
- Anything unusual about your environment or deployment
## Contributing via Pull Requests
### Discussion of New Features
Before initiating the implementation of a new feature, I encourage contributors to open a discussion by creating a new GitHub issue. This allows me to provide feedback, share insights, and ensure alignment with the project's direction and save your time.
#### Process for Discussing New Features:
1. **Create an Issue:**
- Go to the "Issues" tab in the repository.
- Click on "New Issue."
- Clearly describe the proposed feature, its purpose, and potential benefits.
2. **Engage in Discussion:**
- Respond promptly to comments and feedback from the community.
- Be open to adjusting the feature based on collaborative input.
3. **Consensus Building:**
- Strive to reach a consensus on the proposed feature.
- Ensure alignment with the overall project vision.
### Bug Fixes and Improvements
For bug fixes, documentation improvements, and general enhancements, feel free to submit a pull request directly.
#### Pull Request Guidelines:
1. **Fork the Repository:**
- Fork the repository to your GitHub account.
2. **Create a Branch:**
- Create a new branch for your changes.
3. **Make Changes:**
- Make your changes and ensure they adhere to coding standards.
4. **Submit a Pull Request:**
- Submit a pull request to the main repository.
5. **Engage in Review:**
- Be responsive to feedback and address any requested changes.
6. **Merge Process:**
- Once approved, your changes will be merged into the main branch.
## Licensing
See the [LICENSE](LICENSE) file for my project's licensing.

View file

@ -42,12 +42,13 @@ 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 |
| `WGUI_PASSWORD_HASH` | The password hash for the user on the login page. (alternative to `WGUI_PASSWORD`). Used for db initialization only | N/A | | `WGUI_PASSWORD_HASH` | The password hash for the user on the login page. (alternative to `WGUI_PASSWORD`). Used for db initialization only | N/A |
| `WGUI_PASSWORD_HASH_FILE` | Optional filepath for the user login password hash. (alternative to `WGUI_PASSWORD_FILE`). Used for db initialization only. Leave `WGUI_PASSWORD_HASH` blank to take effect | N/A | | `WGUI_PASSWORD_HASH_FILE` | Optional filepath for the user login password hash. (alternative to `WGUI_PASSWORD_FILE`). Used for db initialization only. Leave `WGUI_PASSWORD_HASH` blank to take effect | N/A |
| `WGUI_ENDPOINT_ADDRESS` | The default endpoint address used in global settings where clients should connect to | Resolved to your public ip address | | `WGUI_ENDPOINT_ADDRESS` | The default endpoint address used in global settings where clients should connect to. The endpoint can contain a port as well, useful when you are listening internally on the `WGUI_SERVER_LISTEN_PORT` port, but you forward on another port (ex 9000). Ex: myvpn.dyndns.com:9000 | Resolved to your public ip address |
| `WGUI_FAVICON_FILE_PATH` | The file path used as website favicon | Embedded WireGuard logo | | `WGUI_FAVICON_FILE_PATH` | The file path used as website favicon | Embedded WireGuard logo |
| `WGUI_DNS` | The default DNS servers (comma-separated-list) used in the global settings | `1.1.1.1` | | `WGUI_DNS` | The default DNS servers (comma-separated-list) used in the global settings | `1.1.1.1` |
| `WGUI_MTU` | The default MTU used in global settings | `1450` | | `WGUI_MTU` | The default MTU used in global settings | `1450` |

View file

@ -18,6 +18,11 @@ function renderClientList(data) {
allowedIpsHtml += `<small class="badge badge-secondary">${obj}</small>&nbsp;`; 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 // 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>

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) 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
View file

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

View file

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

View file

@ -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');
}, },

View file

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

View file

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

View file

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

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

View file

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