Ver código fonte

feat(firewall): address-lists in Filter/NAT/Mangle with inversion and dedicated Geo-Location IP filtering

Proxmox Router 1 semana atrás
pai
commit
44cad4e166

+ 11 - 0
internal/api/api.go

@@ -87,6 +87,17 @@ func (s *Server) registerRoutes() {
 	s.mux.HandleFunc("GET /api/firewall/address-lists", s.handleListAddressLists)
 	s.mux.HandleFunc("POST /api/firewall/address-lists", s.handleSaveAddressListEntry)
 	s.mux.HandleFunc("DELETE /api/firewall/address-lists/{id}", s.handleDeleteAddressListEntry)
+	s.mux.HandleFunc("POST /api/firewall/address-lists/{id}/state", s.handleSetAddressListEntryState)
+
+	// Geo-Location
+	s.mux.HandleFunc("GET /api/firewall/geoip/status", s.handleGeoIPStatus)
+	s.mux.HandleFunc("POST /api/firewall/geoip/config", s.handleGeoIPConfig)
+	s.mux.HandleFunc("GET /api/firewall/geoip/countries", s.handleListGeoCountries)
+	s.mux.HandleFunc("POST /api/firewall/geoip/countries/{code}/toggle", s.handleToggleGeoCountry)
+	s.mux.HandleFunc("POST /api/firewall/geoip/countries/{code}/update", s.handleUpdateGeoCountry)
+	s.mux.HandleFunc("POST /api/firewall/geoip/update-now", s.handleGeoIPUpdateNow)
+	s.mux.HandleFunc("POST /api/firewall/geoip/batch-toggle", s.handleGeoIPBatchToggle)
+	s.mux.HandleFunc("GET /api/firewall/geoip/countries/{code}/subnets", s.handleGeoCountrySubnets)
 
 	// Active Connections
 	s.mux.HandleFunc("GET /api/firewall/connections", s.handleListConnections)

+ 94 - 0
internal/api/firewall_handlers.go

@@ -211,6 +211,100 @@ func (s *Server) handleDeleteAddressListEntry(w http.ResponseWriter, r *http.Req
 	writeJSON(w, http.StatusOK, map[string]any{"success": true, "id": id})
 }
 
+func (s *Server) handleSetAddressListEntryState(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	var req struct {
+		Disabled bool `json:"disabled"`
+	}
+	_ = json.NewDecoder(r.Body).Decode(&req)
+	if err := s.fwMgr.SetAddressListEntryState(id, req.Disabled); err != nil {
+		writeError(w, http.StatusInternalServerError, err.Error())
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]any{"success": true, "id": id, "disabled": req.Disabled})
+}
+
+// ======================== Geo-Location ========================
+func (s *Server) handleGeoIPStatus(w http.ResponseWriter, r *http.Request) {
+	status := s.fwMgr.GeoIP().GetStatus()
+	writeJSON(w, http.StatusOK, status)
+}
+
+func (s *Server) handleGeoIPConfig(w http.ResponseWriter, r *http.Request) {
+	var req struct {
+		AutoUpdate    bool `json:"auto_update"`
+		IntervalHours int  `json:"interval_hours"`
+	}
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		writeError(w, http.StatusBadRequest, "invalid request body")
+		return
+	}
+	if err := s.fwMgr.GeoIP().SetConfig(req.AutoUpdate, req.IntervalHours); err != nil {
+		writeError(w, http.StatusInternalServerError, err.Error())
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]any{"success": true, "config": req})
+}
+
+func (s *Server) handleListGeoCountries(w http.ResponseWriter, r *http.Request) {
+	countries := s.fwMgr.GeoIP().ListCountries()
+	writeJSON(w, http.StatusOK, countries)
+}
+
+func (s *Server) handleToggleGeoCountry(w http.ResponseWriter, r *http.Request) {
+	code := r.PathValue("code")
+	var req struct {
+		Enabled bool `json:"enabled"`
+	}
+	_ = json.NewDecoder(r.Body).Decode(&req)
+	if err := s.fwMgr.GeoIP().ToggleCountry(code, req.Enabled); err != nil {
+		writeError(w, http.StatusInternalServerError, err.Error())
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]any{"success": true, "code": code, "enabled": req.Enabled})
+}
+
+func (s *Server) handleUpdateGeoCountry(w http.ResponseWriter, r *http.Request) {
+	code := r.PathValue("code")
+	count, err := s.fwMgr.GeoIP().SyncCountryZone(code)
+	if err != nil {
+		writeError(w, http.StatusInternalServerError, err.Error())
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]any{"success": true, "code": code, "subnets": count})
+}
+
+func (s *Server) handleGeoIPUpdateNow(w http.ResponseWriter, r *http.Request) {
+	if err := s.fwMgr.GeoIP().UpdateNow(); err != nil {
+		writeError(w, http.StatusConflict, err.Error())
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]any{"success": true, "message": "update started"})
+}
+
+func (s *Server) handleGeoIPBatchToggle(w http.ResponseWriter, r *http.Request) {
+	var req struct {
+		Preset  string   `json:"preset"`
+		Codes   []string `json:"codes"`
+		Enabled bool     `json:"enabled"`
+	}
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		writeError(w, http.StatusBadRequest, "invalid request body")
+		return
+	}
+	if err := s.fwMgr.GeoIP().BatchToggle(req.Preset, req.Enabled, req.Codes); err != nil {
+		writeError(w, http.StatusInternalServerError, err.Error())
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]any{"success": true, "preset": req.Preset, "enabled": req.Enabled})
+}
+
+func (s *Server) handleGeoCountrySubnets(w http.ResponseWriter, r *http.Request) {
+	code := r.PathValue("code")
+	subnets := s.fwMgr.GeoIP().GetCountrySubnets(code, 100)
+	writeJSON(w, http.StatusOK, map[string]any{"code": code, "subnets": subnets})
+}
+
 // ======================== Active Connections ========================
 func (s *Server) handleListConnections(w http.ResponseWriter, r *http.Request) {
 	conns, err := firewall.ListActiveConnections()

+ 502 - 0
internal/firewall/geoip.go

@@ -0,0 +1,502 @@
+package firewall
+
+import (
+	"bufio"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"sort"
+	"strings"
+	"sync"
+	"time"
+)
+
+const (
+	GeoIPDir        = "/etc/proxmox-router/firewall/geoip"
+	GeoIPConfigFile = "/etc/proxmox-router/firewall/geoip/config.json"
+	GeoIPZonesDir   = "/etc/proxmox-router/firewall/geoip/zones"
+)
+
+// GeoIPConfig defines interval and enabled country persistence
+type GeoIPConfig struct {
+	AutoUpdate       bool      `json:"auto_update"`
+	IntervalHours    int       `json:"interval_hours"` // e.g. 24
+	LastUpdated      time.Time `json:"last_updated"`
+	NextUpdate       time.Time `json:"next_update"`
+	EnabledCountries []string  `json:"enabled_countries"`
+}
+
+// GeoIPManager handles country IP databases, ipset sync, and auto-update scheduling
+type GeoIPManager struct {
+	mu         sync.RWMutex
+	config     GeoIPConfig
+	countries  map[string]*GeoCountryInfo
+	timer      *time.Timer
+	stopCh     chan struct{}
+	isUpdating bool
+}
+
+func NewGeoIPManager() *GeoIPManager {
+	_ = os.MkdirAll(GeoIPZonesDir, 0755)
+
+	m := &GeoIPManager{
+		countries: make(map[string]*GeoCountryInfo),
+		stopCh:    make(chan struct{}),
+	}
+
+	// 1. Initialize country metadata
+	for _, c := range StandardISOCountries {
+		info := c
+		info.IPSetName = "mk_geo_" + c.Code
+		m.countries[c.Code] = &info
+	}
+
+	// 2. Load Config
+	m.loadConfig()
+
+	// 3. Scan cached zone files
+	m.scanCachedZones()
+
+	// 4. Pre-download / sync initial enabled countries if missing
+	go func() {
+		m.mu.RLock()
+		enabled := make([]string, len(m.config.EnabledCountries))
+		copy(enabled, m.config.EnabledCountries)
+		m.mu.RUnlock()
+
+		for _, code := range enabled {
+			zonePath := filepath.Join(GeoIPZonesDir, strings.ToLower(code)+".zone")
+			if _, err := os.Stat(zonePath); os.IsNotExist(err) {
+				_, _ = m.SyncCountryZone(code)
+			} else {
+				_ = m.ApplyCountryIPSet(code)
+			}
+		}
+	}()
+
+	// 5. Start background auto-update scheduler
+	m.startScheduler()
+
+	return m
+}
+
+func (m *GeoIPManager) loadConfig() {
+	m.config = GeoIPConfig{
+		AutoUpdate:       true,
+		IntervalHours:    24,
+		EnabledCountries: []string{"GR"},
+	}
+
+	if data, err := os.ReadFile(GeoIPConfigFile); err == nil {
+		_ = json.Unmarshal(data, &m.config)
+	}
+
+	if m.config.IntervalHours <= 0 {
+		m.config.IntervalHours = 24
+	}
+
+	// Mark enabled countries
+	for _, code := range m.config.EnabledCountries {
+		if c, ok := m.countries[strings.ToUpper(code)]; ok {
+			c.Enabled = true
+		}
+	}
+}
+
+func (m *GeoIPManager) saveConfig() error {
+	_ = os.MkdirAll(GeoIPDir, 0755)
+
+	m.config.EnabledCountries = make([]string, 0)
+	for code, c := range m.countries {
+		if c.Enabled {
+			m.config.EnabledCountries = append(m.config.EnabledCountries, code)
+		}
+	}
+	sort.Strings(m.config.EnabledCountries)
+
+	data, err := json.MarshalIndent(m.config, "", "  ")
+	if err != nil {
+		return err
+	}
+	return os.WriteFile(GeoIPConfigFile, data, 0644)
+}
+
+func (m *GeoIPManager) scanCachedZones() {
+	for code, c := range m.countries {
+		zoneFile := filepath.Join(GeoIPZonesDir, strings.ToLower(code)+".zone")
+		if stat, err := os.Stat(zoneFile); err == nil && stat.Size() > 0 {
+			c.LastUpdated = stat.ModTime()
+			if count, err := countLines(zoneFile); err == nil {
+				c.SubnetCount = count
+			}
+		}
+	}
+}
+
+func countLines(path string) (int, error) {
+	f, err := os.Open(path)
+	if err != nil {
+		return 0, err
+	}
+	defer f.Close()
+
+	count := 0
+	scanner := bufio.NewScanner(f)
+	for scanner.Scan() {
+		line := strings.TrimSpace(scanner.Text())
+		if line != "" && !strings.HasPrefix(line, "#") {
+			count++
+		}
+	}
+	return count, scanner.Err()
+}
+
+func (m *GeoIPManager) startScheduler() {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	if m.timer != nil {
+		m.timer.Stop()
+	}
+
+	if !m.config.AutoUpdate {
+		return
+	}
+
+	interval := time.Duration(m.config.IntervalHours) * time.Hour
+	now := time.Now()
+
+	// Calculate delay until next run
+	var delay time.Duration
+	if m.config.LastUpdated.IsZero() {
+		delay = 1 * time.Minute
+	} else {
+		next := m.config.LastUpdated.Add(interval)
+		if next.After(now) {
+			delay = next.Sub(now)
+		} else {
+			delay = 10 * time.Second
+		}
+	}
+
+	m.config.NextUpdate = now.Add(delay)
+
+	m.timer = time.AfterFunc(delay, func() {
+		_ = m.UpdateNow()
+		// Re-arm for next full interval
+		m.startScheduler()
+	})
+}
+
+// ListCountries returns all countries sorted by Name with Greece & Cyprus first
+func (m *GeoIPManager) ListCountries() []GeoCountryInfo {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+
+	res := make([]GeoCountryInfo, 0, len(m.countries))
+	for _, c := range m.countries {
+		res = append(res, *c)
+	}
+
+	sort.Slice(res, func(i, j int) bool {
+		// Put enabled countries at the top, then alphabetically
+		if res[i].Enabled != res[j].Enabled {
+			return res[i].Enabled
+		}
+		if res[i].Code == "GR" {
+			return true
+		}
+		if res[j].Code == "GR" {
+			return false
+		}
+		return res[i].Name < res[j].Name
+	})
+
+	return res
+}
+
+func (m *GeoIPManager) GetStatus() map[string]any {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+
+	enabledCount := 0
+	totalSubnets := 0
+	for _, c := range m.countries {
+		if c.Enabled {
+			enabledCount++
+			totalSubnets += c.SubnetCount
+		}
+	}
+
+	return map[string]any{
+		"auto_update":       m.config.AutoUpdate,
+		"interval_hours":    m.config.IntervalHours,
+		"last_updated":      m.config.LastUpdated,
+		"next_update":       m.config.NextUpdate,
+		"enabled_countries": m.config.EnabledCountries,
+		"enabled_count":     enabledCount,
+		"total_subnets":     totalSubnets,
+		"is_updating":       m.isUpdating,
+	}
+}
+
+func (m *GeoIPManager) SetConfig(autoUpdate bool, intervalHours int) error {
+	m.mu.Lock()
+	if intervalHours <= 0 {
+		intervalHours = 24
+	}
+	m.config.AutoUpdate = autoUpdate
+	m.config.IntervalHours = intervalHours
+	_ = m.saveConfig()
+	m.mu.Unlock()
+
+	m.startScheduler()
+	return nil
+}
+
+func (m *GeoIPManager) ToggleCountry(code string, enable bool) error {
+	code = strings.ToUpper(code)
+
+	m.mu.Lock()
+	c, ok := m.countries[code]
+	if !ok {
+		m.mu.Unlock()
+		return fmt.Errorf("country %q not found", code)
+	}
+	c.Enabled = enable
+	_ = m.saveConfig()
+	m.mu.Unlock()
+
+	if enable {
+		zoneFile := filepath.Join(GeoIPZonesDir, strings.ToLower(code)+".zone")
+		if _, err := os.Stat(zoneFile); os.IsNotExist(err) {
+			_, err = m.SyncCountryZone(code)
+			return err
+		}
+		return m.ApplyCountryIPSet(code)
+	}
+
+	// Disable -> destroy ipset
+	setName := "mk_geo_" + code
+	_ = exec.Command("ipset", "flush", setName).Run()
+	_ = exec.Command("ipset", "destroy", setName).Run()
+	return nil
+}
+
+func (m *GeoIPManager) BatchToggle(preset string, enable bool, customCodes []string) error {
+	var targetCodes []string
+
+	switch strings.ToUpper(preset) {
+	case "EU":
+		targetCodes = []string{"AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE"}
+	case "BALKANS":
+		targetCodes = []string{"GR", "CY", "AL", "BG", "RO", "RS", "MK", "ME", "BA", "HR", "SI", "TR"}
+	case "BLOCKLIST", "HIGHRISK":
+		targetCodes = []string{"CN", "RU", "IR", "KP", "BY"}
+	case "ALL":
+		m.mu.RLock()
+		for code := range m.countries {
+			targetCodes = append(targetCodes, code)
+		}
+		m.mu.RUnlock()
+	case "NONE":
+		m.mu.RLock()
+		for code, c := range m.countries {
+			if c.Enabled {
+				targetCodes = append(targetCodes, code)
+			}
+		}
+		m.mu.RUnlock()
+		enable = false
+	default:
+		targetCodes = customCodes
+	}
+
+	for _, code := range targetCodes {
+		_ = m.ToggleCountry(code, enable)
+	}
+
+	return nil
+}
+
+// SyncCountryZone downloads the country zone file from ipdeny and refreshes ipset
+func (m *GeoIPManager) SyncCountryZone(code string) (int, error) {
+	code = strings.ToUpper(code)
+	url := fmt.Sprintf("https://www.ipdeny.com/ipblocks/data/countries/%s.zone", strings.ToLower(code))
+
+	client := &http.Client{Timeout: 15 * time.Second}
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		return 0, err
+	}
+	req.Header.Set("User-Agent", "Proxmox-Router/1.0")
+
+	resp, err := client.Do(req)
+	if err != nil {
+		return 0, fmt.Errorf("failed to fetch zone for %s: %w", code, err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		return 0, fmt.Errorf("ipdeny returned HTTP %d for %s", resp.StatusCode, code)
+	}
+
+	zoneFile := filepath.Join(GeoIPZonesDir, strings.ToLower(code)+".zone")
+	tmpFile := zoneFile + ".tmp"
+
+	out, err := os.Create(tmpFile)
+	if err != nil {
+		return 0, err
+	}
+
+	if _, err := io.Copy(out, resp.Body); err != nil {
+		out.Close()
+		_ = os.Remove(tmpFile)
+		return 0, err
+	}
+	out.Close()
+
+	_ = os.Rename(tmpFile, zoneFile)
+
+	count, _ := countLines(zoneFile)
+
+	m.mu.Lock()
+	if c, ok := m.countries[code]; ok {
+		c.SubnetCount = count
+		c.LastUpdated = time.Now()
+	}
+	m.mu.Unlock()
+
+	// If enabled, load into kernel ipset immediately
+	m.mu.RLock()
+	enabled := m.countries[code] != nil && m.countries[code].Enabled
+	m.mu.RUnlock()
+
+	if enabled {
+		_ = m.ApplyCountryIPSet(code)
+	}
+
+	return count, nil
+}
+
+// ApplyCountryIPSet uses high-speed `ipset restore` to load the country's subnets
+func (m *GeoIPManager) ApplyCountryIPSet(code string) error {
+	code = strings.ToUpper(code)
+	zoneFile := filepath.Join(GeoIPZonesDir, strings.ToLower(code)+".zone")
+
+	f, err := os.Open(zoneFile)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	setName := "mk_geo_" + code
+
+	var sb strings.Builder
+	sb.WriteString(fmt.Sprintf("create %s hash:net maxelem 262144 -exist\n", setName))
+	sb.WriteString(fmt.Sprintf("flush %s\n", setName))
+
+	scanner := bufio.NewScanner(f)
+	for scanner.Scan() {
+		line := strings.TrimSpace(scanner.Text())
+		if line != "" && !strings.HasPrefix(line, "#") {
+			sb.WriteString(fmt.Sprintf("add %s %s -exist\n", setName, line))
+		}
+	}
+
+	cmd := exec.Command("ipset", "restore")
+	cmd.Stdin = strings.NewReader(sb.String())
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		return fmt.Errorf("ipset restore failed for %s: %s: %w", code, string(out), err)
+	}
+
+	return nil
+}
+
+// ApplyAllEnabledSets ensures all enabled countries are present in kernel ipset
+func (m *GeoIPManager) ApplyAllEnabledSets() error {
+	m.mu.RLock()
+	var enabled []string
+	for code, c := range m.countries {
+		if c.Enabled {
+			enabled = append(enabled, code)
+		}
+	}
+	m.mu.RUnlock()
+
+	for _, code := range enabled {
+		_ = m.ApplyCountryIPSet(code)
+	}
+	return nil
+}
+
+// UpdateNow updates zone files for all currently enabled countries
+func (m *GeoIPManager) UpdateNow() error {
+	m.mu.Lock()
+	if m.isUpdating {
+		m.mu.Unlock()
+		return fmt.Errorf("update already in progress")
+	}
+	m.isUpdating = true
+	m.mu.Unlock()
+
+	go func() {
+		defer func() {
+			m.mu.Lock()
+			m.isUpdating = false
+			m.config.LastUpdated = time.Now()
+			m.config.NextUpdate = time.Now().Add(time.Duration(m.config.IntervalHours) * time.Hour)
+			_ = m.saveConfig()
+			m.mu.Unlock()
+		}()
+
+		m.mu.RLock()
+		var enabled []string
+		for code, c := range m.countries {
+			if c.Enabled {
+				enabled = append(enabled, code)
+			}
+		}
+		m.mu.RUnlock()
+
+		for _, code := range enabled {
+			_, _ = m.SyncCountryZone(code)
+		}
+	}()
+
+	return nil
+}
+
+// GetCountrySubnets returns first N subnets from cached zone file for preview
+func (m *GeoIPManager) GetCountrySubnets(code string, limit int) []string {
+	code = strings.ToUpper(code)
+	zoneFile := filepath.Join(GeoIPZonesDir, strings.ToLower(code)+".zone")
+
+	f, err := os.Open(zoneFile)
+	if err != nil {
+		return nil
+	}
+	defer f.Close()
+
+	if limit <= 0 {
+		limit = 100
+	}
+
+	var res []string
+	scanner := bufio.NewScanner(f)
+	for scanner.Scan() {
+		line := strings.TrimSpace(scanner.Text())
+		if line != "" && !strings.HasPrefix(line, "#") {
+			res = append(res, line)
+			if len(res) >= limit {
+				break
+			}
+		}
+	}
+	return res
+}

