first commit
This commit is contained in:
222
dhcp/config.go
Normal file
222
dhcp/config.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package dhcp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
ConfigFile = "/etc/dnsmasq.d/alpine-router-dhcp.conf"
|
||||
StateFile = "/var/lib/alpine-router/dhcp.json"
|
||||
)
|
||||
|
||||
// Pool describes a DHCP pool tied to one interface/subnet.
|
||||
type Pool struct {
|
||||
Interface string `json:"interface"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Subnet string `json:"subnet"` // e.g. "172.16.54.0"
|
||||
Netmask string `json:"netmask"` // e.g. "255.255.255.0"
|
||||
RangeStart string `json:"range_start"` // e.g. "172.16.54.100"
|
||||
RangeEnd string `json:"range_end"` // e.g. "172.16.54.200"
|
||||
Router string `json:"router"` // option routers (advertised gateway)
|
||||
DNS []string `json:"dns"` // option domain-name-servers
|
||||
LeaseTime int `json:"lease_time"` // seconds, 0 → 86400
|
||||
}
|
||||
|
||||
// Config is the top-level DHCP configuration persisted on disk.
|
||||
type Config struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Pools []Pool `json:"pools"`
|
||||
}
|
||||
|
||||
// StaticBinding represents a DHCP host reservation (MAC → fixed IP).
|
||||
type StaticBinding struct {
|
||||
MAC string `json:"mac"`
|
||||
Host string `json:"host"`
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
var mu sync.Mutex
|
||||
|
||||
// IsInstalled reports whether dnsmasq binary is available.
|
||||
func IsInstalled() bool {
|
||||
_, err := exec.LookPath("dnsmasq")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Load reads the config from the state file.
|
||||
// Returns an empty config if the file does not exist yet.
|
||||
func Load() (*Config, error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
data, err := os.ReadFile(StateFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return &Config{Pools: []Pool{}}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("read state: %w", err)
|
||||
}
|
||||
var cfg Config
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("parse state: %w", err)
|
||||
}
|
||||
if cfg.Pools == nil {
|
||||
cfg.Pools = []Pool{}
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// Save writes the config to the state file (JSON, not dnsmasq.conf).
|
||||
func Save(cfg *Config) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if err := os.MkdirAll("/var/lib/alpine-router", 0755); err != nil {
|
||||
return fmt.Errorf("mkdir state dir: %w", err)
|
||||
}
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(StateFile, data, 0644)
|
||||
}
|
||||
|
||||
// WriteConfigs generates /etc/dnsmasq.d/alpine-router-dhcp.conf.
|
||||
// dnsmasq is used in DHCP-only mode (port=0 disables DNS resolver).
|
||||
func WriteConfigs(cfg *Config) error {
|
||||
if err := os.MkdirAll("/etc/dnsmasq.d", 0755); err != nil {
|
||||
return fmt.Errorf("mkdir /etc/dnsmasq.d: %w", err)
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("# Generated by alpine-router — do not edit manually\n")
|
||||
sb.WriteString("# dnsmasq running in DHCP-only mode (DNS disabled)\n\n")
|
||||
sb.WriteString("port=0\n") // disable DNS
|
||||
sb.WriteString("bind-interfaces\n") // only listen on specified interfaces
|
||||
sb.WriteString("\n")
|
||||
|
||||
for _, pool := range cfg.Pools {
|
||||
if !pool.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
leaseTime := pool.LeaseTime
|
||||
if leaseTime <= 0 {
|
||||
leaseTime = 86400
|
||||
}
|
||||
|
||||
tag := pool.Interface // use interface name as tag for option scoping
|
||||
|
||||
fmt.Fprintf(&sb, "# Pool: %s\n", pool.Interface)
|
||||
fmt.Fprintf(&sb, "interface=%s\n", pool.Interface)
|
||||
|
||||
if pool.RangeStart != "" && pool.RangeEnd != "" {
|
||||
fmt.Fprintf(&sb, "dhcp-range=set:%s,%s,%s,%s,%ds\n",
|
||||
tag, pool.RangeStart, pool.RangeEnd, pool.Netmask, leaseTime)
|
||||
}
|
||||
|
||||
if pool.Router != "" {
|
||||
fmt.Fprintf(&sb, "dhcp-option=tag:%s,option:router,%s\n", tag, pool.Router)
|
||||
}
|
||||
|
||||
if len(pool.DNS) > 0 {
|
||||
fmt.Fprintf(&sb, "dhcp-option=tag:%s,option:dns-server,%s\n",
|
||||
tag, strings.Join(pool.DNS, ","))
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
return os.WriteFile(ConfigFile, []byte(sb.String()), 0644)
|
||||
}
|
||||
|
||||
// WriteConfigsWithBindings generates dnsmasq config including static host reservations.
|
||||
func WriteConfigsWithBindings(cfg *Config, bindings []StaticBinding) error {
|
||||
if err := os.MkdirAll("/etc/dnsmasq.d", 0755); err != nil {
|
||||
return fmt.Errorf("mkdir /etc/dnsmasq.d: %w", err)
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("# Generated by alpine-router — do not edit manually\n")
|
||||
sb.WriteString("# dnsmasq running in DHCP-only mode (DNS disabled)\n\n")
|
||||
sb.WriteString("port=0\n")
|
||||
sb.WriteString("bind-interfaces\n")
|
||||
sb.WriteString("\n")
|
||||
|
||||
for _, pool := range cfg.Pools {
|
||||
if !pool.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
leaseTime := pool.LeaseTime
|
||||
if leaseTime <= 0 {
|
||||
leaseTime = 86400
|
||||
}
|
||||
|
||||
tag := pool.Interface
|
||||
|
||||
fmt.Fprintf(&sb, "# Pool: %s\n", pool.Interface)
|
||||
fmt.Fprintf(&sb, "interface=%s\n", pool.Interface)
|
||||
|
||||
if pool.RangeStart != "" && pool.RangeEnd != "" {
|
||||
fmt.Fprintf(&sb, "dhcp-range=set:%s,%s,%s,%s,%ds\n",
|
||||
tag, pool.RangeStart, pool.RangeEnd, pool.Netmask, leaseTime)
|
||||
}
|
||||
|
||||
if pool.Router != "" {
|
||||
fmt.Fprintf(&sb, "dhcp-option=tag:%s,option:router,%s\n", tag, pool.Router)
|
||||
}
|
||||
|
||||
if len(pool.DNS) > 0 {
|
||||
fmt.Fprintf(&sb, "dhcp-option=tag:%s,option:dns-server,%s\n",
|
||||
tag, strings.Join(pool.DNS, ","))
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
if len(bindings) > 0 {
|
||||
sb.WriteString("# Static host reservations\n")
|
||||
for _, b := range bindings {
|
||||
if b.MAC == "" || b.IP == "" {
|
||||
continue
|
||||
}
|
||||
if b.Host != "" {
|
||||
fmt.Fprintf(&sb, "dhcp-host=%s,%s,%s,infinite\n", b.MAC, b.IP, b.Host)
|
||||
} else {
|
||||
fmt.Fprintf(&sb, "dhcp-host=%s,%s,infinite\n", b.MAC, b.IP)
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
return os.WriteFile(ConfigFile, []byte(sb.String()), 0644)
|
||||
}
|
||||
|
||||
// ServiceStatus returns true if dnsmasq is running.
|
||||
func ServiceStatus() bool {
|
||||
return exec.Command("rc-service", "dnsmasq", "status").Run() == nil
|
||||
}
|
||||
|
||||
// ServiceRestart restarts the dnsmasq service.
|
||||
func ServiceRestart() error {
|
||||
out, err := exec.Command("rc-service", "dnsmasq", "restart").CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("restart dnsmasq: %s", strings.TrimSpace(string(out)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServiceStop stops the dnsmasq service.
|
||||
func ServiceStop() error {
|
||||
out, err := exec.Command("rc-service", "dnsmasq", "stop").CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("stop dnsmasq: %s", strings.TrimSpace(string(out)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user