Files

300 lines
7.5 KiB
Go
Raw Permalink Normal View History

2026-04-13 09:46:02 +03:00
package config
import (
"fmt"
"os"
"path/filepath"
"sync"
"gopkg.in/yaml.v3"
)
type InterfaceConfig struct {
2026-04-13 12:40:49 +03:00
Label string `yaml:"label,omitempty"`
2026-04-15 11:38:26 +03:00
Type string `yaml:"type,omitempty"`
2026-04-13 09:46:02 +03:00
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"`
2026-04-15 11:38:26 +03:00
Policy string `yaml:"policy,omitempty"` // "disabled" | "direct" | "vpn" | "" (use default)
}
// ClientPolicyConfig holds the default routing policy for newly discovered clients.
type ClientPolicyConfig struct {
Default string `yaml:"default"` // "disabled" | "direct" | "vpn"
2026-04-13 09:46:02 +03:00
}
type MihomoConfig struct {
Enabled bool `yaml:"enabled"`
}
2026-04-13 12:40:49 +03:00
type FirewallRule struct {
ID string `yaml:"id" json:"id"`
Enabled bool `yaml:"enabled" json:"enabled"`
Action string `yaml:"action" json:"action"`
Protocol string `yaml:"protocol" json:"protocol"`
SrcAddr string `yaml:"src_addr" json:"src_addr"`
SrcPort string `yaml:"src_port" json:"src_port"`
DstAddr string `yaml:"dst_addr" json:"dst_addr"`
DstPort string `yaml:"dst_port" json:"dst_port"`
InIface string `yaml:"in_iface" json:"in_iface"`
OutIface string `yaml:"out_iface" json:"out_iface"`
Comment string `yaml:"comment" json:"comment"`
}
type FirewallConfig struct {
Rules []FirewallRule `yaml:"rules" json:"rules"`
VLANIsolation bool `yaml:"vlan_isolation" json:"vlan_isolation"`
}
2026-04-15 11:38:26 +03:00
type CheckEndpoint struct {
Name string `yaml:"name" json:"name"`
URL string `yaml:"url" json:"url"`
}
type ConnectivityConfig struct {
Direct []CheckEndpoint `yaml:"direct" json:"direct"`
ViaProxy []CheckEndpoint `yaml:"via_proxy" json:"via_proxy"`
}
type AuthConfig struct {
Username string `yaml:"username,omitempty"`
PasswordHash string `yaml:"password_hash,omitempty"`
APIKey string `yaml:"api_key,omitempty"`
}
2026-04-13 09:46:02 +03:00
type AppConfig struct {
2026-04-15 12:25:39 +03:00
Interfaces map[string]*InterfaceConfig `yaml:"interfaces"`
DHCP DHCPConfig `yaml:"dhcp"`
NAT NATConfig `yaml:"nat"`
Firewall FirewallConfig `yaml:"firewall"`
KnownDevices []KnownDevice `yaml:"known_devices"`
Mihomo MihomoConfig `yaml:"mihomo"`
ClientPolicy ClientPolicyConfig `yaml:"client_policy,omitempty"`
Connectivity ConnectivityConfig `yaml:"connectivity,omitempty" json:"connectivity,omitempty"`
Auth AuthConfig `yaml:"auth,omitempty"`
ListenAddresses []string `yaml:"listen_addresses,omitempty" json:"listen_addresses,omitempty"`
2026-04-13 09:46:02 +03:00
}
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},
}
}
2026-04-15 11:38:26 +03:00
func DefaultConnectivity() ConnectivityConfig {
return ConnectivityConfig{
Direct: []CheckEndpoint{
{Name: "Cloudflare", URL: "http://cp.cloudflare.com/generate_204"},
{Name: "Google", URL: "http://connectivitycheck.gstatic.com/generate_204"},
},
ViaProxy: []CheckEndpoint{
{Name: "Cloudflare", URL: "http://cp.cloudflare.com/generate_204"},
{Name: "Google", URL: "http://connectivitycheck.gstatic.com/generate_204"},
},
}
}
2026-04-13 09:46:02 +03:00
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{}
}
2026-04-13 12:40:49 +03:00
if cfg.Firewall.Rules == nil {
cfg.Firewall.Rules = []FirewallRule{}
}
2026-04-13 09:46:02 +03:00
if cfg.KnownDevices == nil {
cfg.KnownDevices = []KnownDevice{}
}
2026-04-15 11:38:26 +03:00
if cfg.ClientPolicy.Default == "" {
cfg.ClientPolicy.Default = "direct"
}
if len(cfg.Connectivity.Direct) == 0 {
cfg.Connectivity = DefaultConnectivity()
}
2026-04-13 09:46:02 +03:00
}
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
}
2026-04-15 11:38:26 +03:00
// Policy is always preserved from existingDev; never overwritten by discovery
2026-04-13 09:46:02 +03:00
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)
}