@@ -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}
0 commit comments