+ 254 - 0
internal/firewall/geoip_countries.go

@@ -0,0 +1,254 @@
+package firewall
+
+// StandardISOCountries contains all ISO 3166-1 alpha-2 countries with continents
+var StandardISOCountries = []GeoCountryInfo{
+	{Code: "AD", Name: "Andorra", Continent: "Europe", IPSetName: "mk_geo_AD"},
+	{Code: "AE", Name: "United Arab Emirates", Continent: "Asia", IPSetName: "mk_geo_AE"},
+	{Code: "AF", Name: "Afghanistan", Continent: "Asia", IPSetName: "mk_geo_AF"},
+	{Code: "AG", Name: "Antigua and Barbuda", Continent: "Americas", IPSetName: "mk_geo_AG"},
+	{Code: "AI", Name: "Anguilla", Continent: "Americas", IPSetName: "mk_geo_AI"},
+	{Code: "AL", Name: "Albania", Continent: "Europe", IPSetName: "mk_geo_AL"},
+	{Code: "AM", Name: "Armenia", Continent: "Asia", IPSetName: "mk_geo_AM"},
+	{Code: "AO", Name: "Angola", Continent: "Africa", IPSetName: "mk_geo_AO"},
+	{Code: "AQ", Name: "Antarctica", Continent: "Antarctica", IPSetName: "mk_geo_AQ"},
+	{Code: "AR", Name: "Argentina", Continent: "Americas", IPSetName: "mk_geo_AR"},
+	{Code: "AS", Name: "American Samoa", Continent: "Oceania", IPSetName: "mk_geo_AS"},
+	{Code: "AT", Name: "Austria", Continent: "Europe", IPSetName: "mk_geo_AT"},
+	{Code: "AU", Name: "Australia", Continent: "Oceania", IPSetName: "mk_geo_AU"},
+	{Code: "AW", Name: "Aruba", Continent: "Americas", IPSetName: "mk_geo_AW"},
+	{Code: "AX", Name: "Aland Islands", Continent: "Europe", IPSetName: "mk_geo_AX"},
+	{Code: "AZ", Name: "Azerbaijan", Continent: "Asia", IPSetName: "mk_geo_AZ"},
+	{Code: "BA", Name: "Bosnia and Herzegovina", Continent: "Europe", IPSetName: "mk_geo_BA"},
+	{Code: "BB", Name: "Barbados", Continent: "Americas", IPSetName: "mk_geo_BB"},
+	{Code: "BD", Name: "Bangladesh", Continent: "Asia", IPSetName: "mk_geo_BD"},
+	{Code: "BE", Name: "Belgium", Continent: "Europe", IPSetName: "mk_geo_BE"},
+	{Code: "BF", Name: "Burkina Faso", Continent: "Africa", IPSetName: "mk_geo_BF"},
+	{Code: "BG", Name: "Bulgaria", Continent: "Europe", IPSetName: "mk_geo_BG"},
+	{Code: "BH", Name: "Bahrain", Continent: "Asia", IPSetName: "mk_geo_BH"},
+	{Code: "BI", Name: "Burundi", Continent: "Africa", IPSetName: "mk_geo_BI"},
+	{Code: "BJ", Name: "Benin", Continent: "Africa", IPSetName: "mk_geo_BJ"},
+	{Code: "BL", Name: "Saint Barthelemy", Continent: "Americas", IPSetName: "mk_geo_BL"},
+	{Code: "BM", Name: "Bermuda", Continent: "Americas", IPSetName: "mk_geo_BM"},
+	{Code: "BN", Name: "Brunei", Continent: "Asia", IPSetName: "mk_geo_BN"},
+	{Code: "BO", Name: "Bolivia", Continent: "Americas", IPSetName: "mk_geo_BO"},
+	{Code: "BQ", Name: "Bonaire, Sint Eustatius and Saba", Continent: "Americas", IPSetName: "mk_geo_BQ"},
+	{Code: "BR", Name: "Brazil", Continent: "Americas", IPSetName: "mk_geo_BR"},
+	{Code: "BS", Name: "Bahamas", Continent: "Americas", IPSetName: "mk_geo_BS"},
+	{Code: "BT", Name: "Bhutan", Continent: "Asia", IPSetName: "mk_geo_BT"},
+	{Code: "BV", Name: "Bouvet Island", Continent: "Antarctica", IPSetName: "mk_geo_BV"},
+	{Code: "BW", Name: "Botswana", Continent: "Africa", IPSetName: "mk_geo_BW"},
+	{Code: "BY", Name: "Belarus", Continent: "Europe", IPSetName: "mk_geo_BY"},
+	{Code: "BZ", Name: "Belize", Continent: "Americas", IPSetName: "mk_geo_BZ"},
+	{Code: "CA", Name: "Canada", Continent: "Americas", IPSetName: "mk_geo_CA"},
+	{Code: "CC", Name: "Cocos (Keeling) Islands", Continent: "Oceania", IPSetName: "mk_geo_CC"},
+	{Code: "CD", Name: "Congo (DRC)", Continent: "Africa", IPSetName: "mk_geo_CD"},
+	{Code: "CF", Name: "Central African Republic", Continent: "Africa", IPSetName: "mk_geo_CF"},
+	{Code: "CG", Name: "Congo", Continent: "Africa", IPSetName: "mk_geo_CG"},
+	{Code: "CH", Name: "Switzerland", Continent: "Europe", IPSetName: "mk_geo_CH"},
+	{Code: "CI", Name: "Cote d'Ivoire", Continent: "Africa", IPSetName: "mk_geo_CI"},
+	{Code: "CK", Name: "Cook Islands", Continent: "Oceania", IPSetName: "mk_geo_CK"},
+	{Code: "CL", Name: "Chile", Continent: "Americas", IPSetName: "mk_geo_CL"},
+	{Code: "CM", Name: "Cameroon", Continent: "Africa", IPSetName: "mk_geo_CM"},
+	{Code: "CN", Name: "China", Continent: "Asia", IPSetName: "mk_geo_CN"},
+	{Code: "CO", Name: "Colombia", Continent: "Americas", IPSetName: "mk_geo_CO"},
+	{Code: "CR", Name: "Costa Rica", Continent: "Americas", IPSetName: "mk_geo_CR"},
+	{Code: "CU", Name: "Cuba", Continent: "Americas", IPSetName: "mk_geo_CU"},
+	{Code: "CV", Name: "Cabo Verde", Continent: "Africa", IPSetName: "mk_geo_CV"},
+	{Code: "CW", Name: "Curacao", Continent: "Americas", IPSetName: "mk_geo_CW"},
+	{Code: "CX", Name: "Christmas Island", Continent: "Oceania", IPSetName: "mk_geo_CX"},
+	{Code: "CY", Name: "Cyprus", Continent: "Europe", IPSetName: "mk_geo_CY"},
+	{Code: "CZ", Name: "Czechia", Continent: "Europe", IPSetName: "mk_geo_CZ"},
+	{Code: "DE", Name: "Germany", Continent: "Europe", IPSetName: "mk_geo_DE"},
+	{Code: "DJ", Name: "Djibouti", Continent: "Africa", IPSetName: "mk_geo_DJ"},
+	{Code: "DK", Name: "Denmark", Continent: "Europe", IPSetName: "mk_geo_DK"},
+	{Code: "DM", Name: "Dominica", Continent: "Americas", IPSetName: "mk_geo_DM"},
+	{Code: "DO", Name: "Dominican Republic", Continent: "Americas", IPSetName: "mk_geo_DO"},
+	{Code: "DZ", Name: "Algeria", Continent: "Africa", IPSetName: "mk_geo_DZ"},
+	{Code: "EC", Name: "Ecuador", Continent: "Americas", IPSetName: "mk_geo_EC"},
+	{Code: "EE", Name: "Estonia", Continent: "Europe", IPSetName: "mk_geo_EE"},
+	{Code: "EG", Name: "Egypt", Continent: "Africa", IPSetName: "mk_geo_EG"},
+	{Code: "EH", Name: "Western Sahara", Continent: "Africa", IPSetName: "mk_geo_EH"},
+	{Code: "ER", Name: "Eritrea", Continent: "Africa", IPSetName: "mk_geo_ER"},
+	{Code: "ES", Name: "Spain", Continent: "Europe", IPSetName: "mk_geo_ES"},
+	{Code: "ET", Name: "Ethiopia", Continent: "Africa", IPSetName: "mk_geo_ET"},
+	{Code: "FI", Name: "Finland", Continent: "Europe", IPSetName: "mk_geo_FI"},
+	{Code: "FJ", Name: "Fiji", Continent: "Oceania", IPSetName: "mk_geo_FJ"},
+	{Code: "FK", Name: "Falkland Islands", Continent: "Americas", IPSetName: "mk_geo_FK"},
+	{Code: "FM", Name: "Micronesia", Continent: "Oceania", IPSetName: "mk_geo_FM"},
+	{Code: "FO", Name: "Faroe Islands", Continent: "Europe", IPSetName: "mk_geo_FO"},
+	{Code: "FR", Name: "France", Continent: "Europe", IPSetName: "mk_geo_FR"},
+	{Code: "GA", Name: "Gabon", Continent: "Africa", IPSetName: "mk_geo_GA"},
+	{Code: "GB", Name: "United Kingdom", Continent: "Europe", IPSetName: "mk_geo_GB"},
+	{Code: "GD", Name: "Grenada", Continent: "Americas", IPSetName: "mk_geo_GD"},
+	{Code: "GE", Name: "Georgia", Continent: "Asia", IPSetName: "mk_geo_GE"},
+	{Code: "GF", Name: "French Guiana", Continent: "Americas", IPSetName: "mk_geo_GF"},
+	{Code: "GG", Name: "Guernsey", Continent: "Europe", IPSetName: "mk_geo_GG"},
+	{Code: "GH", Name: "Ghana", Continent: "Africa", IPSetName: "mk_geo_GH"},
+	{Code: "GI", Name: "Gibraltar", Continent: "Europe", IPSetName: "mk_geo_GI"},
+	{Code: "GL", Name: "Greenland", Continent: "Americas", IPSetName: "mk_geo_GL"},
+	{Code: "GM", Name: "Gambia", Continent: "Africa", IPSetName: "mk_geo_GM"},
+	{Code: "GN", Name: "Guinea", Continent: "Africa", IPSetName: "mk_geo_GN"},
+	{Code: "GP", Name: "Guadeloupe", Continent: "Americas", IPSetName: "mk_geo_GP"},
+	{Code: "GQ", Name: "Equatorial Guinea", Continent: "Africa", IPSetName: "mk_geo_GQ"},
+	{Code: "GR", Name: "Greece", Continent: "Europe", IPSetName: "mk_geo_GR"},
+	{Code: "GS", Name: "South Georgia", Continent: "Antarctica", IPSetName: "mk_geo_GS"},
+	{Code: "GT", Name: "Guatemala", Continent: "Americas", IPSetName: "mk_geo_GT"},
+	{Code: "GU", Name: "Guam", Continent: "Oceania", IPSetName: "mk_geo_GU"},
+	{Code: "GW", Name: "Guinea-Bissau", Continent: "Africa", IPSetName: "mk_geo_GW"},
+	{Code: "GY", Name: "Guyana", Continent: "Americas", IPSetName: "mk_geo_GY"},
+	{Code: "HK", Name: "Hong Kong", Continent: "Asia", IPSetName: "mk_geo_HK"},
+	{Code: "HM", Name: "Heard Island", Continent: "Antarctica", IPSetName: "mk_geo_HM"},
+	{Code: "HN", Name: "Honduras", Continent: "Americas", IPSetName: "mk_geo_HN"},
+	{Code: "HR", Name: "Croatia", Continent: "Europe", IPSetName: "mk_geo_HR"},
+	{Code: "HT", Name: "Haiti", Continent: "Americas", IPSetName: "mk_geo_HT"},
+	{Code: "HU", Name: "Hungary", Continent: "Europe", IPSetName: "mk_geo_HU"},
+	{Code: "ID", Name: "Indonesia", Continent: "Asia", IPSetName: "mk_geo_ID"},
+	{Code: "IE", Name: "Ireland", Continent: "Europe", IPSetName: "mk_geo_IE"},
+	{Code: "IL", Name: "Israel", Continent: "Asia", IPSetName: "mk_geo_IL"},
+	{Code: "IM", Name: "Isle of Man", Continent: "Europe", IPSetName: "mk_geo_IM"},
+	{Code: "IN", Name: "India", Continent: "Asia", IPSetName: "mk_geo_IN"},
+	{Code: "IO", Name: "British Indian Ocean Territory", Continent: "Africa", IPSetName: "mk_geo_IO"},
+	{Code: "IQ", Name: "Iraq", Continent: "Asia", IPSetName: "mk_geo_IQ"},
+	{Code: "IR", Name: "Iran", Continent: "Asia", IPSetName: "mk_geo_IR"},
+	{Code: "IS", Name: "Iceland", Continent: "Europe", IPSetName: "mk_geo_IS"},
+	{Code: "IT", Name: "Italy", Continent: "Europe", IPSetName: "mk_geo_IT"},
+	{Code: "JE", Name: "Jersey", Continent: "Europe", IPSetName: "mk_geo_JE"},
+	{Code: "JM", Name: "Jamaica", Continent: "Americas", IPSetName: "mk_geo_JM"},
+	{Code: "JO", Name: "Jordan", Continent: "Asia", IPSetName: "mk_geo_JO"},
+	{Code: "JP", Name: "Japan", Continent: "Asia", IPSetName: "mk_geo_JP"},
+	{Code: "KE", Name: "Kenya", Continent: "Africa", IPSetName: "mk_geo_KE"},
+	{Code: "KG", Name: "Kyrgyzstan", Continent: "Asia", IPSetName: "mk_geo_KG"},
+	{Code: "KH", Name: "Cambodia", Continent: "Asia", IPSetName: "mk_geo_KH"},
+	{Code: "KI", Name: "Kiribati", Continent: "Oceania", IPSetName: "mk_geo_KI"},
+	{Code: "KM", Name: "Comoros", Continent: "Africa", IPSetName: "mk_geo_KM"},
+	{Code: "KN", Name: "Saint Kitts and Nevis", Continent: "Americas", IPSetName: "mk_geo_KN"},
+	{Code: "KP", Name: "North Korea", Continent: "Asia", IPSetName: "mk_geo_KP"},
+	{Code: "KR", Name: "South Korea", Continent: "Asia", IPSetName: "mk_geo_KR"},
+	{Code: "KW", Name: "Kuwait", Continent: "Asia", IPSetName: "mk_geo_KW"},
+	{Code: "KY", Name: "Cayman Islands", Continent: "Americas", IPSetName: "mk_geo_KY"},
+	{Code: "KZ", Name: "Kazakhstan", Continent: "Asia", IPSetName: "mk_geo_KZ"},
+	{Code: "LA", Name: "Laos", Continent: "Asia", IPSetName: "mk_geo_LA"},
+	{Code: "LB", Name: "Lebanon", Continent: "Asia", IPSetName: "mk_geo_LB"},
+	{Code: "LC", Name: "Saint Lucia", Continent: "Americas", IPSetName: "mk_geo_LC"},
+	{Code: "LI", Name: "Liechtenstein", Continent: "Europe", IPSetName: "mk_geo_LI"},
+	{Code: "LK", Name: "Sri Lanka", Continent: "Asia", IPSetName: "mk_geo_LK"},
+	{Code: "LR", Name: "Liberia", Continent: "Africa", IPSetName: "mk_geo_LR"},
+	{Code: "LS", Name: "Lesotho", Continent: "Africa", IPSetName: "mk_geo_LS"},
+	{Code: "LT", Name: "Lithuania", Continent: "Europe", IPSetName: "mk_geo_LT"},
+	{Code: "LU", Name: "Luxembourg", Continent: "Europe", IPSetName: "mk_geo_LU"},
+	{Code: "LV", Name: "Latvia", Continent: "Europe", IPSetName: "mk_geo_LV"},
+	{Code: "LY", Name: "Libya", Continent: "Africa", IPSetName: "mk_geo_LY"},
+	{Code: "MA", Name: "Morocco", Continent: "Africa", IPSetName: "mk_geo_MA"},
+	{Code: "MC", Name: "Monaco", Continent: "Europe", IPSetName: "mk_geo_MC"},
+	{Code: "MD", Name: "Moldova", Continent: "Europe", IPSetName: "mk_geo_MD"},
+	{Code: "ME", Name: "Montenegro", Continent: "Europe", IPSetName: "mk_geo_ME"},
+	{Code: "MF", Name: "Saint Martin", Continent: "Americas", IPSetName: "mk_geo_MF"},
+	{Code: "MG", Name: "Madagascar", Continent: "Africa", IPSetName: "mk_geo_MG"},
+	{Code: "MH", Name: "Marshall Islands", Continent: "Oceania", IPSetName: "mk_geo_MH"},
+	{Code: "MK", Name: "North Macedonia", Continent: "Europe", IPSetName: "mk_geo_MK"},
+	{Code: "ML", Name: "Mali", Continent: "Africa", IPSetName: "mk_geo_ML"},
+	{Code: "MM", Name: "Myanmar", Continent: "Asia", IPSetName: "mk_geo_MM"},
+	{Code: "MN", Name: "Mongolia", Continent: "Asia", IPSetName: "mk_geo_MN"},
+	{Code: "MO", Name: "Macao", Continent: "Asia", IPSetName: "mk_geo_MO"},
+	{Code: "MP", Name: "Northern Mariana Islands", Continent: "Oceania", IPSetName: "mk_geo_MP"},
+	{Code: "MQ", Name: "Martinique", Continent: "Americas", IPSetName: "mk_geo_MQ"},
+	{Code: "MR", Name: "Mauritania", Continent: "Africa", IPSetName: "mk_geo_MR"},
+	{Code: "MS", Name: "Montserrat", Continent: "Americas", IPSetName: "mk_geo_MS"},
+	{Code: "MT", Name: "Malta", Continent: "Europe", IPSetName: "mk_geo_MT"},
+	{Code: "MU", Name: "Mauritius", Continent: "Africa", IPSetName: "mk_geo_MU"},
+	{Code: "MV", Name: "Maldives", Continent: "Asia", IPSetName: "mk_geo_MV"},
+	{Code: "MW", Name: "Malawi", Continent: "Africa", IPSetName: "mk_geo_MW"},
+	{Code: "MX", Name: "Mexico", Continent: "Americas", IPSetName: "mk_geo_MX"},
+	{Code: "MY", Name: "Malaysia", Continent: "Asia", IPSetName: "mk_geo_MY"},
+	{Code: "MZ", Name: "Mozambique", Continent: "Africa", IPSetName: "mk_geo_MZ"},
+	{Code: "NA", Name: "Namibia", Continent: "Africa", IPSetName: "mk_geo_NA"},
+	{Code: "NC", Name: "New Caledonia", Continent: "Oceania", IPSetName: "mk_geo_NC"},
+	{Code: "NE", Name: "Niger", Continent: "Africa", IPSetName: "mk_geo_NE"},
+	{Code: "NF", Name: "Norfolk Island", Continent: "Oceania", IPSetName: "mk_geo_NF"},
+	{Code: "NG", Name: "Nigeria", Continent: "Africa", IPSetName: "mk_geo_NG"},
+	{Code: "NI", Name: "Nicaragua", Continent: "Americas", IPSetName: "mk_geo_NI"},
+	{Code: "NL", Name: "Netherlands", Continent: "Europe", IPSetName: "mk_geo_NL"},
+	{Code: "NO", Name: "Norway", Continent: "Europe", IPSetName: "mk_geo_NO"},
+	{Code: "NP", Name: "Nepal", Continent: "Asia", IPSetName: "mk_geo_NP"},
+	{Code: "NR", Name: "Nauru", Continent: "Oceania", IPSetName: "mk_geo_NR"},
+	{Code: "NU", Name: "Niue", Continent: "Oceania", IPSetName: "mk_geo_NU"},
+	{Code: "NZ", Name: "New Zealand", Continent: "Oceania", IPSetName: "mk_geo_NZ"},
+	{Code: "OM", Name: "Oman", Continent: "Asia", IPSetName: "mk_geo_OM"},
+	{Code: "PA", Name: "Panama", Continent: "Americas", IPSetName: "mk_geo_PA"},
+	{Code: "PE", Name: "Peru", Continent: "Americas", IPSetName: "mk_geo_PE"},
+	{Code: "PF", Name: "French Polynesia", Continent: "Oceania", IPSetName: "mk_geo_PF"},
+	{Code: "PG", Name: "Papua New Guinea", Continent: "Oceania", IPSetName: "mk_geo_PG"},
+	{Code: "PH", Name: "Philippines", Continent: "Asia", IPSetName: "mk_geo_PH"},
+	{Code: "PK", Name: "Pakistan", Continent: "Asia", IPSetName: "mk_geo_PK"},
+	{Code: "PL", Name: "Poland", Continent: "Europe", IPSetName: "mk_geo_PL"},
+	{Code: "PM", Name: "Saint Pierre and Miquelon", Continent: "Americas", IPSetName: "mk_geo_PM"},
+	{Code: "PN", Name: "Pitcairn", Continent: "Oceania", IPSetName: "mk_geo_PN"},
+	{Code: "PR", Name: "Puerto Rico", Continent: "Americas", IPSetName: "mk_geo_PR"},
+	{Code: "PS", Name: "Palestine", Continent: "Asia", IPSetName: "mk_geo_PS"},
+	{Code: "PT", Name: "Portugal", Continent: "Europe", IPSetName: "mk_geo_PT"},
+	{Code: "PW", Name: "Palau", Continent: "Oceania", IPSetName: "mk_geo_PW"},
+	{Code: "PY", Name: "Paraguay", Continent: "Americas", IPSetName: "mk_geo_PY"},
+	{Code: "QA", Name: "Qatar", Continent: "Asia", IPSetName: "mk_geo_QA"},
+	{Code: "RE", Name: "Reunion", Continent: "Africa", IPSetName: "mk_geo_RE"},
+	{Code: "RO", Name: "Romania", Continent: "Europe", IPSetName: "mk_geo_RO"},
+	{Code: "RS", Name: "Serbia", Continent: "Europe", IPSetName: "mk_geo_RS"},
+	{Code: "RU", Name: "Russia", Continent: "Europe", IPSetName: "mk_geo_RU"},
+	{Code: "RW", Name: "Rwanda", Continent: "Africa", IPSetName: "mk_geo_RW"},
+	{Code: "SA", Name: "Saudi Arabia", Continent: "Asia", IPSetName: "mk_geo_SA"},
+	{Code: "SB", Name: "Solomon Islands", Continent: "Oceania", IPSetName: "mk_geo_SB"},
+	{Code: "SC", Name: "Seychelles", Continent: "Africa", IPSetName: "mk_geo_SC"},
+	{Code: "SD", Name: "Sudan", Continent: "Africa", IPSetName: "mk_geo_SD"},
+	{Code: "SE", Name: "Sweden", Continent: "Europe", IPSetName: "mk_geo_SE"},
+	{Code: "SG", Name: "Singapore", Continent: "Asia", IPSetName: "mk_geo_SG"},
+	{Code: "SH", Name: "Saint Helena", Continent: "Africa", IPSetName: "mk_geo_SH"},
+	{Code: "SI", Name: "Slovenia", Continent: "Europe", IPSetName: "mk_geo_SI"},
+	{Code: "SJ", Name: "Svalbard and Jan Mayen", Continent: "Europe", IPSetName: "mk_geo_SJ"},
+	{Code: "SK", Name: "Slovakia", Continent: "Europe", IPSetName: "mk_geo_SK"},
+	{Code: "SL", Name: "Sierra Leone", Continent: "Africa", IPSetName: "mk_geo_SL"},
+	{Code: "SM", Name: "San Marino", Continent: "Europe", IPSetName: "mk_geo_SM"},
+	{Code: "SN", Name: "Senegal", Continent: "Africa", IPSetName: "mk_geo_SN"},
+	{Code: "SO", Name: "Somalia", Continent: "Africa", IPSetName: "mk_geo_SO"},
+	{Code: "SR", Name: "Suriname", Continent: "Americas", IPSetName: "mk_geo_SR"},
+	{Code: "SS", Name: "South Sudan", Continent: "Africa", IPSetName: "mk_geo_SS"},
+	{Code: "ST", Name: "Sao Tome and Principe", Continent: "Africa", IPSetName: "mk_geo_ST"},
+	{Code: "SV", Name: "El Salvador", Continent: "Americas", IPSetName: "mk_geo_SV"},
+	{Code: "SX", Name: "Sint Maarten", Continent: "Americas", IPSetName: "mk_geo_SX"},
+	{Code: "SY", Name: "Syria", Continent: "Asia", IPSetName: "mk_geo_SY"},
+	{Code: "SZ", Name: "Eswatini", Continent: "Africa", IPSetName: "mk_geo_SZ"},
+	{Code: "TC", Name: "Turks and Caicos Islands", Continent: "Americas", IPSetName: "mk_geo_TC"},
+	{Code: "TD", Name: "Chad", Continent: "Africa", IPSetName: "mk_geo_TD"},
+	{Code: "TF", Name: "French Southern Territories", Continent: "Antarctica", IPSetName: "mk_geo_TF"},
+	{Code: "TG", Name: "Togo", Continent: "Africa", IPSetName: "mk_geo_TG"},
+	{Code: "TH", Name: "Thailand", Continent: "Asia", IPSetName: "mk_geo_TH"},
+	{Code: "TJ", Name: "Tajikistan", Continent: "Asia", IPSetName: "mk_geo_TJ"},
+	{Code: "TK", Name: "Tokelau", Continent: "Oceania", IPSetName: "mk_geo_TK"},
+	{Code: "TL", Name: "Timor-Leste", Continent: "Asia", IPSetName: "mk_geo_TL"},
+	{Code: "TM", Name: "Turkmenistan", Continent: "Asia", IPSetName: "mk_geo_TM"},
+	{Code: "TN", Name: "Tunisia", Continent: "Africa", IPSetName: "mk_geo_TN"},
+	{Code: "TO", Name: "Tonga", Continent: "Oceania", IPSetName: "mk_geo_TO"},
+	{Code: "TR", Name: "Turkey", Continent: "Asia", IPSetName: "mk_geo_TR"},
+	{Code: "TT", Name: "Trinidad and Tobago", Continent: "Americas", IPSetName: "mk_geo_TT"},
+	{Code: "TV", Name: "Tuvalu", Continent: "Oceania", IPSetName: "mk_geo_TV"},
+	{Code: "TW", Name: "Taiwan", Continent: "Asia", IPSetName: "mk_geo_TW"},
+	{Code: "TZ", Name: "Tanzania", Continent: "Africa", IPSetName: "mk_geo_TZ"},
+	{Code: "UA", Name: "Ukraine", Continent: "Europe", IPSetName: "mk_geo_UA"},
+	{Code: "UG", Name: "Uganda", Continent: "Africa", IPSetName: "mk_geo_UG"},
+	{Code: "UM", Name: "United States Minor Outlying Islands", Continent: "Americas", IPSetName: "mk_geo_UM"},
+	{Code: "US", Name: "United States", Continent: "Americas", IPSetName: "mk_geo_US"},
+	{Code: "UY", Name: "Uruguay", Continent: "Americas", IPSetName: "mk_geo_UY"},
+	{Code: "UZ", Name: "Uzbekistan", Continent: "Asia", IPSetName: "mk_geo_UZ"},
+	{Code: "VA", Name: "Vatican City", Continent: "Europe", IPSetName: "mk_geo_VA"},
+	{Code: "VC", Name: "Saint Vincent and the Grenadines", Continent: "Americas", IPSetName: "mk_geo_VC"},
+	{Code: "VE", Name: "Venezuela", Continent: "Americas", IPSetName: "mk_geo_VE"},
+	{Code: "VG", Name: "Virgin Islands (British)", Continent: "Americas", IPSetName: "mk_geo_VG"},
+	{Code: "VI", Name: "Virgin Islands (U.S.)", Continent: "Americas", IPSetName: "mk_geo_VI"},
+	{Code: "VN", Name: "Vietnam", Continent: "Asia", IPSetName: "mk_geo_VN"},
+	{Code: "VU", Name: "Vanuatu", Continent: "Oceania", IPSetName: "mk_geo_VU"},
+	{Code: "WF", Name: "Wallis and Futuna", Continent: "Oceania", IPSetName: "mk_geo_WF"},
+	{Code: "WS", Name: "Samoa", Continent: "Oceania", IPSetName: "mk_geo_WS"},
+	{Code: "YE", Name: "Yemen", Continent: "Asia", IPSetName: "mk_geo_YE"},
+	{Code: "YT", Name: "Mayotte", Continent: "Africa", IPSetName: "mk_geo_YT"},
+	{Code: "ZA", Name: "South Africa", Continent: "Africa", IPSetName: "mk_geo_ZA"},
+	{Code: "ZM", Name: "Zambia", Continent: "Africa", IPSetName: "mk_geo_ZM"},
+	{Code: "ZW", Name: "Zimbabwe", Continent: "Africa", IPSetName: "mk_geo_ZW"},
+}

