129 lines
3.0 KiB
Go
129 lines
3.0 KiB
Go
package peer
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"net/netip"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"syscall"
|
|
|
|
"git.crumpington.com/lib/go/flock"
|
|
)
|
|
|
|
const (
|
|
hostsFile = "/etc/hosts"
|
|
hostsBegin = "# BEGIN vppn"
|
|
hostsEnd = "# END vppn"
|
|
)
|
|
|
|
// hostMarkers returns the begin/end marker lines that delimit the managed
|
|
// section for localDomain. The domain is wrapped in parentheses so one domain's
|
|
// marker can never be a prefix of another's (e.g. "net" vs "net2") when
|
|
// multiple vppn instances share /etc/hosts.
|
|
func hostMarkers(localDomain string) (begin, end string) {
|
|
return hostsBegin + "(" + localDomain + ")", hostsEnd + "(" + localDomain + ")"
|
|
}
|
|
|
|
// updateHosts rewrites the managed vppn section in /etc/hosts using the
|
|
// current peersByIP map. Peers without a Name are skipped.
|
|
func (a *App) updateHosts() {
|
|
if a.localDomain == "" {
|
|
return
|
|
}
|
|
if err := updateHosts(hostsFile, a.localDomain, a.peersByIP); err != nil {
|
|
log.Printf("Failed to update hosts file: %v", err)
|
|
}
|
|
}
|
|
|
|
func updateHosts(hostsPath, localDomain string, peers map[netip.Addr]*Peer) error {
|
|
lockFile, err := flock.Lock(hostsPath + ".vppn.lock")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer lockFile.Close()
|
|
|
|
begin, end := hostMarkers(localDomain)
|
|
|
|
info, err := os.Stat(hostsPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
raw, err := os.ReadFile(hostsPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
data := string(raw)
|
|
|
|
before := strings.TrimSpace(data)
|
|
after := ""
|
|
|
|
if idxBegin := strings.Index(data, begin); idxBegin != -1 {
|
|
idxEnd := strings.Index(data[idxBegin:], end)
|
|
if idxEnd != -1 {
|
|
after = strings.TrimSpace(data[idxBegin+idxEnd+len(end):])
|
|
}
|
|
before = strings.TrimSpace(data[:idxBegin])
|
|
}
|
|
|
|
b := strings.Builder{}
|
|
b.WriteString(before)
|
|
b.WriteRune('\n')
|
|
b.WriteString(after)
|
|
b.WriteRune('\n')
|
|
b.WriteRune('\n')
|
|
|
|
b.WriteString(begin)
|
|
b.WriteRune('\n')
|
|
|
|
// Collect entries so we can sort by IP for stable output. Pad the IP
|
|
// column to the width of the widest possible address ("255.255.255.255")
|
|
// for readability.
|
|
type entry struct {
|
|
ip netip.Addr
|
|
host string
|
|
}
|
|
var entries []entry
|
|
for ip, p := range peers {
|
|
if p.Name == "" {
|
|
continue
|
|
}
|
|
entries = append(entries, entry{ip: ip, host: p.Name + "." + localDomain})
|
|
}
|
|
sort.Slice(entries, func(i, j int) bool {
|
|
return entries[i].ip.Less(entries[j].ip)
|
|
})
|
|
|
|
for _, e := range entries {
|
|
b.WriteString(fmt.Sprintf("%-15s %s\n", e.ip.String(), e.host))
|
|
}
|
|
|
|
b.WriteString(end)
|
|
b.WriteRune('\n')
|
|
|
|
// Write to a temp file in the same directory, then rename over the
|
|
// original so readers never observe a partial file. Preserve the
|
|
// original's mode and ownership, since rename replaces the inode.
|
|
tmpPath := hostsPath + ".vppn.tmp"
|
|
if err := os.WriteFile(tmpPath, []byte(b.String()), info.Mode().Perm()); err != nil {
|
|
return err
|
|
}
|
|
|
|
if st, ok := info.Sys().(*syscall.Stat_t); ok {
|
|
if err := os.Chown(tmpPath, int(st.Uid), int(st.Gid)); err != nil {
|
|
os.Remove(tmpPath)
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := os.Rename(tmpPath, hostsPath); err != nil {
|
|
os.Remove(tmpPath)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|