diff --git a/README.md b/README.md
index d585868..c43adc3 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,7 @@ docker-compose up
| `BIND_ADDRESS` | The addresses that can access to the web interface and the port, use unix:///abspath/to/file.socket for unix domain socket. | 0.0.0.0:80 |
| `SESSION_SECRET` | The secret key used to encrypt the session cookies. Set this to a random value | N/A |
| `SESSION_SECRET_FILE` | Optional filepath for the secret key used to encrypt the session cookies. Leave `SESSION_SECRET` blank to take effect | N/A |
+| `SUBNET_RANGES` | The list of address subdivision ranges. Format: `SR Name:10.0.1.0/24; SR2:10.0.2.0/24,10.0.3.0/24` Each CIDR must be inside one of the server interfaces. | N/A |
| `WGUI_USERNAME` | The username for the login page. Used for db initialization only | `admin` |
| `WGUI_PASSWORD` | The password for the user on the login page. Will be hashed automatically. Used for db initialization only | `admin` |
| `WGUI_PASSWORD_FILE` | Optional filepath for the user login password. Will be hashed automatically. Used for db initialization only. Leave `WGUI_PASSWORD` blank to take effect | N/A |
diff --git a/custom/js/helper.js b/custom/js/helper.js
index 39bc1fa..4f21c1c 100644
--- a/custom/js/helper.js
+++ b/custom/js/helper.js
@@ -18,6 +18,11 @@ function renderClientList(data) {
allowedIpsHtml += `${obj} `;
})
+ let subnetRangesString = "";
+ if (obj.Client.subnet_ranges && obj.Client.subnet_ranges.length > 0) {
+ subnetRangesString = obj.Client.subnet_ranges.join(',')
+ }
+
// render client html content
let html = `
@@ -59,6 +64,7 @@ function renderClientList(data) {
${obj.Client.name} ${obj.Client.public_key}
+ ${subnetRangesString} ${obj.Client.email}
${prettyDateTime(obj.Client.created_at)}
diff --git a/handler/routes.go b/handler/routes.go
index aa5461b..0358750 100644
--- a/handler/routes.go
+++ b/handler/routes.go
@@ -366,6 +366,10 @@ func GetClients(db store.IStore) echo.HandlerFunc {
})
}
+ for i, clientData := range clientDataList {
+ clientDataList[i] = util.FillClientSubnetRange(clientData)
+ }
+
return c.JSON(http.StatusOK, clientDataList)
}
}
@@ -391,7 +395,7 @@ func GetClient(db store.IStore) echo.HandlerFunc {
return c.JSON(http.StatusNotFound, jsonHTTPResponse{false, "Client not found"})
}
- return c.JSON(http.StatusOK, clientData)
+ return c.JSON(http.StatusOK, util.FillClientSubnetRange(clientData))
}
}
@@ -988,6 +992,13 @@ func MachineIPAddresses() echo.HandlerFunc {
}
}
+// GetOrderedSubnetRanges handler to get the ordered list of subnet ranges
+func GetOrderedSubnetRanges() echo.HandlerFunc {
+ return func(c echo.Context) error {
+ return c.JSON(http.StatusOK, util.SubnetRangesOrder)
+ }
+}
+
// SuggestIPAllocation handler to get the list of ip address for client
func SuggestIPAllocation(db store.IStore) echo.HandlerFunc {
return func(c echo.Context) error {
@@ -1009,22 +1020,48 @@ func SuggestIPAllocation(db store.IStore) echo.HandlerFunc {
false, "Cannot suggest ip allocation: failed to get list of allocated ip addresses",
})
}
- for _, cidr := range server.Interface.Addresses {
- ip, err := util.GetAvailableIP(cidr, allocatedIPs)
+
+ sr := c.QueryParam("sr")
+ searchCIDRList := make([]string, 0)
+ found := false
+
+ // Use subnet range or default to interface addresses
+ if util.SubnetRanges[sr] != nil {
+ for _, cidr := range util.SubnetRanges[sr] {
+ searchCIDRList = append(searchCIDRList, cidr.String())
+ }
+ } else {
+ searchCIDRList = append(searchCIDRList, server.Interface.Addresses...)
+ }
+
+ // Save only unique IPs
+ ipSet := make(map[string]struct{})
+
+ for _, cidr := range searchCIDRList {
+ ip, err := util.GetAvailableIP(cidr, allocatedIPs, server.Interface.Addresses)
if err != nil {
log.Error("Failed to get available ip from a CIDR: ", err)
- return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{
- false,
- fmt.Sprintf("Cannot suggest ip allocation: failed to get available ip from network %s", cidr),
- })
+ continue
}
+ found = true
if strings.Contains(ip, ":") {
- suggestedIPs = append(suggestedIPs, fmt.Sprintf("%s/128", ip))
+ ipSet[fmt.Sprintf("%s/128", ip)] = struct{}{}
} else {
- suggestedIPs = append(suggestedIPs, fmt.Sprintf("%s/32", ip))
+ ipSet[fmt.Sprintf("%s/32", ip)] = struct{}{}
}
}
+ if !found {
+ return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{
+ false,
+ "Cannot suggest ip allocation: failed to get available ip. Try a different subnet or deallocate some ips.",
+ })
+ }
+
+ for ip := range ipSet {
+ suggestedIPs = append(suggestedIPs, ip)
+ }
+
return c.JSON(http.StatusOK, suggestedIPs)
}
}
diff --git a/main.go b/main.go
index fd4bc90..5f6e341 100644
--- a/main.go
+++ b/main.go
@@ -45,6 +45,7 @@ var (
flagSessionSecret string = util.RandomString(32)
flagWgConfTemplate string
flagBasePath string
+ flagSubnetRanges string
)
const (
@@ -81,6 +82,7 @@ func init() {
flag.StringVar(&flagEmailFromName, "email-from-name", util.LookupEnvOrString("EMAIL_FROM_NAME", flagEmailFromName), "'From' email name.")
flag.StringVar(&flagWgConfTemplate, "wg-conf-template", util.LookupEnvOrString("WG_CONF_TEMPLATE", flagWgConfTemplate), "Path to custom wg.conf template.")
flag.StringVar(&flagBasePath, "base-path", util.LookupEnvOrString("BASE_PATH", flagBasePath), "The base path of the URL")
+ flag.StringVar(&flagSubnetRanges, "subnet-ranges", util.LookupEnvOrString("SUBNET_RANGES", flagSubnetRanges), "IP ranges to choose from when assigning an IP for a client.")
var (
smtpPasswordLookup = util.LookupEnvOrString("SMTP_PASSWORD", flagSmtpPassword)
@@ -127,6 +129,7 @@ func init() {
util.SessionSecret = []byte(flagSessionSecret)
util.WgConfTemplate = flagWgConfTemplate
util.BasePath = util.ParseBasePath(flagBasePath)
+ util.SubnetRanges = util.ParseSubnetRanges(flagSubnetRanges)
// print only if log level is INFO or lower
if lvl, _ := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO")); lvl <= log.INFO {
@@ -145,6 +148,7 @@ func init() {
//fmt.Println("Session secret\t:", util.SessionSecret)
fmt.Println("Custom wg.conf\t:", util.WgConfTemplate)
fmt.Println("Base path\t:", util.BasePath+"/")
+ fmt.Println("Subnet ranges\t:", util.GetSubnetRangesString())
}
}
@@ -170,6 +174,17 @@ func main() {
// create the wireguard config on start, if it doesn't exist
initServerConfig(db, tmplDir)
+ // Check if subnet ranges are valid for the server configuration
+ // Remove any non-valid CIDRs
+ if err := util.ValidateAndFixSubnetRanges(db); err != nil {
+ panic(err)
+ }
+
+ // Print valid ranges
+ if lvl, _ := util.ParseLogLevel(util.LookupEnvOrString(util.LogLevel, "INFO")); lvl <= log.INFO {
+ fmt.Println("Valid subnet ranges:", util.GetSubnetRangesString())
+ }
+
// register routes
app := router.New(tmplDir, extraData, util.SessionSecret)
@@ -218,6 +233,7 @@ func main() {
app.GET(util.BasePath+"/api/clients", handler.GetClients(db), handler.ValidSession)
app.GET(util.BasePath+"/api/client/:id", handler.GetClient(db), handler.ValidSession)
app.GET(util.BasePath+"/api/machine-ips", handler.MachineIPAddresses(), handler.ValidSession)
+ app.GET(util.BasePath+"/api/subnet-ranges", handler.GetOrderedSubnetRanges(), handler.ValidSession)
app.GET(util.BasePath+"/api/suggest-client-ips", handler.SuggestIPAllocation(db), handler.ValidSession)
app.POST(util.BasePath+"/api/apply-wg-config", handler.ApplyServerConfig(db, tmplDir), handler.ValidSession, handler.ContentTypeJson)
app.GET(util.BasePath+"/wake_on_lan_hosts", handler.GetWakeOnLanHosts(db), handler.ValidSession)
diff --git a/model/client.go b/model/client.go
index 95342f0..187ec72 100644
--- a/model/client.go
+++ b/model/client.go
@@ -12,6 +12,7 @@ type Client struct {
PresharedKey string `json:"preshared_key"`
Name string `json:"name"`
Email string `json:"email"`
+ SubnetRanges []string `json:"subnet_ranges,omitempty"`
AllocatedIPs []string `json:"allocated_ips"`
AllowedIPs []string `json:"allowed_ips"`
ExtraAllowedIPs []string `json:"extra_allowed_ips"`
diff --git a/templates/base.html b/templates/base.html
index c2fa367..3f34140 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -58,11 +58,13 @@