Skip to content

Commit 472c491

Browse files
committed
fix(wireguard): prevent port reset and protect imported peers
- Replace wg syncconf with wg set for live peer add/remove to avoid resetting ListenPort on the interface - Imported peers (from existing wg0.conf) are no longer removed from the config file on delete — only the DB record is removed - Config/QR download properly disabled for imported peers
1 parent dc6babd commit 472c491

2 files changed

Lines changed: 61 additions & 57 deletions

File tree

internal/wireguard/engine_linux.go

Lines changed: 55 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,8 @@ func (e *Engine) AddPeer(name, publicKey, presharedKey, allowedIPs string) error
200200
return fmt.Errorf("write peer block: %w", err)
201201
}
202202

203-
// Apply live without restarting
204-
if err := e.syncConf(); err != nil {
203+
// Apply live: add peer via wg set
204+
if err := e.addPeerLive(publicKey, presharedKey, allowedIPs); err != nil {
205205
return err
206206
}
207207

@@ -216,15 +216,35 @@ func (e *Engine) RemovePeer(name string) error {
216216
return fmt.Errorf("read config: %w", err)
217217
}
218218

219-
re := regexp.MustCompile(`(?ms)\n?# BEGIN_PEER ` + regexp.QuoteMeta(name) + `\n.*?# END_PEER ` + regexp.QuoteMeta(name) + `\n?`)
220-
newData := re.ReplaceAll(data, []byte("\n"))
219+
// Find the public key before removing from config
220+
peerRe := regexp.MustCompile(`(?ms)# BEGIN_PEER ` + regexp.QuoteMeta(name) + `\n(.*?)# END_PEER ` + regexp.QuoteMeta(name))
221+
match := peerRe.FindStringSubmatch(string(data))
222+
var publicKey string
223+
if match != nil {
224+
for _, line := range strings.Split(match[1], "\n") {
225+
line = strings.TrimSpace(line)
226+
if strings.HasPrefix(line, "PublicKey") {
227+
parts := strings.SplitN(line, "=", 2)
228+
if len(parts) == 2 {
229+
publicKey = strings.TrimSpace(parts[1])
230+
}
231+
}
232+
}
233+
}
234+
235+
// Remove from config file
236+
removeRe := regexp.MustCompile(`(?ms)\n?# BEGIN_PEER ` + regexp.QuoteMeta(name) + `\n.*?# END_PEER ` + regexp.QuoteMeta(name) + `\n?`)
237+
newData := removeRe.ReplaceAll(data, []byte("\n"))
221238

222239
if err := os.WriteFile(wgConfPath, newData, 0600); err != nil {
223240
return fmt.Errorf("write config: %w", err)
224241
}
225242

226-
if err := e.syncConf(); err != nil {
227-
return err
243+
// Remove peer live via wg set
244+
if publicKey != "" {
245+
if err := e.removePeerLive(publicKey); err != nil {
246+
slog.Error("failed to remove peer live", "name", name, "error", err)
247+
}
228248
}
229249

230250
slog.Info("wireguard peer removed", "name", name)
@@ -248,7 +268,11 @@ func (e *Engine) UpdatePeerInConfig(oldName, newName, publicKey, presharedKey, a
248268
return fmt.Errorf("write config: %w", err)
249269
}
250270

251-
if err := e.syncConf(); err != nil {
271+
// Remove old peer and add updated one live
272+
if err := e.removePeerLive(publicKey); err != nil {
273+
slog.Error("failed to remove old peer live", "error", err)
274+
}
275+
if err := e.addPeerLive(publicKey, presharedKey, allowedIPs); err != nil {
252276
return err
253277
}
254278

@@ -390,63 +414,39 @@ func (e *Engine) ParseExistingPeers() ([]ParsedPeer, error) {
390414
return peers, nil
391415
}
392416

393-
// syncConf applies the current config without restarting the interface.
394-
func (e *Engine) syncConf() error {
395-
// Strip the interface-level config and create a temp file for wg syncconf
396-
data, err := os.ReadFile(wgConfPath)
417+
// addPeerLive adds a peer to the running WireGuard interface using wg set.
418+
func (e *Engine) addPeerLive(publicKey, presharedKey, allowedIPs string) error {
419+
// wg set requires preshared-key via file descriptor
420+
tmpPsk, err := os.CreateTemp("", "wg-psk-*")
397421
if err != nil {
398-
return fmt.Errorf("read config: %w", err)
422+
return fmt.Errorf("create psk temp file: %w", err)
399423
}
424+
defer os.Remove(tmpPsk.Name())
400425

401-
// wg syncconf needs only the [Peer] sections
402-
var peerSections []string
403-
lines := strings.Split(string(data), "\n")
404-
inPeer := false
405-
var current []string
406-
for _, line := range lines {
407-
trimmed := strings.TrimSpace(line)
408-
if trimmed == "[Peer]" {
409-
inPeer = true
410-
current = []string{line}
411-
continue
412-
}
413-
if trimmed == "[Interface]" {
414-
if inPeer && len(current) > 0 {
415-
peerSections = append(peerSections, strings.Join(current, "\n"))
416-
}
417-
inPeer = false
418-
current = nil
419-
continue
420-
}
421-
if inPeer {
422-
// Skip comment lines in peer sections for syncconf
423-
if strings.HasPrefix(trimmed, "#") {
424-
continue
425-
}
426-
current = append(current, line)
427-
}
428-
}
429-
if inPeer && len(current) > 0 {
430-
peerSections = append(peerSections, strings.Join(current, "\n"))
426+
if _, err := tmpPsk.WriteString(presharedKey); err != nil {
427+
tmpPsk.Close()
428+
return fmt.Errorf("write psk: %w", err)
431429
}
430+
tmpPsk.Close()
432431

433-
tmpFile, err := os.CreateTemp("", "wg-syncconf-*.conf")
432+
out, err := exec.Command("wg", "set", wgInterface,
433+
"peer", publicKey,
434+
"preshared-key", tmpPsk.Name(),
435+
"allowed-ips", allowedIPs,
436+
).CombinedOutput()
434437
if err != nil {
435-
return fmt.Errorf("create temp file: %w", err)
436-
}
437-
defer os.Remove(tmpFile.Name())
438-
439-
content := strings.Join(peerSections, "\n\n")
440-
if _, err := tmpFile.WriteString(content); err != nil {
441-
tmpFile.Close()
442-
return fmt.Errorf("write temp file: %w", err)
438+
return fmt.Errorf("wg set add peer: %s: %w", string(out), err)
443439
}
444-
tmpFile.Close()
440+
return nil
441+
}
445442

446-
out, err := exec.Command("wg", "syncconf", wgInterface, tmpFile.Name()).CombinedOutput()
443+
// removePeerLive removes a peer from the running WireGuard interface using wg set.
444+
func (e *Engine) removePeerLive(publicKey string) error {
445+
out, err := exec.Command("wg", "set", wgInterface,
446+
"peer", publicKey, "remove",
447+
).CombinedOutput()
447448
if err != nil {
448-
return fmt.Errorf("wg syncconf: %s: %w", string(out), err)
449+
return fmt.Errorf("wg set remove peer: %s: %w", string(out), err)
449450
}
450-
451451
return nil
452452
}

internal/wireguard/service.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,14 +239,18 @@ func (s *Service) UpdatePeer(id int64, req UpdatePeerRequest) (*Peer, error) {
239239
}
240240

241241
// DeletePeer removes a peer.
242+
// For imported peers, only removes from DB (config is managed externally).
243+
// For created peers, removes from both DB and wg0.conf.
242244
func (s *Service) DeletePeer(id int64) error {
243245
peer, err := s.repo.GetByID(id)
244246
if err != nil {
245247
return fmt.Errorf("peer not found: %w", err)
246248
}
247249

248-
if err := s.engine.RemovePeer(peer.Name); err != nil {
249-
return fmt.Errorf("remove from wireguard: %w", err)
250+
if !peer.Imported {
251+
if err := s.engine.RemovePeer(peer.Name); err != nil {
252+
return fmt.Errorf("remove from wireguard: %w", err)
253+
}
250254
}
251255

252256
if err := s.repo.Delete(id); err != nil {

0 commit comments

Comments
 (0)