Skip to content

Commit 9bcf8c8

Browse files
committed
Enforce unique WireGuard listen ports per group
Backend now ensures WireGuard listen_port is unique for each group during creation and update, with auto-assignment if not provided. Updated frontend theme colors to WireGuard palette and improved empty state spacing in GroupList. Added WireGuard SVG logo to public assets.
1 parent 87c5372 commit 9bcf8c8

File tree

5 files changed

+67
-24
lines changed

5 files changed

+67
-24
lines changed

backend/app/models/group.py

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class Group(db.Model):
2929
server_ip_v6 = db.Column(db.String(39)) # Server's IPv6 in the range
3030

3131
# WireGuard configuration
32-
listen_port = db.Column(db.Integer, default=51820)
32+
listen_port = db.Column(db.Integer, nullable=True) # Auto-assigned if not provided
3333
dns = db.Column(db.String(255), default='1.1.1.1, 8.8.8.8')
3434
endpoint = db.Column(db.String(255))
3535
persistent_keepalive = db.Column(db.Integer, default=25)
@@ -106,6 +106,22 @@ def get_subnet_mask_v6(self):
106106
network = ipaddress.ip_network(self.ip_range_v6, strict=False)
107107
return str(network.prefixlen)
108108

109+
def get_auto_listen_port(self):
110+
"""Get or assign an auto-generated unique listen port based on group ID.
111+
112+
Returns:
113+
int: A unique port number for this group's WireGuard interface
114+
"""
115+
# If port is explicitly set, use it
116+
if self.listen_port and self.listen_port > 0:
117+
return self.listen_port
118+
119+
# Generate unique port: base_port (51820) + group_id
120+
# This ensures each group gets a unique port
121+
# e.g., group 1 -> 51821, group 2 -> 51822, etc.
122+
base_port = 51820
123+
return base_port + self.id
124+
109125
def to_dict(self):
110126
"""Convert to dictionary."""
111127
return {
@@ -137,10 +153,13 @@ def generate_server_config(self):
137153
if self.server_ip_v6 and self.ip_range_v6:
138154
address += f", {self.server_ip_v6}/{self.get_subnet_mask_v6()}"
139155

156+
# Get unique listen port
157+
listen_port = self.get_auto_listen_port()
158+
140159
config = f"""[Interface]
141160
PrivateKey = {self.server_private_key}
142161
Address = {address}
143-
ListenPort = {self.listen_port}
162+
ListenPort = {listen_port}
144163
"""
145164

146165
# Build PostUp rules for NAT and forwarding (simplified - no peer-to-peer restrictions)
@@ -197,7 +216,7 @@ def generate_server_config(self):
197216

198217
def get_group_config_dir(self):
199218
"""Get the configuration directory path for this group.
200-
219+
201220
Returns a group-specific subdirectory for storing client configs.
202221
"""
203222
from config import Config
@@ -210,10 +229,10 @@ def get_group_config_dir(self):
210229
interface_name = self.get_wireguard_interface_name()
211230
group_dir = os.path.join(config_path, interface_name)
212231
return group_dir
213-
232+
214233
def get_server_config_path(self):
215234
"""Get the full path to the server config file.
216-
235+
217236
Returns a path like /etc/wireguard/wg-groupname.conf which wg-quick expects.
218237
Server config is stored in the base WireGuard directory for wg-quick compatibility.
219238
"""
@@ -222,7 +241,7 @@ def get_server_config_path(self):
222241
config_path = Config.WG_CONFIG_PATH
223242
if not config_path:
224243
return None
225-
244+
226245
interface_name = self.get_wireguard_interface_name()
227246
return os.path.join(config_path, f"{interface_name}.conf")
228247

@@ -240,14 +259,14 @@ def save_server_config(self):
240259
# Save server config with secure permissions (0600)
241260
try:
242261
config_content = self.generate_server_config()
243-
262+
244263
# Write with secure permissions
245264
with open(server_filepath, 'w') as f:
246265
f.write(config_content)
247-
266+
248267
# Set secure permissions (0600 - owner read/write only)
249268
os.chmod(server_filepath, 0o600)
250-
269+
251270
logger.info("Server config saved for group_id=%s to %s", self.id, server_filepath)
252271
except Exception as e:
253272
logger.error("Failed to save server config for group_id=%s: %s", self.id, e, exc_info=True)
@@ -265,7 +284,7 @@ def save_server_config(self):
265284

266285
def get_wireguard_interface_name(self):
267286
"""Get the WireGuard interface name for this group.
268-
287+
269288
Uses sanitized group name for the interface name (e.g., 'wg-groupname').
270289
Falls back to group ID if sanitization results in empty string.
271290
"""
@@ -278,20 +297,20 @@ def get_wireguard_interface_name(self):
278297
sanitized = sanitized.replace('--', '-')
279298
# Remove leading/trailing hyphens
280299
sanitized = sanitized.strip('-')
281-
300+
282301
# Fallback to group ID if name is empty after sanitization
283302
if not sanitized:
284303
sanitized = f"group{self.id}"
285-
304+
286305
# Limit length to avoid filesystem issues (max 15 chars for interface name in Linux)
287306
if len(sanitized) > 12: # Leave room for 'wg-' prefix
288307
sanitized = sanitized[:12]
289308
sanitized = sanitized.rstrip('-')
290-
309+
291310
# Final check: ensure we have a valid name
292311
if not sanitized:
293312
sanitized = f"group{self.id}"
294-
313+
295314
return f"wg-{sanitized}"
296315

297316
def start_wireguard(self):
@@ -364,7 +383,7 @@ def delete_server_config(self):
364383
logger.info("Server config deleted for group_id=%s from %s", self.id, server_filepath)
365384
except Exception as e:
366385
logger.error("Failed to delete server config for group_id=%s: %s", self.id, e, exc_info=True)
367-
386+
368387
# Delete group directory with all client configs
369388
group_dir = self.get_group_config_dir()
370389
if group_dir:

backend/app/routes/groups.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,13 @@ def create_group():
113113
logger.info("Duplicate IPv6 range %s attempted by user_id=%s", str(network_v6), user_id)
114114
return jsonify({'error': f'IPv6 range {str(network_v6)} is already in use'}), 400
115115

116+
# Validate listen_port is unique
117+
listen_port = data.get('listen_port', 51820)
118+
existing_port = Group.query.filter_by(listen_port=listen_port).first()
119+
if existing_port:
120+
logger.info("Duplicate listen_port %s attempted by user_id=%s", listen_port, user_id)
121+
return jsonify({'error': f'Listen port {listen_port} is already in use'}), 400
122+
116123
# Generate WireGuard keys
117124
private_key, public_key = generate_keypair()
118125

@@ -125,7 +132,7 @@ def create_group():
125132
server_ip=server_ip,
126133
ip_range_v6=ip_range_v6,
127134
server_ip_v6=server_ip_v6,
128-
listen_port=data.get('listen_port', 51820),
135+
listen_port=listen_port,
129136
dns=data.get('dns', '1.1.1.1, 8.8.8.8'),
130137
endpoint=data.get('endpoint', ''),
131138
persistent_keepalive=data.get('persistent_keepalive', 25),
@@ -221,7 +228,22 @@ def update_group(group_id):
221228

222229
# Only admin can change listen_port
223230
if 'listen_port' in data and user.is_admin():
224-
group.listen_port = data['listen_port']
231+
new_listen_port = data['listen_port']
232+
233+
# Check if new listen_port is unique (if it's different from current)
234+
if new_listen_port != group.listen_port:
235+
existing_port = Group.query.filter(
236+
Group.listen_port == new_listen_port,
237+
Group.id != group_id
238+
).first()
239+
if existing_port:
240+
logger.info(
241+
"Listen port %s already in use; update blocked for group_id=%s user_id=%s",
242+
new_listen_port, group_id, user_id
243+
)
244+
return jsonify({'error': f'Listen port {new_listen_port} is already in use'}), 400
245+
246+
group.listen_port = new_listen_port
225247

226248
db.session.commit()
227249

0 commit comments

Comments
 (0)