+ 11 - 3
internal/firewall/ipset.go

@@ -16,12 +16,12 @@ func SyncAddressLists(entries []AddressListEntry) error {
 		listMap[e.Name] = append(listMap[e.Name], e.Address)
 	}
 
-	// Clean up any mk_ sets that no longer exist or are now empty
+	// Clean up any mk_ sets that no longer exist or are now empty (ignoring mk_geo_ sets managed by GeoIP)
 	if out, err := exec.Command("ipset", "list", "-name").Output(); err == nil {
 		lines := strings.Split(string(out), "\n")
 		for _, line := range lines {
 			line = strings.TrimSpace(line)
-			if strings.HasPrefix(line, "mk_") {
+			if strings.HasPrefix(line, "mk_") && !strings.HasPrefix(line, "mk_geo_") {
 				name := strings.TrimPrefix(line, "mk_")
 				if _, ok := listMap[name]; !ok {
 					_ = exec.Command("ipset", "flush", line).Run()
@@ -50,5 +50,13 @@ func GetIPSetName(listName string) string {
 	if listName == "" {
 		return ""
 	}
-	return "mk_" + strings.ReplaceAll(listName, " ", "_")
+	clean := strings.TrimPrefix(listName, "!")
+	if strings.HasPrefix(clean, "geo:") {
+		code := strings.ToUpper(strings.TrimPrefix(clean, "geo:"))
+		return "mk_geo_" + code
+	}
+	if strings.HasPrefix(clean, "geo_") {
+		return "mk_" + clean
+	}
+	return "mk_" + strings.ReplaceAll(clean, " ", "_")
 }

+ 16 - 2
internal/firewall/iptables.go

@@ -394,11 +394,25 @@ func appendMatchCriteria(args []string, srcAddr, dstAddr, proto, srcPort, dstPor
 	}
 
 	if srcList != "" {
-		args = append(args, fmt.Sprintf("-m set --match-set %s src", GetIPSetName(srcList)))
+		invert := ""
+		cleanList := srcList
+		if strings.HasPrefix(cleanList, "!") {
+			invert = "! "
+			cleanList = strings.TrimPrefix(cleanList, "!")
+		}
+		setName := GetIPSetName(cleanList)
+		args = append(args, fmt.Sprintf("-m set %s--match-set %s src", invert, setName))
 	}
 
 	if dstList != "" {
-		args = append(args, fmt.Sprintf("-m set --match-set %s dst", GetIPSetName(dstList)))
+		invert := ""
+		cleanList := dstList
+		if strings.HasPrefix(cleanList, "!") {
+			invert = "! "
+			cleanList = strings.TrimPrefix(cleanList, "!")
+		}
+		setName := GetIPSetName(cleanList)
+		args = append(args, fmt.Sprintf("-m set %s--match-set %s dst", invert, setName))
 	}
 
 	return args

+ 41 - 2
internal/firewall/manager.go

@@ -22,6 +22,7 @@ type FirewallManager struct {
 	natRules     []NATRule
 	mangleRules  []MangleRule
 	addressLists []AddressListEntry
+	geoIPMgr     *GeoIPManager
 }
 
 func NewFirewallManager() *FirewallManager {
@@ -30,6 +31,7 @@ func NewFirewallManager() *FirewallManager {
 		natRules:     make([]NATRule, 0),
 		mangleRules:  make([]MangleRule, 0),
 		addressLists: make([]AddressListEntry, 0),
+		geoIPMgr:     NewGeoIPManager(),
 	}
 	m.load()
 
@@ -110,15 +112,24 @@ func (m *FirewallManager) save() error {
 	return nil
 }
 
-// Apply re-synchronizes AddressLists with ipset and applies iptables
+// Apply re-synchronizes AddressLists & GeoIP sets with ipset and applies iptables
 func (m *FirewallManager) Apply() error {
-	// Sync ipset address lists
+	// Sync GeoIP country sets
+	if m.geoIPMgr != nil {
+		_ = m.geoIPMgr.ApplyAllEnabledSets()
+	}
+
+	// Sync custom ipset address lists
 	_ = SyncAddressLists(m.addressLists)
 
 	payload := BuildIPTablesRestore(m.filterRules, m.natRules, m.mangleRules)
 	return ApplyIPTables(payload)
 }
 
+func (m *FirewallManager) GeoIP() *GeoIPManager {
+	return m.geoIPMgr
+}
+
 // ==================== Filter Rules ====================
 
 func (m *FirewallManager) ListFilterRules() []FilterRule {
@@ -491,6 +502,20 @@ func (m *FirewallManager) DeleteAddressListEntry(id string) error {
 	return m.Apply()
 }
 
+func (m *FirewallManager) SetAddressListEntryState(id string, disabled bool) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	for i := range m.addressLists {
+		if m.addressLists[i].ID == id {
+			m.addressLists[i].Disabled = disabled
+			_ = m.save()
+			return m.Apply()
+		}
+	}
+	return fmt.Errorf("address list entry not found")
+}
+
 func (m *FirewallManager) ResetCounters() error {
 	_ = exec.Command("iptables", "-Z").Run()
 	return nil
@@ -502,11 +527,25 @@ func (m *FirewallManager) GetSummary() FirewallSummary {
 
 	conns, _ := ListActiveConnections()
 
+	geoCount := 0
+	geoSubnets := 0
+	if m.geoIPMgr != nil {
+		status := m.geoIPMgr.GetStatus()
+		if c, ok := status["enabled_count"].(int); ok {
+			geoCount = c
+		}
+		if s, ok := status["total_subnets"].(int); ok {
+			geoSubnets = s
+		}
+	}
+
 	return FirewallSummary{
 		FilterRulesCount:  len(m.filterRules),
 		NATRulesCount:     len(m.natRules),
 		MangleRulesCount:  len(m.mangleRules),
 		AddressListsCount: len(m.addressLists),
+		GeoCountriesCount: geoCount,
+		GeoSubnetsCount:   geoSubnets,
 		ActiveConnections: len(conns),
 	}
 }

+ 20 - 5
internal/firewall/types.go

@@ -1,5 +1,7 @@
 package firewall
 
+import "time"
+
 // FilterRule represents a MikroTik-style /ip firewall filter rule
 type FilterRule struct {
 	ID              string   `json:"id"`
@@ -90,6 +92,17 @@ type AddressListEntry struct {
 	Disabled bool   `json:"disabled"`
 }
 
+// GeoCountryInfo represents a Geo-Location country entry in the firewall
+type GeoCountryInfo struct {
+	Code        string `json:"code"`         // e.g. "GR"
+	Name        string `json:"name"`         // e.g. "Greece"
+	Continent   string `json:"continent"`    // e.g. "Europe"
+	SubnetCount int    `json:"subnet_count"` // number of CIDR subnets
+	Enabled     bool   `json:"enabled"`      // enabled in firewall (mk_geo_XX)
+	IPSetName   string `json:"ipset_name"`   // e.g. "mk_geo_GR"
+	LastUpdated time.Time `json:"last_updated,omitempty"`
+}
+
 // ActiveConnection represents an active flow from /proc/net/nf_conntrack / conntrack table
 type ActiveConnection struct {
 	ID               string `json:"id"`
@@ -112,9 +125,11 @@ type ActiveConnection struct {
 
 // FirewallSummary contains total rule and connection counts
 type FirewallSummary struct {
-	FilterRulesCount   int `json:"filter_rules_count"`
-	NATRulesCount      int `json:"nat_rules_count"`
-	MangleRulesCount   int `json:"mangle_rules_count"`
-	AddressListsCount  int `json:"address_lists_count"`
-	ActiveConnections  int `json:"active_connections"`
+	FilterRulesCount  int `json:"filter_rules_count"`
+	NATRulesCount     int `json:"nat_rules_count"`
+	MangleRulesCount  int `json:"mangle_rules_count"`
+	AddressListsCount int `json:"address_lists_count"`
+	GeoCountriesCount int `json:"geo_countries_count"`
+	GeoSubnetsCount   int `json:"geo_subnets_count"`
+	ActiveConnections int `json:"active_connections"`
 }

+ 470 - 15
internal/web/static/app.js

@@ -2122,7 +2122,7 @@ function renderActionBadge(action) {
 
 function switchFWSubTab(tab) {
   state.fwSubTab = tab;
-  const tabs = ['filter', 'nat', 'mangle', 'addrlist', 'connections'];
+  const tabs = ['filter', 'nat', 'mangle', 'addrlist', 'geo', 'connections'];
   tabs.forEach(t => {
     const panel = document.getElementById(`fw-subtab-${t}`);
     const btn = document.getElementById(`fw-subtab-btn-${t}`);
@@ -2145,9 +2145,11 @@ function switchFWSubTab(tab) {
     else if (tab === 'nat') addBtn.innerHTML = '<span>+ Add NAT Rule</span>';
     else if (tab === 'mangle') addBtn.innerHTML = '<span>+ Add Mangle Rule</span>';
     else if (tab === 'addrlist') addBtn.innerHTML = '<span>+ Add Address List</span>';
+    else if (tab === 'geo') addBtn.innerHTML = '<span>⚙️ Schedule Settings</span>';
     else if (tab === 'connections') addBtn.innerHTML = '<span>Refresh Flows</span>';
   }
 
+  if (tab === 'geo') loadGeoIP();
   if (tab === 'connections') loadConnections();
 }
 
@@ -2156,17 +2158,20 @@ function openCurrentFWModal() {
   else if (state.fwSubTab === 'nat') openAddNATModal();
   else if (state.fwSubTab === 'mangle') openAddMangleModal();
   else if (state.fwSubTab === 'addrlist') openAddAddrListModal();
+  else if (state.fwSubTab === 'geo') openGeoIntervalModal();
   else if (state.fwSubTab === 'connections') loadConnections();
 }
 
 async function loadFirewall(populateDropdowns = true) {
   try {
-    const [summaryRes, filterRes, natRes, mangleRes, addrlistRes] = await Promise.all([
+    const [summaryRes, filterRes, natRes, mangleRes, addrlistRes, geoStatusRes, geoCountriesRes] = await Promise.all([
       fetch(`${API_BASE}/firewall/summary`),
       fetch(`${API_BASE}/firewall/filter`),
       fetch(`${API_BASE}/firewall/nat`),
       fetch(`${API_BASE}/firewall/mangle`),
-      fetch(`${API_BASE}/firewall/address-lists`)
+      fetch(`${API_BASE}/firewall/address-lists`),
+      fetch(`${API_BASE}/firewall/geoip/status`),
+      fetch(`${API_BASE}/firewall/geoip/countries`)
     ]);
 
     if (summaryRes.ok) state.fwSummary = await summaryRes.json();
@@ -2174,6 +2179,8 @@ async function loadFirewall(populateDropdowns = true) {
     if (natRes.ok) state.natRules = await natRes.json();
     if (mangleRes.ok) state.mangleRules = await mangleRes.json();
     if (addrlistRes.ok) state.addressLists = await addrlistRes.json();
+    if (geoStatusRes.ok) state.geoStatus = await geoStatusRes.json();
+    if (geoCountriesRes.ok) state.geoCountries = await geoCountriesRes.json();
 
     // Badges
     const bFilter = document.getElementById('fw-badge-filter');
@@ -2184,11 +2191,14 @@ async function loadFirewall(populateDropdowns = true) {
     if (bMangle) bMangle.innerText = (state.mangleRules || []).length;
     const bAddr = document.getElementById('fw-badge-addrlist');
     if (bAddr) bAddr.innerText = (state.addressLists || []).length;
+    const bGeo = document.getElementById('fw-badge-geo');
+    if (bGeo) bGeo.innerText = state.geoStatus?.enabled_count || 0;
 
     renderFilterRules();
     renderNATRules();
     renderMangleRules();
     renderAddressLists();
+    renderGeoCountries();
 
     if (state.fwSubTab === 'connections') {
       loadConnections();
@@ -2197,6 +2207,10 @@ async function loadFirewall(populateDropdowns = true) {
     console.error('Failed to load firewall:', err);
   }
 }
+  } catch (err) {
+    console.error('Failed to load firewall:', err);
+  }
+}
 
 // ----------------- Filter Rules -----------------
 function renderFilterRules() {
@@ -2265,14 +2279,49 @@ function populateAddressListSelect(selectId, currentVal = '') {
   const sel = document.getElementById(selectId);
   if (!sel) return;
   sel.innerHTML = '<option value="">(None)</option>';
-  const set = new Set((state.addressLists || []).map(a => a.name));
-  set.forEach(name => {
+
+  const cleanVal = currentVal.startsWith('!') ? currentVal.substring(1) : currentVal;
+
+  // 1. Custom Address Lists
+  const customNames = Array.from(new Set((state.addressLists || []).map(a => a.name))).sort();
+  if (customNames.length > 0) {
+    const optgroup = document.createElement('optgroup');
+    optgroup.label = 'Custom Address Lists';
+    customNames.forEach(name => {
+      const count = (state.addressLists || []).filter(a => a.name === name).length;
+      const opt = document.createElement('option');
+      opt.value = name;
+      opt.innerText = `${name} (${count} entries)`;
+      if (name === cleanVal) opt.selected = true;
+      optgroup.appendChild(opt);
+    });
+    sel.appendChild(optgroup);
+  }
+
+  // 2. Enabled Geo-Location Countries
+  const geoCountries = (state.geoCountries || []).filter(c => c.enabled);
+  if (geoCountries.length > 0) {
+    const optgroup = document.createElement('optgroup');
+    optgroup.label = '🌍 Geo-Location Countries';
+    geoCountries.forEach(c => {
+      const val = `geo:${c.code}`;
+      const opt = document.createElement('option');
+      opt.value = val;
+      opt.innerText = `${c.name} (${c.code}) - ${c.subnet_count.toLocaleString()} subnets`;
+      if (val === cleanVal || `geo_${c.code}` === cleanVal) opt.selected = true;
+      optgroup.appendChild(opt);
+    });
+    sel.appendChild(optgroup);
+  }
+
+  // Fallback: If cleanVal is set but not in list, add it as custom option
+  if (cleanVal && !sel.querySelector(`option[value="${cleanVal}"]`)) {
     const opt = document.createElement('option');
-    opt.value = name;
-    opt.innerText = name;
-    if (name === currentVal) opt.selected = true;
+    opt.value = cleanVal;
+    opt.innerText = cleanVal;
+    opt.selected = true;
     sel.appendChild(opt);
-  });
+  }
 }
 
 function openAddFilterModal() {
@@ -2515,6 +2564,12 @@ function openAddNATModal() {
 
   populateInterfaceSelect('nat-modal-iniface');
   populateInterfaceSelect('nat-modal-outiface');
+  populateAddressListSelect('nat-modal-srclist');
+  populateAddressListSelect('nat-modal-dstlist');
+  const srcInvEl = document.getElementById('nat-modal-srclist-inv');
+  if (srcInvEl) srcInvEl.checked = false;
+  const dstInvEl = document.getElementById('nat-modal-dstlist-inv');
+  if (dstInvEl) dstInvEl.checked = false;
   toggleNATActionFields();
 
   document.getElementById('nat-modal').classList.remove('hidden');
@@ -2539,6 +2594,15 @@ function openEditNATModal(id) {
 
   populateInterfaceSelect('nat-modal-iniface', r.in_interface);
   populateInterfaceSelect('nat-modal-outiface', r.out_interface);
+
+  const srcInv = (r.src_address_list || '').startsWith('!');
+  const dstInv = (r.dst_address_list || '').startsWith('!');
+  const srcInvEl = document.getElementById('nat-modal-srclist-inv');
+  if (srcInvEl) srcInvEl.checked = srcInv;
+  const dstInvEl = document.getElementById('nat-modal-dstlist-inv');
+  if (dstInvEl) dstInvEl.checked = dstInv;
+  populateAddressListSelect('nat-modal-srclist', r.src_address_list || '');
+  populateAddressListSelect('nat-modal-dstlist', r.dst_address_list || '');
   toggleNATActionFields();
 
   document.getElementById('nat-modal').classList.remove('hidden');
@@ -2581,6 +2645,14 @@ async function submitSaveNATRule(e) {
   const dport = document.getElementById('nat-modal-dport').value.trim();
   const inIface = document.getElementById('nat-modal-iniface').value;
   const outIface = document.getElementById('nat-modal-outiface').value;
+  let srclist = document.getElementById('nat-modal-srclist')?.value || '';
+  if (srclist && document.getElementById('nat-modal-srclist-inv')?.checked) {
+    srclist = '!' + srclist.replace(/^!/, '');
+  }
+  let dstlist = document.getElementById('nat-modal-dstlist')?.value || '';
+  if (dstlist && document.getElementById('nat-modal-dstlist-inv')?.checked) {
+    dstlist = '!' + dstlist.replace(/^!/, '');
+  }
   const comment = document.getElementById('nat-modal-comment').value.trim();
   const disabled = document.getElementById('nat-modal-disabled').checked;
 
@@ -2589,6 +2661,23 @@ async function submitSaveNATRule(e) {
       method: 'POST',
       headers: { 'Content-Type': 'application/json' },
       body: JSON.stringify({
+        id,
+        chain,
+        action,
+        to_addresses: toaddr,
+        to_ports: toport,
+        protocol: proto,
+        src_address: src,
+        dst_address: dst,
+        dst_port: dport,
+        in_interface: inIface,
+        out_interface: outIface,
+        src_address_list: srclist,
+        dst_address_list: dstlist,
+        comment,
+        disabled
+      })
+    });
         id,
         chain,
         action,
@@ -2715,6 +2804,13 @@ function openAddMangleModal() {
   document.getElementById('mangle-modal-disabled').checked = false;
 
   populateInterfaceSelect('mangle-modal-iniface');
+  populateAddressListSelect('mangle-modal-srclist');
+  populateAddressListSelect('mangle-modal-dstlist');
+  const srcInvEl = document.getElementById('mangle-modal-srclist-inv');
+  if (srcInvEl) srcInvEl.checked = false;
+  const dstInvEl = document.getElementById('mangle-modal-dstlist-inv');
+  if (dstInvEl) dstInvEl.checked = false;
+
   document.getElementById('mangle-modal').classList.remove('hidden');
 }
 
@@ -2735,6 +2831,16 @@ function openEditMangleModal(id) {
   document.getElementById('mangle-modal-disabled').checked = !!r.disabled;
 
   populateInterfaceSelect('mangle-modal-iniface', r.in_interface);
+
+  const srcInv = (r.src_address_list || '').startsWith('!');
+  const dstInv = (r.dst_address_list || '').startsWith('!');
+  const srcInvEl = document.getElementById('mangle-modal-srclist-inv');
+  if (srcInvEl) srcInvEl.checked = srcInv;
+  const dstInvEl = document.getElementById('mangle-modal-dstlist-inv');
+  if (dstInvEl) dstInvEl.checked = dstInv;
+  populateAddressListSelect('mangle-modal-srclist', r.src_address_list || '');
+  populateAddressListSelect('mangle-modal-dstlist', r.dst_address_list || '');
+
   document.getElementById('mangle-modal').classList.remove('hidden');
 }
 
@@ -2756,6 +2862,14 @@ async function submitSaveMangleRule(e) {
   const src = document.getElementById('mangle-modal-src').value.trim();
   const dst = document.getElementById('mangle-modal-dst').value.trim();
   const inIface = document.getElementById('mangle-modal-iniface').value;
+  let srclist = document.getElementById('mangle-modal-srclist')?.value || '';
+  if (srclist && document.getElementById('mangle-modal-srclist-inv')?.checked) {
+    srclist = '!' + srclist.replace(/^!/, '');
+  }
+  let dstlist = document.getElementById('mangle-modal-dstlist')?.value || '';
+  if (dstlist && document.getElementById('mangle-modal-dstlist-inv')?.checked) {
+    dstlist = '!' + dstlist.replace(/^!/, '');
+  }
   const connmark = document.getElementById('mangle-modal-connmark').value.trim();
   const comment = document.getElementById('mangle-modal-comment').value.trim();
   const disabled = document.getElementById('mangle-modal-disabled').checked;
@@ -2768,6 +2882,8 @@ async function submitSaveMangleRule(e) {
     src_address: src,
     dst_address: dst,
     in_interface: inIface,
+    src_address_list: srclist,
+    dst_address_list: dstlist,
     connection_mark: connmark,
     comment,
     disabled
@@ -2867,14 +2983,17 @@ function renderAddressLists() {
 
   entries.forEach((e, idx) => {
     const tr = document.createElement('tr');
-    tr.className = `hover:bg-slate-800/40 transition text-xs ${e.disabled ? 'opacity-50' : ''}`;
+    tr.className = `hover:bg-slate-800/40 transition text-xs cursor-pointer ${e.disabled ? 'opacity-50' : ''}`;
+    tr.setAttribute('ondblclick', `openEditAddrListModal('${e.id}')`);
     tr.innerHTML = `
       <td class="py-2.5 px-3 text-center text-slate-500 font-bold">${idx + 1}</td>
-      <td class="py-2.5 px-3 text-center">${e.disabled ? '<span class="text-rose-400 font-bold">X</span>' : '<span class="text-slate-600">-</span>'}</td>
+      <td class="py-2.5 px-3 text-center">${e.disabled ? '<span class="text-rose-400 font-bold" title="Disabled">X</span>' : '<span class="text-emerald-400 font-bold">✓</span>'}</td>
       <td class="py-2.5 px-3 font-bold text-white font-mono">${e.name}</td>
       <td class="py-2.5 px-3 font-mono font-bold text-emerald-400">${e.address}</td>
-      <td class="py-2.5 px-3 text-slate-400 italic">${e.comment || '-'}</td>
-      <td class="py-2.5 px-3 text-right font-sans">
+      <td class="py-2.5 px-3 text-slate-400 italic truncate max-w-[150px]">${e.comment || '-'}</td>
+      <td class="py-2.5 px-3 text-right space-x-2 font-sans whitespace-nowrap">
+        <button onclick="toggleAddrListState('${e.id}', ${!e.disabled})" class="text-xs ${e.disabled ? 'text-emerald-400' : 'text-slate-400'} hover:underline">${e.disabled ? 'Enable' : 'Disable'}</button>
+        <button onclick="openEditAddrListModal('${e.id}')" class="text-xs text-cyan-400 hover:text-cyan-300 font-medium">Edit</button>
         <button onclick="deleteAddrList('${e.id}')" class="text-xs text-slate-500 hover:text-rose-400">Delete</button>
       </td>
     `;
@@ -2883,9 +3002,29 @@ function renderAddressLists() {
 }
 
 function openAddAddrListModal() {
+  document.getElementById('addrlist-modal-title').innerText = 'Add Address List Entry';
+  document.getElementById('addrlist-modal-id').value = '';
   document.getElementById('addrlist-modal-name').value = '';
   document.getElementById('addrlist-modal-addr').value = '';
   document.getElementById('addrlist-modal-comment').value = '';
+  document.getElementById('addrlist-modal-disabled').checked = false;
+  const submitBtn = document.getElementById('addrlist-modal-submit-btn');
+  if (submitBtn) submitBtn.innerText = 'Add to List';
+  document.getElementById('addrlist-modal').classList.remove('hidden');
+}
+
+function openEditAddrListModal(id) {
+  const e = (state.addressLists || []).find(item => item.id === id);
+  if (!e) return;
+
+  document.getElementById('addrlist-modal-title').innerText = `Edit Address List Entry: ${e.name}`;
+  document.getElementById('addrlist-modal-id').value = e.id;
+  document.getElementById('addrlist-modal-name').value = e.name;
+  document.getElementById('addrlist-modal-addr').value = e.address;
+  document.getElementById('addrlist-modal-comment').value = e.comment || '';
+  document.getElementById('addrlist-modal-disabled').checked = !!e.disabled;
+  const submitBtn = document.getElementById('addrlist-modal-submit-btn');
+  if (submitBtn) submitBtn.innerText = 'Save Changes';
   document.getElementById('addrlist-modal').classList.remove('hidden');
 }
 
@@ -2895,20 +3034,40 @@ function closeAddrListModal() {
 
 async function submitSaveAddrList(e) {
   e.preventDefault();
+  const id = document.getElementById('addrlist-modal-id').value;
   const name = document.getElementById('addrlist-modal-name').value.trim();
   const addr = document.getElementById('addrlist-modal-addr').value.trim();
   const comment = document.getElementById('addrlist-modal-comment').value.trim();
+  const disabled = document.getElementById('addrlist-modal-disabled').checked;
 
   try {
     const res = await fetch(`${API_BASE}/firewall/address-lists`, {
       method: 'POST',
       headers: { 'Content-Type': 'application/json' },
-      body: JSON.stringify({ name, address: addr, comment, disabled: false })
+      body: JSON.stringify({ id, name, address: addr, comment, disabled })
     });
     if (res.ok) {
-      showToast('Address List Item Added', `Added ${addr} to ${name}`, 'success');
+      showToast('Address List Item Saved', `Saved ${addr} in ${name}`, 'success');
       closeAddrListModal();
       loadFirewall(false);
+    } else {
+      const err = await res.json();
+      showToast('Error', err.message || 'Failed to save address list', 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function toggleAddrListState(id, disabled) {
+  try {
+    const res = await fetch(`${API_BASE}/firewall/address-lists/${encodeURIComponent(id)}/state`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ disabled })
+    });
+    if (res.ok) {
+      loadFirewall(false);
     }
   } catch (err) {
     showToast('Error', err.message, 'error');
@@ -2916,6 +3075,7 @@ async function submitSaveAddrList(e) {
 }
 
 async function deleteAddrList(id) {
+  if (!confirm('Are you sure you want to delete this address list item?')) return;
   try {
     const res = await fetch(`${API_BASE}/firewall/address-lists/${encodeURIComponent(id)}`, { method: 'DELETE' });
     if (res.ok) {
@@ -2927,6 +3087,301 @@ async function deleteAddrList(id) {
   }
 }
 
+// ----------------- Geo-Location IP Management -----------------
+async function loadGeoIP() {
+  try {
+    const [statusRes, countriesRes] = await Promise.all([
+      fetch(`${API_BASE}/firewall/geoip/status`),
+      fetch(`${API_BASE}/firewall/geoip/countries`)
+    ]);
+
+    if (statusRes.ok) state.geoStatus = await statusRes.json();
+    if (countriesRes.ok) state.geoCountries = await countriesRes.json();
+
+    updateGeoHeader();
+    renderGeoCountries();
+  } catch (err) {
+    console.error('Failed to load GeoIP:', err);
+  }
+}
+
+function updateGeoHeader() {
+  const s = state.geoStatus || {};
+  const statusBadge = document.getElementById('geo-status-badge');
+  if (statusBadge) {
+    statusBadge.innerText = s.auto_update ? 'Auto-Update ON' : 'Auto-Update OFF';
+    statusBadge.className = s.auto_update
+      ? 'px-2 py-0.5 rounded text-[10px] font-mono font-bold bg-emerald-500/20 text-emerald-300 border border-emerald-500/30'
+      : 'px-2 py-0.5 rounded text-[10px] font-mono font-bold bg-slate-800 text-slate-400';
+  }
+
+  const intervalLabel = document.getElementById('geo-interval-label');
+  if (intervalLabel) {
+    intervalLabel.innerText = `Every ${s.interval_hours || 24}h`;
+  }
+
+  const activeCountEl = document.getElementById('geo-stat-active-countries');
+  if (activeCountEl) activeCountEl.innerText = (s.enabled_count || 0).toLocaleString();
+
+  const totalSubnetsEl = document.getElementById('geo-stat-total-subnets');
+  if (totalSubnetsEl) totalSubnetsEl.innerText = (s.total_subnets || 0).toLocaleString();
+
+  const lastSyncEl = document.getElementById('geo-stat-last-sync');
+  if (lastSyncEl) {
+    if (s.last_updated && !s.last_updated.startsWith('0001')) {
+      const d = new Date(s.last_updated);
+      lastSyncEl.innerText = d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+    } else {
+      lastSyncEl.innerText = 'Never (Sync Pending)';
+    }
+  }
+
+  const nextSyncEl = document.getElementById('geo-stat-next-sync');
+  if (nextSyncEl) {
+    if (s.next_update && !s.next_update.startsWith('0001')) {
+      const d = new Date(s.next_update);
+      nextSyncEl.innerText = d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+    } else {
+      nextSyncEl.innerText = '-';
+    }
+  }
+
+  const badge = document.getElementById('fw-badge-geo');
+  if (badge) badge.innerText = s.enabled_count || 0;
+}
+
+function renderGeoCountries() {
+  const tbody = document.getElementById('geo-table-body');
+  const empty = document.getElementById('geo-empty');
+  if (!tbody) return;
+  tbody.innerHTML = '';
+
+  const q = (document.getElementById('geo-search-input')?.value || '').trim().toLowerCase();
+  const continent = document.getElementById('geo-continent-filter')?.value || '';
+  const enabledOnly = document.getElementById('geo-filter-enabled-only')?.checked;
+
+  const countries = (state.geoCountries || []).filter(c => {
+    if (enabledOnly && !c.enabled) return false;
+    if (continent && c.continent !== continent) return false;
+    if (q) {
+      const matchCode = c.code.toLowerCase().includes(q);
+      const matchName = c.name.toLowerCase().includes(q);
+      if (!matchCode && !matchName) return false;
+    }
+    return true;
+  });
+
+  if (countries.length === 0) {
+    empty?.classList.remove('hidden');
+    return;
+  }
+  empty?.classList.add('hidden');
+
+  countries.forEach(c => {
+    const tr = document.createElement('tr');
+    tr.className = `hover:bg-slate-800/40 transition text-xs ${c.enabled ? 'bg-emerald-950/10' : ''}`;
+
+    tr.innerHTML = `
+      <td class="py-2.5 px-3 text-center">
+        <span class="px-2 py-0.5 rounded font-mono font-bold ${c.enabled ? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/40' : 'bg-slate-800 text-cyan-300 border border-slate-700'}">
+          ${c.code}
+        </span>
+      </td>
+      <td class="py-2.5 px-3 font-bold text-white">${c.name}</td>
+      <td class="py-2.5 px-3 text-slate-400">${c.continent}</td>
+      <td class="py-2.5 px-3 font-mono ${c.subnet_count > 0 ? 'text-emerald-400 font-bold' : 'text-slate-500'}">
+        ${c.subnet_count > 0 ? c.subnet_count.toLocaleString() + ' CIDRs' : 'Not Cached'}
+      </td>
+      <td class="py-2.5 px-3 font-mono text-purple-300">${c.ipset_name}</td>
+      <td class="py-2.5 px-3">
+        ${c.enabled
+          ? '<span class="px-2 py-0.5 rounded text-[10px] font-mono font-bold bg-emerald-500/20 text-emerald-300 border border-emerald-500/40">ENABLED</span>'
+          : '<span class="px-2 py-0.5 rounded text-[10px] font-mono font-bold bg-slate-800 text-slate-500">DISABLED</span>'}
+      </td>
+      <td class="py-2.5 px-3 text-right space-x-2 font-sans whitespace-nowrap">
+        ${c.enabled
+          ? `<button onclick="toggleGeoCountry('${c.code}', false)" class="text-xs text-rose-400 hover:underline font-semibold">Disable</button>`
+          : `<button onclick="toggleGeoCountry('${c.code}', true)" class="text-xs text-emerald-400 hover:underline font-semibold">Enable</button>`}
+        <button onclick="updateGeoCountry('${c.code}')" class="text-xs text-cyan-400 hover:text-cyan-300" title="Re-download latest country IP blocks">Sync</button>
+        <button onclick="viewGeoSubnets('${c.code}', '${c.name.replace(/'/g, "\\'")}')" class="text-xs text-slate-400 hover:text-white" title="Preview Subnet Ranges">View</button>
+      </td>
+    `;
+    tbody.appendChild(tr);
+  });
+}
+
+async function toggleGeoCountry(code, enabled) {
+  try {
+    const res = await fetch(`${API_BASE}/firewall/geoip/countries/${encodeURIComponent(code)}/toggle`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ enabled })
+    });
+    if (res.ok) {
+      showToast(enabled ? 'Country Enabled' : 'Country Disabled', `${code} ${enabled ? 'active in firewall' : 'removed from firewall'}`, 'success');
+      loadGeoIP();
+    } else {
+      const err = await res.json();
+      showToast('Error', err.message || 'Failed to toggle country', 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function updateGeoCountry(code) {
+  try {
+    showToast('Syncing Country', `Downloading latest IP blocks for ${code}...`, 'info');
+    const res = await fetch(`${API_BASE}/firewall/geoip/countries/${encodeURIComponent(code)}/update`, { method: 'POST' });
+    if (res.ok) {
+      const data = await res.json();
+      showToast('Synced', `Loaded ${data.subnets} CIDR blocks for ${code}`, 'success');
+      loadGeoIP();
+    } else {
+      const err = await res.json();
+      showToast('Sync Error', err.message || 'Failed to sync country', 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function triggerGeoUpdateNow() {
+  const spinner = document.getElementById('geo-update-spinner');
+  if (spinner) spinner.classList.remove('hidden');
+
+  try {
+    const res = await fetch(`${API_BASE}/firewall/geoip/update-now`, { method: 'POST' });
+    if (res.ok) {
+      showToast('Update Started', 'Updating all active GeoIP country zones in background...', 'info');
+      setTimeout(() => {
+        loadGeoIP();
+        if (spinner) spinner.classList.add('hidden');
+      }, 2500);
+    } else {
+      const err = await res.json();
+      showToast('Notice', err.message || 'Update in progress', 'info');
+      if (spinner) spinner.classList.add('hidden');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+    if (spinner) spinner.classList.add('hidden');
+  }
+}
+
+async function applyGeoPreset(preset) {
+  try {
+    const res = await fetch(`${API_BASE}/firewall/geoip/batch-toggle`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ preset, enabled: preset !== 'NONE' })
+    });
+    if (res.ok) {
+      showToast('Preset Applied', `Preset ${preset} updated in firewall`, 'success');
+      loadGeoIP();
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+function openGeoIntervalModal() {
+  const s = state.geoStatus || {};
+  document.getElementById('geo-cfg-autoupdate').checked = s.auto_update !== false;
+
+  const sel = document.getElementById('geo-cfg-interval');
+  const customBox = document.getElementById('geo-cfg-custom-box');
+  const customHoursInput = document.getElementById('geo-cfg-custom-hours');
+
+  const curHours = s.interval_hours || 24;
+  const matchOption = Array.from(sel.options).find(o => parseInt(o.value, 10) === curHours);
+
+  if (matchOption) {
+    sel.value = String(curHours);
+    customBox.classList.add('hidden');
+  } else {
+    sel.value = 'custom';
+    customHoursInput.value = curHours;
+    customBox.classList.remove('hidden');
+  }
+
+  document.getElementById('geo-interval-modal').classList.remove('hidden');
+}
+
+function closeGeoIntervalModal() {
+  document.getElementById('geo-interval-modal').classList.add('hidden');
+}
+
+function toggleCustomGeoInterval() {
+  const val = document.getElementById('geo-cfg-interval').value;
+  const customBox = document.getElementById('geo-cfg-custom-box');
+  if (val === 'custom') {
+    customBox.classList.remove('hidden');
+  } else {
+    customBox.classList.add('hidden');
+  }
+}
+
+async function submitGeoIntervalSettings(e) {
+  e.preventDefault();
+  const autoUpdate = document.getElementById('geo-cfg-autoupdate').checked;
+  const intervalVal = document.getElementById('geo-cfg-interval').value;
+  let hours = 24;
+
+  if (intervalVal === 'custom') {
+    hours = parseInt(document.getElementById('geo-cfg-custom-hours').value, 10) || 24;
+  } else {
+    hours = parseInt(intervalVal, 10) || 24;
+  }
+
+  try {
+    const res = await fetch(`${API_BASE}/firewall/geoip/config`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ auto_update: autoUpdate, interval_hours: hours })
+    });
+    if (res.ok) {
+      showToast('Schedule Saved', `Auto-update set to every ${hours} hours`, 'success');
+      closeGeoIntervalModal();
+      loadGeoIP();
+    } else {
+      const err = await res.json();
+      showToast('Error', err.message || 'Failed to save schedule', 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function viewGeoSubnets(code, name) {
+  document.getElementById('geo-subnets-title').innerText = `${name} (${code}) Subnets`;
+  document.getElementById('geo-subnets-subtitle').innerText = `Kernel IPSet: mk_geo_${code}`;
+  const listEl = document.getElementById('geo-subnets-list');
+  listEl.innerHTML = '<span class="text-slate-500">Loading subnets...</span>';
+  document.getElementById('geo-subnets-modal').classList.remove('hidden');
+
+  try {
+    const res = await fetch(`${API_BASE}/firewall/geoip/countries/${encodeURIComponent(code)}/subnets`);
+    if (res.ok) {
+      const data = await res.json();
+      const subnets = data.subnets || [];
+      if (subnets.length === 0) {
+        listEl.innerHTML = '<span class="text-slate-500">No cached subnets found. Click Sync to download this zone.</span>';
+      } else {
+        listEl.innerHTML = subnets.map(s => `<div>${s}</div>`).join('');
+      }
+    } else {
+      listEl.innerHTML = '<span class="text-rose-400">Failed to load subnets.</span>';
+    }
+  } catch (err) {
+    listEl.innerHTML = `<span class="text-rose-400">Error: ${err.message}</span>`;
+  }
+}
+
+function closeGeoSubnetsModal() {
+  document.getElementById('geo-subnets-modal').classList.add('hidden');
+}
+
 // ----------------- Active Connections Tracking -----------------
 async function loadConnections() {
   try {

+ 226 - 1
internal/web/static/index.html

@@ -1197,6 +1197,10 @@
               <span>Address Lists</span>
               <span id="fw-badge-addrlist" class="px-1.5 py-0.2 rounded text-[10px] bg-slate-800 text-slate-300">0</span>
             </button>
+            <button id="fw-subtab-btn-geo" onclick="switchFWSubTab('geo')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap">
+              <span>Geo-Location</span>
+              <span id="fw-badge-geo" class="px-1.5 py-0.2 rounded text-[10px] bg-slate-800 text-slate-300">0</span>
+            </button>
             <button id="fw-subtab-btn-connections" onclick="switchFWSubTab('connections')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap">
               <span>Connections</span>
               <span id="fw-badge-connections" class="px-1.5 py-0.2 rounded text-[10px] bg-emerald-950 text-emerald-400 border border-emerald-800/40">0</span>
@@ -1416,6 +1420,112 @@
               No active connections found matching your search.
             </div>
           </div>
+
+          <!-- ================= TAB 5: GEO-LOCATION ================= -->
+          <div id="fw-subtab-geo" class="hidden space-y-4">
+            <!-- Geo Header & Scheduler Status -->
+            <div class="bg-slate-900/90 border border-slate-800 rounded-xl p-4 flex flex-wrap items-center justify-between gap-4">
+              <div class="flex items-center space-x-3">
+                <div class="p-2.5 rounded-lg bg-emerald-500/10 border border-emerald-500/20 text-emerald-400">
+                  <span class="text-xl">🌍</span>
+                </div>
+                <div>
+                  <div class="flex items-center gap-2">
+                    <h3 class="text-sm font-bold text-white">Geo-Location IP Filtering</h3>
+                    <span id="geo-status-badge" class="px-2 py-0.5 rounded text-[10px] font-mono font-bold bg-emerald-500/20 text-emerald-300 border border-emerald-500/30">Auto-Update ON</span>
+                  </div>
+                  <p class="text-xs text-slate-400">Aggregated country CIDR databases loaded into Linux kernel <code class="text-emerald-300">ipset</code>. Usable anywhere in Filter, NAT, and Mangle rules.</p>
+                </div>
+              </div>
+
+              <!-- Controls -->
+              <div class="flex items-center flex-wrap gap-2">
+                <div class="flex items-center space-x-1.5 bg-slate-950/60 border border-slate-800 px-3 py-1.5 rounded-lg text-xs font-mono">
+                  <span class="text-slate-400">Schedule:</span>
+                  <span id="geo-interval-label" class="font-bold text-white">Every 24h</span>
+                </div>
+                <button onclick="openGeoIntervalModal()" class="text-xs font-semibold px-3 py-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 transition">
+                  ⚙️ Settings
+                </button>
+                <button id="geo-btn-update-now" onclick="triggerGeoUpdateNow()" class="text-xs font-semibold px-3.5 py-1.5 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white shadow-lg shadow-emerald-500/20 transition flex items-center gap-1.5">
+                  <span id="geo-update-spinner" class="hidden animate-spin text-xs">⏳</span>
+                  <span>🔄 Update Now</span>
+                </button>
+              </div>
+            </div>
+
+            <!-- Stats Bar -->
+            <div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
+              <div class="bg-slate-900/60 border border-slate-800/80 rounded-xl p-3">
+                <span class="text-[10px] uppercase font-bold text-slate-500 block">Active Countries</span>
+                <span id="geo-stat-active-countries" class="text-base font-bold font-mono text-emerald-400">0</span>
+              </div>
+              <div class="bg-slate-900/60 border border-slate-800/80 rounded-xl p-3">
+                <span class="text-[10px] uppercase font-bold text-slate-500 block">Total Subnets in Kernel</span>
+                <span id="geo-stat-total-subnets" class="text-base font-bold font-mono text-cyan-400">0</span>
+              </div>
+              <div class="bg-slate-900/60 border border-slate-800/80 rounded-xl p-3">
+                <span class="text-[10px] uppercase font-bold text-slate-500 block">Last Synced</span>
+                <span id="geo-stat-last-sync" class="text-xs font-mono text-slate-300">-</span>
+              </div>
+              <div class="bg-slate-900/60 border border-slate-800/80 rounded-xl p-3">
+                <span class="text-[10px] uppercase font-bold text-slate-500 block">Next Scheduled Run</span>
+                <span id="geo-stat-next-sync" class="text-xs font-mono text-slate-300">-</span>
+              </div>
+            </div>
+
+            <!-- Filters & Country Table -->
+            <div class="bg-slate-900/90 border border-slate-800 rounded-xl overflow-hidden">
+              <div class="p-4 border-b border-slate-800 flex items-center justify-between flex-wrap gap-3">
+                <div class="flex items-center flex-wrap gap-2">
+                  <input type="text" id="geo-search-input" oninput="renderGeoCountries()" placeholder="Search country name or code (e.g. Greece, GR, Germany)..." class="bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-emerald-500 w-64">
+
+                  <select id="geo-continent-filter" onchange="renderGeoCountries()" class="bg-slate-800 border border-slate-700 rounded-lg px-2.5 py-1.5 text-xs text-white font-mono">
+                    <option value="">All Continents</option>
+                    <option value="Europe">Europe</option>
+                    <option value="Asia">Asia</option>
+                    <option value="Americas">Americas</option>
+                    <option value="Africa">Africa</option>
+                    <option value="Oceania">Oceania</option>
+                  </select>
+
+                  <label class="flex items-center space-x-1.5 text-xs text-slate-300 cursor-pointer ml-2">
+                    <input type="checkbox" id="geo-filter-enabled-only" onchange="renderGeoCountries()" class="rounded bg-slate-800 border-slate-700 text-emerald-500">
+                    <span>Enabled in Firewall only</span>
+                  </label>
+                </div>
+
+                <div class="flex items-center space-x-2">
+                  <span class="text-xs text-slate-400 font-sans">Presets:</span>
+                  <button onclick="applyGeoPreset('GR')" class="px-2.5 py-1 text-xs rounded bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 font-mono">🇬🇷 GR</button>
+                  <button onclick="applyGeoPreset('EU')" class="px-2.5 py-1 text-xs rounded bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 font-mono">🇪🇺 EU</button>
+                  <button onclick="applyGeoPreset('BLOCKLIST')" class="px-2.5 py-1 text-xs rounded bg-rose-950/40 hover:bg-rose-900/50 text-rose-300 border border-rose-800/50 font-mono">🚫 High-Risk</button>
+                  <button onclick="applyGeoPreset('NONE')" class="px-2.5 py-1 text-xs rounded bg-slate-800 hover:bg-slate-700 text-slate-400 border border-slate-700 font-mono">Clear All</button>
+                </div>
+              </div>
+
+              <div class="overflow-x-auto max-h-[600px] overflow-y-auto">
+                <table class="w-full text-left text-xs font-mono">
+                  <thead class="text-slate-400 bg-slate-950/80 border-b border-slate-800 uppercase tracking-wider text-[10px] font-sans sticky top-0 z-10 backdrop-blur-sm">
+                    <tr>
+                      <th class="py-2.5 px-3 w-16 text-center">Code</th>
+                      <th class="py-2.5 px-3">Country Name</th>
+                      <th class="py-2.5 px-3">Continent</th>
+                      <th class="py-2.5 px-3">Subnets</th>
+                      <th class="py-2.5 px-3">Kernel IPSet</th>
+                      <th class="py-2.5 px-3">Firewall Status</th>
+                      <th class="py-2.5 px-3 text-right font-sans">Actions</th>
+                    </tr>
+                  </thead>
+                  <tbody id="geo-table-body" class="divide-y divide-slate-800/60"></tbody>
+                </table>
+              </div>
+
+              <div id="geo-empty" class="hidden p-10 text-center text-slate-500 text-xs font-sans">
+                No countries found matching your search.
+              </div>
+            </div>
+          </div>
         </section>
 
         <!-- ================= SECTION: ROUTES & POLICY (/ip route & rules) ================= -->
@@ -2534,6 +2644,29 @@
               <select id="nat-modal-outiface" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono"></select>
             </div>
           </div>
+
+          <div class="grid grid-cols-2 gap-3">
+            <div>
+              <div class="flex items-center justify-between mb-1">
+                <label class="font-semibold text-slate-300">Src. Address List</label>
+                <label class="flex items-center space-x-1 cursor-pointer text-[10px] text-amber-400 font-mono">
+                  <input type="checkbox" id="nat-modal-srclist-inv" class="rounded bg-slate-800 border-slate-700">
+                  <span>! Invert</span>
+                </label>
+              </div>
+              <select id="nat-modal-srclist" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono"></select>
+            </div>
+            <div>
+              <div class="flex items-center justify-between mb-1">
+                <label class="font-semibold text-slate-300">Dst. Address List</label>
+                <label class="flex items-center space-x-1 cursor-pointer text-[10px] text-amber-400 font-mono">
+                  <input type="checkbox" id="nat-modal-dstlist-inv" class="rounded bg-slate-800 border-slate-700">
+                  <span>! Invert</span>
+                </label>
+              </div>
+              <select id="nat-modal-dstlist" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono"></select>
+            </div>
+          </div>
         </div>
 
         <div>
@@ -2629,6 +2762,29 @@
               <select id="mangle-modal-iniface" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono"></select>
             </div>
           </div>
+
+          <div class="grid grid-cols-2 gap-3">
+            <div>
+              <div class="flex items-center justify-between mb-1">
+                <label class="font-semibold text-slate-300">Src. Address List</label>
+                <label class="flex items-center space-x-1 cursor-pointer text-[10px] text-amber-400 font-mono">
+                  <input type="checkbox" id="mangle-modal-srclist-inv" class="rounded bg-slate-800 border-slate-700">
+                  <span>! Invert</span>
+                </label>
+              </div>
+              <select id="mangle-modal-srclist" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono"></select>
+            </div>
+            <div>
+              <div class="flex items-center justify-between mb-1">
+                <label class="font-semibold text-slate-300">Dst. Address List</label>
+                <label class="flex items-center space-x-1 cursor-pointer text-[10px] text-amber-400 font-mono">
+                  <input type="checkbox" id="mangle-modal-dstlist-inv" class="rounded bg-slate-800 border-slate-700">
+                  <span>! Invert</span>
+                </label>
+              </div>
+              <select id="mangle-modal-dstlist" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono"></select>
+            </div>
+          </div>
         </div>
 
         <div>
@@ -2659,6 +2815,7 @@
         <button onclick="closeAddrListModal()" class="text-slate-400 hover:text-white p-1 rounded-lg">✕</button>
       </div>
       <form onsubmit="submitSaveAddrList(event)" class="p-6 space-y-4 text-xs">
+        <input type="hidden" id="addrlist-modal-id">
         <div>
           <label class="block font-semibold text-slate-300 mb-1.5">List Name *</label>
           <input type="text" id="addrlist-modal-name" placeholder="e.g. LAN_Subnets, Blocked_IPs" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
@@ -2671,14 +2828,82 @@
           <label class="block font-semibold text-slate-300 mb-1.5">Comment</label>
           <input type="text" id="addrlist-modal-comment" placeholder="Optional description" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white">
         </div>
+        <div class="pt-2">
+          <label class="flex items-center space-x-2 cursor-pointer">
+            <input type="checkbox" id="addrlist-modal-disabled" class="rounded bg-slate-800 border-slate-700 text-rose-500">
+            <span class="font-medium text-slate-300">Disabled (X)</span>
+          </label>
+        </div>
         <div class="pt-4 border-t border-slate-800 flex justify-end space-x-3">
           <button type="button" onclick="closeAddrListModal()" class="px-4 py-2 rounded-lg text-slate-400 hover:bg-slate-800">Cancel</button>
-          <button type="submit" class="px-5 py-2 rounded-lg font-semibold bg-emerald-600 hover:bg-emerald-500 text-white shadow-lg shadow-emerald-500/20">Add to List</button>
+          <button type="submit" id="addrlist-modal-submit-btn" class="px-5 py-2 rounded-lg font-semibold bg-emerald-600 hover:bg-emerald-500 text-white shadow-lg shadow-emerald-500/20">Save Entry</button>
         </div>
       </form>
     </div>
   </div>
 
+  <!-- ================= MODAL: GEO INTERVAL SETTINGS ================= -->
+  <div id="geo-interval-modal" class="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
+    <div class="bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-md shadow-2xl overflow-hidden animate-in">
+      <div class="px-6 py-4 border-b border-slate-800 flex items-center justify-between bg-slate-950/40">
+        <h3 class="text-sm font-bold text-white">Geo-Location Auto-Update Settings</h3>
+        <button onclick="closeGeoIntervalModal()" class="text-slate-400 hover:text-white p-1 rounded-lg">✕</button>
+      </div>
+      <form onsubmit="submitGeoIntervalSettings(event)" class="p-6 space-y-4 text-xs">
+        <div class="p-3 bg-slate-950/60 border border-slate-800 rounded-xl space-y-2">
+          <label class="flex items-center space-x-2.5 cursor-pointer">
+            <input type="checkbox" id="geo-cfg-autoupdate" checked class="rounded bg-slate-800 border-slate-700 text-emerald-500">
+            <span class="font-semibold text-white">Enable Automatic Background Updates</span>
+          </label>
+          <p class="text-[11px] text-slate-400 pl-6">Automatically fetches the latest country IP blocks from IPdeny registries and updates active kernel ipsets without service interruption.</p>
+        </div>
+
+        <div>
+          <label class="block font-semibold text-slate-300 mb-1.5">Update Interval *</label>
+          <select id="geo-cfg-interval" onchange="toggleCustomGeoInterval()" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+            <option value="12">Every 12 Hours</option>
+            <option value="24" selected>Every 24 Hours (Daily - Recommended)</option>
+            <option value="48">Every 48 Hours (2 Days)</option>
+            <option value="72">Every 3 Days</option>
+            <option value="168">Every 7 Days (Weekly)</option>
+            <option value="720">Every 30 Days (Monthly)</option>
+            <option value="custom">Custom Hours...</option>
+          </select>
+        </div>
+
+        <div id="geo-cfg-custom-box" class="hidden">
+          <label class="block font-semibold text-slate-300 mb-1.5">Custom Interval (Hours)</label>
+          <input type="number" id="geo-cfg-custom-hours" min="1" max="8760" value="24" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+        </div>
+
+        <div class="pt-4 border-t border-slate-800 flex justify-end space-x-3">
+          <button type="button" onclick="closeGeoIntervalModal()" class="px-4 py-2 rounded-lg text-slate-400 hover:bg-slate-800">Cancel</button>
+          <button type="submit" class="px-5 py-2 rounded-lg font-semibold bg-emerald-600 hover:bg-emerald-500 text-white shadow-lg shadow-emerald-500/20">Save Schedule</button>
+        </div>
+      </form>
+    </div>
+  </div>
+
+  <!-- ================= MODAL: GEO SUBNETS PREVIEW ================= -->
+  <div id="geo-subnets-modal" class="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
+    <div class="bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in">
+      <div class="px-6 py-4 border-b border-slate-800 flex items-center justify-between bg-slate-950/40">
+        <div>
+          <h3 id="geo-subnets-title" class="text-sm font-bold text-white">Country Subnets</h3>
+          <span id="geo-subnets-subtitle" class="text-[11px] font-mono text-slate-400">CIDR Blocks</span>
+        </div>
+        <button onclick="closeGeoSubnetsModal()" class="text-slate-400 hover:text-white p-1 rounded-lg">✕</button>
+      </div>
+      <div class="p-6 space-y-3 text-xs">
+        <p class="text-slate-400">Showing first 100 subnets in this country zone:</p>
+        <div id="geo-subnets-list" class="bg-slate-950 border border-slate-800 rounded-xl p-3 max-h-72 overflow-y-auto font-mono text-[11px] text-emerald-400 space-y-1"></div>
+        <div class="pt-3 border-t border-slate-800 flex justify-end">
+          <button type="button" onclick="closeGeoSubnetsModal()" class="px-4 py-2 rounded-lg text-slate-400 hover:bg-slate-800">Close</button>
+        </div>
+      </div>
+    </div>
+  </div>
+
   <!-- ================= MODAL: ADD / EDIT ROUTE ================= -->
   <div id="route-modal" class="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
     <div class="bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in">