Compare commits

...
6 Commits
Author SHA1 Message Date
Evgeny VolfertsandGitHub fa13569d4b Add new AmneziaWG parameters (implemented in 3.0/3.1) (#1363)
Docker Build and Push / docker_build (push) Waiting to run
Docker Build and Push / docker_scan (push) Blocked by required conditions
2026-10-29 10:46:10 +01:00
Evgeny VolfertsandGitHub d4661a8f0f fix: clean up endpoint history tables on rename and delete (#1367) 2026-10-29 10:43:21 +01:00
Mikhail SolovevandGitHub fefde651d2 Bug fix. Remove redundant break statement in IP allocation loop. (#1294)
Docker Build and Push / docker_build (push) Canceled after 0s
Docker Build and Push / docker_scan (push) Canceled after 0s
* Remove redundant break statement in IP allocation loop.

* Fix: append IP to allowed_ips list instead of overwriting it
2026-10-28 17:55:57 +01:00
LEE Kai-ZeandGitHub e6f6201d25 Fix: backup regex, SQL cleanup, and restore metadata conflict (#1324)
Docker Build and Push / docker_build (push) Canceled after 0s
Docker Build and Push / docker_scan (push) Canceled after 0s
* fix: backup filename regex dot escaping

* fix: delete paired .sql file when removing backup

* fix: restore backup metadata conflict
2026-10-28 15:01:42 +01:00
unixroandGitHub c4cbbdb51b fix: addPeers uses OverridePeerSettings for DNS and AllowedIPs fallback (#1310)
* fix: addPeers uses OverridePeerSettings for DNS and AllowedIPs fallback

When adding a peer via the API without explicit DNS or endpoint_allowed_ip
values, the endpoint fell back directly to global wg-dashboard.ini defaults,
ignoring any per-configuration OverridePeerSettings. This caused new peers to
be stored with the wrong DNS and AllowedIPs in the database even when the
configuration had overrides set.

Resolve by fetching the configuration's OverridePeerSettings before the global
defaults. The priority is now: request body → OverridePeerSettings → global
default. The existing config lookup that happened later in the function is
removed as it is now done earlier.

Note: downloadPeer already applied OverridePeerSettings at download time, so
client configs were correct; only the stored peer data was affected.

* fix: addPeers applies OverridePeerSettings for MTU and PersistentKeepalive

Extend the peer-settings priority chain (request body → OverridePeerSettings
→ global default) to MTU and PersistentKeepalive, matching the same pattern
already applied to DNS and EndpointAllowedIPs.

* style: align addPeers OverridePeerSettings fallback with project code style
2026-10-28 15:01:12 +01:00
neolegshishovandGitHub 2df57f9405 Fix: batch IP geolocation requests (limit 100), add deduplication and caching (#1232)
Docker Build and Push / docker_build (push) Canceled after 0s
Docker Build and Push / docker_scan (push) Canceled after 0s
- Added batching of IP addresses into chunks of 100 to comply with ip-api limits
- Implemented deduplication to avoid repeated geolocation requests for identical IPs
- Added in-memory cache to reuse results across batches
- Preserved original order and length of geolocation list in API response
- Improved error handling around external API failures
2026-10-27 21:16:57 +01:00
6 changed files with 123 additions and 40 deletions
+49 -23
View File
@@ -945,8 +945,17 @@ def API_addPeers(configName):
allowed_ips: list[str] = data.get('allowed_ips', [])
allowed_ips_validation: bool = data.get('allowed_ips_validation', True)
endpoint_allowed_ip: str = data.get('endpoint_allowed_ip', DashboardConfig.GetConfig("Peers", "peer_endpoint_allowed_ip")[1])
dns_addresses: str = data.get('DNS', DashboardConfig.GetConfig("Peers", "peer_global_DNS")[1])
config = WireguardConfigurations.get(configName)
overridePeerSettings = config.configurationInfo.OverridePeerSettings
endpoint_allowed_ip: str = data.get('endpoint_allowed_ip', (
overridePeerSettings.EndpointAllowedIPs
if overridePeerSettings.EndpointAllowedIPs else DashboardConfig.GetConfig("Peers", "peer_endpoint_allowed_ip")[1]
))
dns_addresses: str = data.get('DNS', (
overridePeerSettings.DNS
if overridePeerSettings.DNS else DashboardConfig.GetConfig("Peers", "peer_global_DNS")[1]
))
mtu: int = data.get('mtu', None)
@@ -955,7 +964,10 @@ def API_addPeers(configName):
preshared_key: str = data.get('preshared_key', "")
if type(mtu) is not int or mtu < 0 or mtu > 1460:
default: str = DashboardConfig.GetConfig("Peers", "peer_mtu")[1]
default: str = (
str(overridePeerSettings.MTU)
if overridePeerSettings.MTU else DashboardConfig.GetConfig("Peers", "peer_mtu")[1]
)
if default.isnumeric():
try:
mtu = int(default)
@@ -964,7 +976,10 @@ def API_addPeers(configName):
else:
mtu = 0
if type(keep_alive) is not int or keep_alive < 0:
default = DashboardConfig.GetConfig("Peers", "peer_keep_alive")[1]
default = (
str(overridePeerSettings.PersistentKeepalive)
if overridePeerSettings.PersistentKeepalive else DashboardConfig.GetConfig("Peers", "peer_keep_alive")[1]
)
if default.isnumeric():
try:
keep_alive = int(default)
@@ -973,7 +988,6 @@ def API_addPeers(configName):
else:
keep_alive = 0
config = WireguardConfigurations.get(configName)
if not config.getStatus():
config.toggleConfiguration()
ipStatus, availableIps = config.getAvailableIP(-1)
@@ -1040,8 +1054,7 @@ def API_addPeers(configName):
if ipStatus:
for subnet in availableIps.keys():
for ip in availableIps[subnet]:
allowed_ips = [ip]
break
allowed_ips.append(ip)
break
else:
return ResponseObject(False, "No more available IP can assign")
@@ -1140,23 +1153,36 @@ def API_GetPeerHistoricalEndpoints():
if not configurationName or not id:
return ResponseObject(False, "Please provide configurationName and id")
fp, p = WireguardConfigurations.get(configurationName).searchPeer(id)
if fp:
result = p.getEndpoints()
geo = {}
try:
r = requests.post(f"http://ip-api.com/batch?fields=city,country,lat,lon,query",
data=json.dumps([x['endpoint'] for x in result]))
d = r.json()
except Exception as e:
return ResponseObject(data=result, message="Failed to request IP address geolocation. " + str(e))
return ResponseObject(data={
"endpoints": p.getEndpoints(),
"geolocation": d
})
if not fp:
return ResponseObject(False, "Peer does not exist")
endpoints = p.getEndpoints()
ips = [x['endpoint'] for x in endpoints]
unique_ips = list(set(ips))
def chunks(lst, size=100):
for i in range(0, len(lst), size):
yield lst[i:i + size]
geo_cache = {}
try:
for batch in chunks(unique_ips, 100):
r = requests.post(
"http://ip-api.com/batch?fields=city,country,lat,lon,query",
data=json.dumps(batch),
timeout=5
)
r.raise_for_status()
data = r.json()
for item in data:
geo_cache[item["query"]] = item
geolocation = [geo_cache[ip] for ip in ips]
except Exception as e:
return ResponseObject(
data={"endpoints": endpoints},
message="Failed to request IP address geolocation. " + str(e)
)
return ResponseObject(data={
"endpoints": endpoints,
"geolocation": geolocation
})
@app.get(f'{APP_PREFIX}/api/getPeerSessions')
def API_GetPeerSessions():
+18
View File
@@ -37,6 +37,15 @@ class AmneziaConfiguration(WireguardConfiguration):
self.I3 = ""
self.I4 = ""
self.I5 = ""
self.HeaderProtectionKey = ""
self.ContentPaddingAddition = 0
self.RekeyAfterTime = 0
self.RekeyTimeout = 0
self.RejectAfterTime = 0
self.KeepaliveTimeout = 0
self.MaxHandshakeAttempts = 0
self.RandomTrailers = 0
self.DisableCookies = 0
self.J1 = ""
self.J2 = ""
self.J3 = ""
@@ -84,6 +93,15 @@ class AmneziaConfiguration(WireguardConfiguration):
"I3": self.I3,
"I4": self.I4,
"I5": self.I5,
"HeaderProtectionKey": self.HeaderProtectionKey,
"ContentPaddingAddition": self.ContentPaddingAddition,
"RekeyAfterTime": self.RekeyAfterTime,
"RekeyTimeout": self.RekeyTimeout,
"RejectAfterTime": self.RejectAfterTime,
"KeepaliveTimeout": self.KeepaliveTimeout,
"MaxHandshakeAttempts": self.MaxHandshakeAttempts,
"RandomTrailers": self.RandomTrailers,
"DisableCookies": self.DisableCookies,
"J1": self.J1,
"J2": self.J2,
"J3": self.J3,
+9
View File
@@ -207,6 +207,15 @@ class Peer:
"I3": self.configuration.I3,
"I4": self.configuration.I4,
"I5": self.configuration.I5,
"HeaderProtectionKey": self.configuration.HeaderProtectionKey,
"ContentPaddingAddition": self.configuration.ContentPaddingAddition,
"RekeyAfterTime": self.configuration.RekeyAfterTime,
"RekeyTimeout": self.configuration.RekeyTimeout,
"RejectAfterTime": self.configuration.RejectAfterTime,
"KeepaliveTimeout": self.configuration.KeepaliveTimeout,
"MaxHandshakeAttempts": self.configuration.MaxHandshakeAttempts,
"RandomTrailers": self.configuration.RandomTrailers,
"DisableCookies": self.configuration.DisableCookies,
"J1": self.configuration.J1,
"J2": self.configuration.J2,
"J3": self.configuration.J3,
+21 -6
View File
@@ -126,6 +126,15 @@ class WireguardConfiguration:
"I3": self.I3,
"I4": self.I4,
"I5": self.I5,
"HeaderProtectionKey": self.HeaderProtectionKey,
"ContentPaddingAddition": self.ContentPaddingAddition,
"RekeyAfterTime": self.RekeyAfterTime,
"RekeyTimeout": self.RekeyTimeout,
"RejectAfterTime": self.RejectAfterTime,
"KeepaliveTimeout": self.KeepaliveTimeout,
"MaxHandshakeAttempts": self.MaxHandshakeAttempts,
"RandomTrailers": self.RandomTrailers,
"DisableCookies": self.DisableCookies,
"J1": self.J1,
"J2": self.J2,
"J3": self.J3,
@@ -240,7 +249,7 @@ class WireguardConfiguration:
self.Status = self.getStatus()
def __dropDatabase(self):
existingTables = [self.Name, f'{self.Name}_restrict_access', f'{self.Name}_transfer', f'{self.Name}_deleted']
existingTables = [self.Name, f'{self.Name}_restrict_access', f'{self.Name}_transfer', f'{self.Name}_deleted', f'{self.Name}_history_endpoint']
try:
with self.engine.begin() as conn:
for name in existingTables:
@@ -315,7 +324,8 @@ class WireguardConfiguration:
f'{dbName}_history_endpoint', self.metadata,
sqlalchemy.Column('id', sqlalchemy.String(255), nullable=False),
sqlalchemy.Column('endpoint', sqlalchemy.String(255), nullable=False),
sqlalchemy.Column('time', time_col_type)
sqlalchemy.Column('time', time_col_type),
extend_existing=True
)
self.infoTable = sqlalchemy.Table(
@@ -949,8 +959,8 @@ class WireguardConfiguration:
files.sort(key=lambda x: x[1], reverse=True)
for f, ct in files:
if RegexMatch(rf"^({self.Name})_(\d+)\\.(conf)$", f):
s = re.search(rf"^({self.Name})_(\d+)\\.(conf)$", f)
if RegexMatch(rf"^({self.Name})_(\d+)\.(conf)$", f):
s = re.search(rf"^({self.Name})_(\d+)\.(conf)$", f)
date = s.group(2)
d = {
"filename": f,
@@ -991,7 +1001,11 @@ class WireguardConfiguration:
if backupFileName not in backups:
return False
try:
os.remove(os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup', backupFileName))
backupPath = os.path.join(self.__getProtocolPath(), 'WGDashboard_Backup')
for ext in ('.conf', '.sql'):
f = os.path.join(backupPath, os.path.splitext(backupFileName)[0] + ext)
if os.path.exists(f):
os.remove(f)
except Exception as e:
return False
return True
@@ -1024,7 +1038,7 @@ class WireguardConfiguration:
allowEdit = ["Address", "PreUp", "PostUp", "PreDown", "PostDown", "ListenPort", "Table"]
awgKeys = []
if self.Protocol == 'awg':
awgKeys = ["Jc", "Jmin", "Jmax", "S1", "S2", "S3", "S4", "H1", "H2", "H3", "H4", "I1", "I2", "I3", "I4", "I5", "J1", "J2", "J3", "Itime"]
awgKeys = ["Jc", "Jmin", "Jmax", "S1", "S2", "S3", "S4", "H1", "H2", "H3", "H4", "I1", "I2", "I3", "I4", "I5", "HeaderProtectionKey", "ContentPaddingAddition", "RekeyAfterTime", "RekeyTimeout", "RejectAfterTime", "KeepaliveTimeout", "MaxHandshakeAttempts", "RandomTrailers", "DisableCookies", "J1", "J2", "J3", "Itime"]
allowEdit += awgKeys
start = original.index("[Interface]")
try:
@@ -1092,6 +1106,7 @@ class WireguardConfiguration:
doRenameStatement("_restrict_access")
doRenameStatement("_deleted")
doRenameStatement("_transfer")
doRenameStatement("_history_endpoint")
conn.execute(
self.infoTable.update()
.where(self.infoTable.c.ID == self.Name)
@@ -197,7 +197,7 @@ const deleteConfigurationModal = ref(false)
v-model="data[key]"
:id="'configuration_' + key">
</div>
<div v-for="key in ['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'S3', 'S4', 'H1', 'H2', 'H3', 'H4', 'I1', 'I2', 'I3', 'I4', 'I5']"
<div v-for="key in ['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'S3', 'S4', 'H1', 'H2', 'H3', 'H4', 'I1', 'I2', 'I3', 'I4', 'I5', 'HeaderProtectionKey', 'ContentPaddingAddition', 'RekeyAfterTime', 'RekeyTimeout', 'RejectAfterTime', 'KeepaliveTimeout', 'MaxHandshakeAttempts', 'RandomTrailers', 'DisableCookies']"
v-if="configurationInfo.Protocol === 'awg'">
<label :for="'configuration_' + key" class="form-label">
<small class="text-muted">
+24 -9
View File
@@ -49,11 +49,20 @@ export default {
H2: 0,
H3: 0,
H4: 0,
I1: "0",
I2: "0",
I3: "0",
I4: "0",
I5: "0"
I1: "",
I2: "",
I3: "",
I4: "",
I5: "",
HeaderProtectionKey: "",
ContentPaddingAddition: "0-64",
RekeyAfterTime: "100-140",
RekeyTimeout: "4-7",
RejectAfterTime: "160-200",
KeepaliveTimeout: "8-12",
MaxHandshakeAttempts: "14-20",
RandomTrailers: "on",
DisableCookies: "on"
},
numberOfAvailableIPs: "0",
error: false,
@@ -72,9 +81,15 @@ export default {
this.newConfiguration[key] = this.rand(1, 2**31);
});
// Initialize I1 to I5 as "0"
['I1', 'I2', 'I3', 'I4', 'I5'].forEach(key => {
this.newConfiguration[key] = "0";
// Initialize HeaderProtectionKey
let headerProtectionKey = new Uint8Array(32);
window.crypto.getRandomValues(headerProtectionKey);
this.newConfiguration.HeaderProtectionKey = btoa(String.fromCharCode(...headerProtectionKey));
// S1-S4 are recommended be the same to work with RandomTrailers and must 12 or more to work with HeaderProtectionKey
let constantS = 12 + this.rand(1, 2**5);
['S1', 'S2', 'S3', 'S4'].forEach(key => {
this.newConfiguration[key] = constantS;
});
},
methods: {
@@ -388,7 +403,7 @@ export default {
<div class="card rounded-3"
v-if="this.newConfiguration.Protocol === 'awg'"
v-for="key in ['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'S3', 'S4', 'H1', 'H2', 'H3', 'H4', 'I1', 'I2', 'I3', 'I4', 'I5']">
v-for="key in ['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'S3', 'S4', 'H1', 'H2', 'H3', 'H4', 'I1', 'I2', 'I3', 'I4', 'I5', 'HeaderProtectionKey', 'ContentPaddingAddition', 'RekeyAfterTime', 'RekeyTimeout', 'RejectAfterTime', 'KeepaliveTimeout', 'MaxHandshakeAttempts', 'RandomTrailers', 'DisableCookies']">
<div class="card-header">{{ key }}</div>
<div class="card-body">
<input type="text"