package config import ( "fmt" "os" "path/filepath" "sync" "gopkg.in/yaml.v3" ) type InterfaceConfig struct { Auto bool `yaml:"auto"` Mode string `yaml:"mode"` Address string `yaml:"address,omitempty"` Netmask string `yaml:"netmask,omitempty"` Gateway string `yaml:"gateway,omitempty"` DNS []string `yaml:"dns,omitempty"` Extra map[string]string `yaml:"extra,omitempty"` } type DHCPPool struct { Interface string `yaml:"interface"` Enabled bool `yaml:"enabled"` Subnet string `yaml:"subnet"` Netmask string `yaml:"netmask"` RangeStart string `yaml:"range_start"` RangeEnd string `yaml:"range_end"` Router string `yaml:"router"` DNS []string `yaml:"dns"` LeaseTime int `yaml:"lease_time"` } type DHCPConfig struct { Enabled bool `yaml:"enabled"` Pools []DHCPPool `yaml:"pools"` } type NATConfig struct { Interfaces []string `yaml:"interfaces"` } type KnownDevice struct { IP string `yaml:"ip"` MAC string `yaml:"mac"` Hostname string `yaml:"hostname"` Blocked bool `yaml:"blocked,omitempty"` StaticIP string `yaml:"static_ip,omitempty"` } type MihomoConfig struct { Enabled bool `yaml:"enabled"` } type AppConfig struct { Interfaces map[string]*InterfaceConfig `yaml:"interfaces"` DHCP DHCPConfig `yaml:"dhcp"` NAT NATConfig `yaml:"nat"` KnownDevices []KnownDevice `yaml:"known_devices"` Mihomo MihomoConfig `yaml:"mihomo"` } var ( mu sync.Mutex filePath string ) func init() { dir := executableDir() filePath = filepath.Join(dir, "config.yaml") } func executableDir() string { exe, err := os.Executable() if err != nil { return "." } // Resolve symlinks — /proc/self/exe on Linux points to real path if resolved, err := filepath.EvalSymlinks(exe); err == nil { exe = resolved } return filepath.Dir(exe) } func SetPath(p string) { mu.Lock() defer mu.Unlock() filePath = p } func GetPath() string { mu.Lock() defer mu.Unlock() return filePath } func Load() (*AppConfig, error) { mu.Lock() defer mu.Unlock() data, err := os.ReadFile(filePath) if err != nil { if os.IsNotExist(err) { return defaultConfig(), nil } return nil, fmt.Errorf("read config: %w", err) } var cfg AppConfig if err := yaml.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("parse config: %w", err) } EnsureDefaults(&cfg) return &cfg, nil } func Save(cfg *AppConfig) error { mu.Lock() defer mu.Unlock() EnsureDefaults(cfg) data, err := yaml.Marshal(cfg) if err != nil { return fmt.Errorf("marshal config: %w", err) } if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { return fmt.Errorf("mkdir config dir: %w", err) } tmp := filePath + ".tmp" if err := os.WriteFile(tmp, data, 0644); err != nil { return fmt.Errorf("write config tmp: %w", err) } if err := os.Rename(tmp, filePath); err != nil { return fmt.Errorf("rename config: %w", err) } return nil } func LoadInto(dst interface{}) error { mu.Lock() defer mu.Unlock() data, err := os.ReadFile(filePath) if err != nil { if os.IsNotExist(err) { return nil } return fmt.Errorf("read config: %w", err) } return yaml.Unmarshal(data, dst) } func defaultConfig() *AppConfig { return &AppConfig{ Interfaces: map[string]*InterfaceConfig{}, DHCP: DHCPConfig{Pools: []DHCPPool{}}, NAT: NATConfig{Interfaces: []string{}}, KnownDevices: []KnownDevice{}, Mihomo: MihomoConfig{Enabled: false}, } } func EnsureDefaults(cfg *AppConfig) { if cfg.Interfaces == nil { cfg.Interfaces = map[string]*InterfaceConfig{} } if cfg.DHCP.Pools == nil { cfg.DHCP.Pools = []DHCPPool{} } if cfg.NAT.Interfaces == nil { cfg.NAT.Interfaces = []string{} } if cfg.KnownDevices == nil { cfg.KnownDevices = []KnownDevice{} } } func UpdateKnownDevices(seen []KnownDevice) error { cfg, err := Load() if err != nil { return err } existing := make(map[string]KnownDevice) for _, d := range cfg.KnownDevices { key := d.MAC if key == "" { key = d.IP } existing[key] = d } for _, d := range seen { key := d.MAC if key == "" { key = d.IP } if existingDev, ok := existing[key]; ok { if d.Hostname != "" { existingDev.Hostname = d.Hostname } if d.IP != "" { existingDev.IP = d.IP } if d.MAC != "" { existingDev.MAC = d.MAC } if d.StaticIP != "" { existingDev.StaticIP = d.StaticIP } existing[key] = existingDev } else { existing[key] = d } } cfg.KnownDevices = make([]KnownDevice, 0, len(existing)) for _, d := range existing { cfg.KnownDevices = append(cfg.KnownDevices, d) } return Save(cfg) }