feat: pin leases with optional hostname — adds DNS address=/name/IP record for network-wide name resolution
This commit is contained in:
parent
2b65c25219
commit
16b5850aa6
3 changed files with 92 additions and 11 deletions
57
server.go
57
server.go
|
|
@ -120,41 +120,86 @@ func sanitize(s string) string {
|
|||
}, s))
|
||||
}
|
||||
|
||||
// pinLease appends a dhcp-host line to dnsmasq.conf.
|
||||
// pinLease appends a dhcp-host line to dnsmasq.conf. If hostname is provided,
|
||||
// also appends an address=/hostname/IP line so the name resolves network-wide via dnsmasq DNS.
|
||||
func pinLease(mac, ip, hostname string) error {
|
||||
line := fmt.Sprintf("dhcp-host=%s,%s,%s", sanitize(mac), sanitize(ip), sanitize(hostname))
|
||||
safeMAC := sanitize(mac)
|
||||
safeIP := sanitize(ip)
|
||||
safeHost := sanitize(hostname)
|
||||
|
||||
dhcpLine := fmt.Sprintf("dhcp-host=%s,%s,%s", safeMAC, safeIP, safeHost)
|
||||
f, err := os.OpenFile(confFile, os.O_APPEND|os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open config for append: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := fmt.Fprintln(f, line); err != nil {
|
||||
if _, err := fmt.Fprintln(f, dhcpLine); err != nil {
|
||||
return fmt.Errorf("write dhcp-host line: %w", err)
|
||||
}
|
||||
|
||||
// If a custom hostname was given, add a DNS A record too.
|
||||
if safeHost != "" {
|
||||
dnsLine := fmt.Sprintf("address=/%s/%s", safeHost, safeIP)
|
||||
if _, err := fmt.Fprintln(f, dnsLine); err != nil {
|
||||
return fmt.Errorf("write address line: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// unpinLease removes dhcp-host lines matching the given MAC from dnsmasq.conf.
|
||||
// addressPattern matches lines like: address=/hostname/IP
|
||||
var addressPattern = regexp.MustCompile(`^address\s*=\s*/(.+)/(.+)$`)
|
||||
|
||||
// unpinLease removes dhcp-host and associated address= lines matching the given MAC.
|
||||
func unpinLease(mac string) error {
|
||||
macLower := strings.ToLower(mac)
|
||||
|
||||
// First pass: find the IP from the dhcp-host line for this MAC.
|
||||
var targetIP string
|
||||
f, err := os.Open(confFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open config file: %w", err)
|
||||
}
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if matches := dhcpHostPattern.FindStringSubmatch(line); matches != nil {
|
||||
parts := strings.Split(matches[1], ",")
|
||||
if len(parts) > 1 && strings.TrimSpace(strings.ToLower(parts[0])) == macLower {
|
||||
targetIP = strings.TrimSpace(parts[1])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
f.Close()
|
||||
|
||||
// Second pass: rebuild config, removing dhcp-host line + matching address= lines.
|
||||
f, err = os.Open(confFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open config file: %w", err)
|
||||
}
|
||||
|
||||
var kept []string
|
||||
removed := false
|
||||
scanner := bufio.NewScanner(f)
|
||||
scanner = bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
trimmed := strings.TrimSpace(line)
|
||||
// Remove dhcp-host for this MAC.
|
||||
if dhcpHostPattern.MatchString(trimmed) {
|
||||
matchContent := dhcpHostPattern.FindStringSubmatch(trimmed)[1]
|
||||
parts := strings.Split(matchContent, ",")
|
||||
if len(parts) > 0 && strings.TrimSpace(strings.ToLower(parts[0])) == macLower {
|
||||
removed = true
|
||||
continue // skip this line
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Remove address= line for the same IP.
|
||||
if targetIP != "" && addressPattern.MatchString(trimmed) {
|
||||
addrMatches := addressPattern.FindStringSubmatch(trimmed)
|
||||
if strings.TrimSpace(addrMatches[2]) == targetIP {
|
||||
continue
|
||||
}
|
||||
}
|
||||
kept = append(kept, line)
|
||||
|
|
|
|||
38
web/app.js
38
web/app.js
|
|
@ -47,7 +47,12 @@ function renderLeases(data) {
|
|||
'<td class="actions-cell">' +
|
||||
(isPermanent
|
||||
? '<button class="btn btn-danger" data-action="unpin" data-mac="' + escAttr(lease.mac) + '" data-ip="' + escAttr(lease.ip) + '" data-hostname="' + escAttr(lease.hostname) + '">Unpin</button>'
|
||||
: '<button class="btn btn-primary" data-action="pin" data-mac="' + escAttr(lease.mac) + '" data-ip="' + escAttr(lease.ip) + '" data-hostname="' + escAttr(lease.hostname) + '">Pin</button>'
|
||||
: '<button class="btn btn-primary pin-btn" data-action="show-pin-form" data-mac="' + escAttr(lease.mac) + '" data-ip="' + escAttr(lease.ip) + '" data-hostname="' + escAttr(lease.hostname) + '">Pin</button>' +
|
||||
'<span class="pin-form" style="display:none;margin-left:6px;">' +
|
||||
'<input type="text" class="pin-hostname" placeholder="hostname (optional)" size="14" value="' + escAttr(lease.hostname || '') + '">' +
|
||||
'<button class="btn btn-primary pin-confirm" data-mac="' + escAttr(lease.mac) + '" data-ip="' + escAttr(lease.ip) + '">OK</button>' +
|
||||
'<button class="btn btn-secondary pin-cancel">✕</button>' +
|
||||
'</span>'
|
||||
) +
|
||||
'</td>' +
|
||||
'</tr>'
|
||||
|
|
@ -68,7 +73,7 @@ function escAttr(s) {
|
|||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Actions (using $.cjax = Zoraxy's CSRF-safe AJAX wrapper)
|
||||
// Actions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function loadLeases() {
|
||||
|
|
@ -157,10 +162,33 @@ document.getElementById('leases-body').addEventListener('click', function (e) {
|
|||
var ip = btn.dataset.ip;
|
||||
var hostname = btn.dataset.hostname;
|
||||
|
||||
if (action === 'pin') {
|
||||
pinLease(mac, ip, hostname);
|
||||
} else if (action === 'unpin') {
|
||||
if (action === 'unpin') {
|
||||
unpinLease(mac, ip, hostname);
|
||||
} else if (action === 'show-pin-form') {
|
||||
// Show inline form, hide the Pin button
|
||||
var row = btn.closest('td');
|
||||
row.querySelector('.pin-btn').style.display = 'none';
|
||||
row.querySelector('.pin-form').style.display = 'inline';
|
||||
row.querySelector('.pin-hostname').focus();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('leases-body').addEventListener('click', function (e) {
|
||||
// Confirm pin
|
||||
if (e.target.closest('.pin-confirm')) {
|
||||
var row = e.target.closest('td');
|
||||
var mac = e.target.dataset.mac;
|
||||
var ip = e.target.dataset.ip;
|
||||
var hostname = row.querySelector('.pin-hostname').value.trim();
|
||||
row.querySelector('.pin-form').style.display = 'none';
|
||||
row.querySelector('.pin-btn').style.display = 'inline';
|
||||
pinLease(mac, ip, hostname);
|
||||
}
|
||||
// Cancel pin form
|
||||
if (e.target.closest('.pin-cancel')) {
|
||||
var row = e.target.closest('td');
|
||||
row.querySelector('.pin-form').style.display = 'none';
|
||||
row.querySelector('.pin-btn').style.display = 'inline';
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,14 @@ h1 {
|
|||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.pin-form input[type="text"] {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #dcdde1;
|
||||
border-radius: 3px;
|
||||
font-size: 13px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
|
|
|||
Loading…
Reference in a new issue