geoip.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694
  1. package firewall
  2. import (
  3. "bufio"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "net"
  9. "net/http"
  10. "os"
  11. "os/exec"
  12. "path/filepath"
  13. "sort"
  14. "strings"
  15. "sync"
  16. "time"
  17. )
  18. const (
  19. GeoIPDir = "/etc/debtik/firewall/geoip"
  20. GeoIPConfigFile = "/etc/debtik/firewall/geoip/config.json"
  21. GeoIPZonesDir = "/etc/debtik/firewall/geoip/zones"
  22. )
  23. // GeoIPConfig defines interval and enabled country persistence
  24. type GeoIPConfig struct {
  25. AutoUpdate bool `json:"auto_update"`
  26. IntervalHours int `json:"interval_hours"` // e.g. 24
  27. LastUpdated time.Time `json:"last_updated"`
  28. NextUpdate time.Time `json:"next_update"`
  29. EnabledCountries []string `json:"enabled_countries"`
  30. }
  31. // GeoIPManager handles country IP databases, ipset sync, and auto-update scheduling
  32. type GeoIPManager struct {
  33. mu sync.RWMutex
  34. config GeoIPConfig
  35. countries map[string]*GeoCountryInfo
  36. timer *time.Timer
  37. stopCh chan struct{}
  38. restoreDone chan struct{}
  39. isUpdating bool
  40. lastError string
  41. referenced func(string) bool
  42. syncMu sync.Mutex
  43. }
  44. func NewGeoIPManager() *GeoIPManager {
  45. _ = os.MkdirAll(GeoIPZonesDir, 0755)
  46. m := &GeoIPManager{
  47. countries: make(map[string]*GeoCountryInfo),
  48. stopCh: make(chan struct{}),
  49. restoreDone: make(chan struct{}),
  50. }
  51. // 1. Initialize country metadata
  52. for _, c := range StandardISOCountries {
  53. info := c
  54. info.IPSetName = "mk_geo_" + c.Code
  55. m.countries[c.Code] = &info
  56. }
  57. // 2. Load Config
  58. m.loadConfig()
  59. // 3. Scan cached zone files
  60. m.scanCachedZones()
  61. // Restore cached enabled countries without hiding a failed restore.
  62. go func() {
  63. defer close(m.restoreDone)
  64. m.mu.RLock()
  65. var enabled []string
  66. for code, c := range m.countries {
  67. if c.Enabled {
  68. enabled = append(enabled, code)
  69. }
  70. }
  71. m.mu.RUnlock()
  72. var failures []error
  73. for _, code := range enabled {
  74. zone := filepath.Join(GeoIPZonesDir, strings.ToLower(code)+".zone")
  75. if _, err := os.Stat(zone); os.IsNotExist(err) {
  76. if _, err = m.SyncCountryZone(code); err != nil {
  77. failures = append(failures, fmt.Errorf("%s: %w", code, err))
  78. }
  79. } else if err := m.ApplyCountryIPSet(code); err != nil {
  80. failures = append(failures, fmt.Errorf("%s: %w", code, err))
  81. }
  82. }
  83. if err := errors.Join(failures...); err != nil {
  84. m.mu.Lock()
  85. m.lastError = err.Error()
  86. m.mu.Unlock()
  87. fmt.Fprintf(os.Stderr, "GeoIP restore: %v\n", err)
  88. }
  89. }()
  90. // 5. Start background auto-update scheduler
  91. m.startScheduler()
  92. return m
  93. }
  94. func (m *GeoIPManager) loadConfig() {
  95. m.config = GeoIPConfig{
  96. AutoUpdate: true,
  97. IntervalHours: 24,
  98. EnabledCountries: []string{"GR"},
  99. }
  100. if data, err := os.ReadFile(GeoIPConfigFile); err == nil {
  101. if err := json.Unmarshal(data, &m.config); err != nil {
  102. m.lastError = "Invalid GeoIP configuration: " + err.Error()
  103. m.config.AutoUpdate = false
  104. m.config.EnabledCountries = nil
  105. }
  106. }
  107. if m.config.IntervalHours <= 0 {
  108. m.config.IntervalHours = 24
  109. }
  110. // Mark enabled countries
  111. for _, code := range m.config.EnabledCountries {
  112. if c, ok := m.countries[strings.ToUpper(code)]; ok {
  113. c.Enabled = true
  114. }
  115. }
  116. }
  117. func (m *GeoIPManager) saveConfig() error {
  118. if err := os.MkdirAll(GeoIPDir, 0755); err != nil {
  119. return err
  120. }
  121. m.config.EnabledCountries = make([]string, 0)
  122. for code, c := range m.countries {
  123. if c.Enabled {
  124. m.config.EnabledCountries = append(m.config.EnabledCountries, code)
  125. }
  126. }
  127. sort.Strings(m.config.EnabledCountries)
  128. data, err := json.MarshalIndent(m.config, "", " ")
  129. if err != nil {
  130. return err
  131. }
  132. return writeAtomic(GeoIPConfigFile, data)
  133. }
  134. func (m *GeoIPManager) scanCachedZones() {
  135. for code, c := range m.countries {
  136. zoneFile := filepath.Join(GeoIPZonesDir, strings.ToLower(code)+".zone")
  137. if stat, err := os.Stat(zoneFile); err == nil && stat.Size() > 0 {
  138. c.LastUpdated = stat.ModTime()
  139. if count, err := countLines(zoneFile); err == nil {
  140. c.SubnetCount = count
  141. }
  142. }
  143. }
  144. }
  145. func countLines(path string) (int, error) {
  146. f, err := os.Open(path)
  147. if err != nil {
  148. return 0, err
  149. }
  150. defer f.Close()
  151. count := 0
  152. scanner := bufio.NewScanner(f)
  153. for scanner.Scan() {
  154. line := strings.TrimSpace(scanner.Text())
  155. if line != "" && !strings.HasPrefix(line, "#") {
  156. count++
  157. }
  158. }
  159. return count, scanner.Err()
  160. }
  161. func (m *GeoIPManager) startScheduler() {
  162. m.mu.Lock()
  163. defer m.mu.Unlock()
  164. if m.timer != nil {
  165. m.timer.Stop()
  166. }
  167. if !m.config.AutoUpdate {
  168. return
  169. }
  170. interval := time.Duration(m.config.IntervalHours) * time.Hour
  171. now := time.Now()
  172. // Calculate delay until next run
  173. var delay time.Duration
  174. if m.config.LastUpdated.IsZero() {
  175. delay = 1 * time.Minute
  176. } else {
  177. next := m.config.LastUpdated.Add(interval)
  178. if next.After(now) {
  179. delay = next.Sub(now)
  180. } else {
  181. delay = 10 * time.Second
  182. }
  183. }
  184. m.config.NextUpdate = now.Add(delay)
  185. m.timer = time.AfterFunc(delay, func() {
  186. _ = m.UpdateNow()
  187. // Re-arm for next full interval
  188. m.startScheduler()
  189. })
  190. }
  191. // ListCountries returns all countries sorted by Name with Greece & Cyprus first
  192. func (m *GeoIPManager) ListCountries() []GeoCountryInfo {
  193. m.mu.RLock()
  194. defer m.mu.RUnlock()
  195. res := make([]GeoCountryInfo, 0, len(m.countries))
  196. for _, c := range m.countries {
  197. res = append(res, *c)
  198. }
  199. sort.Slice(res, func(i, j int) bool {
  200. // Put enabled countries at the top, then alphabetically
  201. if res[i].Enabled != res[j].Enabled {
  202. return res[i].Enabled
  203. }
  204. if res[i].Code == "GR" {
  205. return true
  206. }
  207. if res[j].Code == "GR" {
  208. return false
  209. }
  210. return res[i].Name < res[j].Name
  211. })
  212. return res
  213. }
  214. func (m *GeoIPManager) GetStatus() map[string]any {
  215. m.mu.RLock()
  216. defer m.mu.RUnlock()
  217. enabledCount := 0
  218. totalSubnets := 0
  219. for _, c := range m.countries {
  220. if c.Enabled {
  221. enabledCount++
  222. totalSubnets += c.SubnetCount
  223. }
  224. }
  225. restoring := false
  226. if m.restoreDone != nil {
  227. select {
  228. case <-m.restoreDone:
  229. default:
  230. restoring = true
  231. }
  232. }
  233. return map[string]any{
  234. "auto_update": m.config.AutoUpdate,
  235. "interval_hours": m.config.IntervalHours,
  236. "last_updated": m.config.LastUpdated,
  237. "next_update": m.config.NextUpdate,
  238. "enabled_countries": m.config.EnabledCountries,
  239. "enabled_count": enabledCount,
  240. "total_subnets": totalSubnets,
  241. "is_updating": m.isUpdating,
  242. "is_restoring": restoring,
  243. "last_error": m.lastError,
  244. }
  245. }
  246. func (m *GeoIPManager) SetConfig(auto bool, hours int) error {
  247. m.mu.Lock()
  248. old := m.config
  249. if hours < 1 || hours > 8760 {
  250. m.mu.Unlock()
  251. return fmt.Errorf("interval must be 1–8760 hours")
  252. }
  253. m.config.AutoUpdate = auto
  254. m.config.IntervalHours = hours
  255. if err := m.saveConfig(); err != nil {
  256. m.config = old
  257. m.mu.Unlock()
  258. return err
  259. }
  260. m.mu.Unlock()
  261. m.startScheduler()
  262. return nil
  263. }
  264. func (m *GeoIPManager) ToggleCountry(code string, enable bool) error {
  265. code = strings.ToUpper(code)
  266. m.mu.RLock()
  267. _, ok := m.countries[code]
  268. m.mu.RUnlock()
  269. if !ok {
  270. return fmt.Errorf("unknown country %q", code)
  271. }
  272. if !enable && m.referenced != nil && m.referenced(code) {
  273. return fmt.Errorf("country %s is used by an active firewall rule; change that rule first", code)
  274. }
  275. if enable {
  276. if _, err := os.Stat(filepath.Join(GeoIPZonesDir, strings.ToLower(code)+".zone")); os.IsNotExist(err) {
  277. if _, err = m.SyncCountryZone(code); err != nil {
  278. return err
  279. }
  280. }
  281. if err := m.ApplyCountryIPSet(code); err != nil {
  282. return err
  283. }
  284. } else {
  285. name := GetGeoIPSetName(code)
  286. if output, err := exec.Command("ipset", "list", name, "-terse").CombinedOutput(); err == nil {
  287. if err := runIPSet("destroy", name); err != nil {
  288. return fmt.Errorf("GeoIP set is referenced; it was preserved: %w", err)
  289. }
  290. } else if !strings.Contains(string(output), "does not exist") {
  291. return fmt.Errorf("cannot inspect GeoIP set: %s: %w", output, err)
  292. }
  293. }
  294. m.mu.Lock()
  295. c := m.countries[code]
  296. old := c.Enabled
  297. oldConfig := m.config
  298. c.Enabled = enable
  299. if err := m.saveConfig(); err != nil {
  300. c.Enabled = old
  301. m.config = oldConfig
  302. m.mu.Unlock()
  303. if !enable && old {
  304. if restoreErr := m.ApplyCountryIPSet(code); restoreErr != nil {
  305. return fmt.Errorf("persist: %v; restore set: %w", err, restoreErr)
  306. }
  307. }
  308. return err
  309. }
  310. m.mu.Unlock()
  311. return nil
  312. }
  313. func (m *GeoIPManager) BatchToggle(preset string, enable bool, customCodes []string) error {
  314. var targetCodes []string
  315. switch strings.ToUpper(preset) {
  316. case "EU":
  317. 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"}
  318. case "BALKANS":
  319. targetCodes = []string{"GR", "CY", "AL", "BG", "RO", "RS", "MK", "ME", "BA", "HR", "SI", "TR"}
  320. case "BLOCKLIST", "HIGHRISK":
  321. targetCodes = []string{"CN", "RU", "IR", "KP", "BY"}
  322. case "ALL":
  323. m.mu.RLock()
  324. for code := range m.countries {
  325. targetCodes = append(targetCodes, code)
  326. }
  327. m.mu.RUnlock()
  328. case "NONE":
  329. m.mu.RLock()
  330. for code, c := range m.countries {
  331. if c.Enabled {
  332. targetCodes = append(targetCodes, code)
  333. }
  334. }
  335. m.mu.RUnlock()
  336. enable = false
  337. default:
  338. targetCodes = customCodes
  339. }
  340. if len(targetCodes) == 0 && strings.ToUpper(preset) != "NONE" {
  341. return fmt.Errorf("no countries selected or unknown preset")
  342. }
  343. for _, code := range targetCodes {
  344. m.mu.RLock()
  345. _, ok := m.countries[strings.ToUpper(code)]
  346. m.mu.RUnlock()
  347. if !ok {
  348. return fmt.Errorf("unknown country %q", code)
  349. }
  350. if !enable && m.referenced != nil && m.referenced(strings.ToUpper(code)) {
  351. return fmt.Errorf("country %s is used by an active firewall rule", code)
  352. }
  353. }
  354. var failures []error
  355. for _, code := range targetCodes {
  356. if err := m.ToggleCountry(code, enable); err != nil {
  357. failures = append(failures, fmt.Errorf("%s: %w", code, err))
  358. }
  359. }
  360. return errors.Join(failures...)
  361. }
  362. // SyncCountryZone downloads the country zone file from ipdeny and refreshes ipset
  363. func (m *GeoIPManager) SyncCountryZone(code string) (int, error) {
  364. m.syncMu.Lock()
  365. defer m.syncMu.Unlock()
  366. m.mu.RLock()
  367. _, valid := m.countries[strings.ToUpper(code)]
  368. m.mu.RUnlock()
  369. if !valid {
  370. return 0, fmt.Errorf("unknown country %q", code)
  371. }
  372. code = strings.ToUpper(code)
  373. url := fmt.Sprintf("https://www.ipdeny.com/ipblocks/data/countries/%s.zone", strings.ToLower(code))
  374. client := &http.Client{Timeout: 15 * time.Second}
  375. req, err := http.NewRequest("GET", url, nil)
  376. if err != nil {
  377. return 0, err
  378. }
  379. req.Header.Set("User-Agent", "DebTik/1.0")
  380. resp, err := client.Do(req)
  381. if err != nil {
  382. return 0, fmt.Errorf("failed to fetch zone for %s: %w", code, err)
  383. }
  384. defer resp.Body.Close()
  385. if resp.StatusCode != http.StatusOK {
  386. return 0, fmt.Errorf("ipdeny returned HTTP %d for %s", resp.StatusCode, code)
  387. }
  388. zoneFile := filepath.Join(GeoIPZonesDir, strings.ToLower(code)+".zone")
  389. out, err := os.CreateTemp(GeoIPZonesDir, ".zone-*")
  390. tmpFile := ""
  391. if out != nil {
  392. tmpFile = out.Name()
  393. }
  394. defer os.Remove(tmpFile)
  395. if err != nil {
  396. return 0, err
  397. }
  398. if _, err := io.Copy(out, io.LimitReader(resp.Body, 32*1024*1024+1)); err != nil {
  399. out.Close()
  400. _ = os.Remove(tmpFile)
  401. return 0, err
  402. }
  403. if err := out.Close(); err != nil {
  404. return 0, err
  405. }
  406. data, err := os.ReadFile(tmpFile)
  407. if err != nil {
  408. return 0, err
  409. }
  410. if len(data) > 32*1024*1024 {
  411. return 0, fmt.Errorf("GeoIP download too large")
  412. }
  413. countValid := 0
  414. for _, line := range strings.Split(string(data), "\n") {
  415. line = strings.TrimSpace(line)
  416. if line == "" || strings.HasPrefix(line, "#") {
  417. continue
  418. }
  419. ip, _, e := net.ParseCIDR(line)
  420. if e != nil || ip.To4() == nil {
  421. return 0, fmt.Errorf("invalid IPv4 CIDR in country download")
  422. }
  423. countValid++
  424. }
  425. if countValid == 0 {
  426. return 0, fmt.Errorf("empty country download")
  427. }
  428. if err := os.Rename(tmpFile, zoneFile); err != nil {
  429. return 0, err
  430. }
  431. count, _ := countLines(zoneFile)
  432. m.mu.Lock()
  433. if c, ok := m.countries[code]; ok {
  434. c.SubnetCount = count
  435. c.LastUpdated = time.Now()
  436. }
  437. m.mu.Unlock()
  438. // If enabled, load into kernel ipset immediately
  439. m.mu.RLock()
  440. enabled := m.countries[code] != nil && m.countries[code].Enabled
  441. m.mu.RUnlock()
  442. if enabled {
  443. if err := m.ApplyCountryIPSet(code); err != nil {
  444. return 0, err
  445. }
  446. }
  447. return count, nil
  448. }
  449. // ApplyCountryIPSet uses high-speed `ipset restore` to load the country's subnets
  450. func (m *GeoIPManager) ApplyCountryIPSet(code string) error {
  451. m.mu.RLock()
  452. _, valid := m.countries[strings.ToUpper(code)]
  453. m.mu.RUnlock()
  454. if !valid {
  455. return fmt.Errorf("unknown country %q", code)
  456. }
  457. code = strings.ToUpper(code)
  458. zoneFile := filepath.Join(GeoIPZonesDir, strings.ToLower(code)+".zone")
  459. f, err := os.Open(zoneFile)
  460. if err != nil {
  461. return err
  462. }
  463. defer f.Close()
  464. setName := "mk_geo_" + code
  465. var addresses []string
  466. scanner := bufio.NewScanner(f)
  467. for scanner.Scan() {
  468. line := strings.TrimSpace(scanner.Text())
  469. if line != "" && !strings.HasPrefix(line, "#") {
  470. addresses = append(addresses, line)
  471. }
  472. }
  473. if err := scanner.Err(); err != nil {
  474. return err
  475. }
  476. return replaceSet(setName, "inet", addresses)
  477. }
  478. // ApplyAllEnabledSets ensures all enabled countries are present in kernel ipset
  479. func (m *GeoIPManager) ApplyAllEnabledSets() error {
  480. m.mu.RLock()
  481. var enabled []string
  482. for code, c := range m.countries {
  483. if c.Enabled {
  484. enabled = append(enabled, code)
  485. }
  486. }
  487. m.mu.RUnlock()
  488. for _, code := range enabled {
  489. if err := m.ApplyCountryIPSet(code); err != nil {
  490. return err
  491. }
  492. }
  493. return nil
  494. }
  495. // UpdateNow updates zone files for all currently enabled countries
  496. func (m *GeoIPManager) UpdateNow() error {
  497. m.mu.Lock()
  498. if m.isUpdating {
  499. m.mu.Unlock()
  500. return fmt.Errorf("update already in progress")
  501. }
  502. m.isUpdating = true
  503. m.lastError = ""
  504. var enabled []string
  505. for code, c := range m.countries {
  506. if c.Enabled {
  507. enabled = append(enabled, code)
  508. }
  509. }
  510. m.mu.Unlock()
  511. go func() {
  512. var failures []error
  513. for _, code := range enabled {
  514. if _, err := m.SyncCountryZone(code); err != nil {
  515. failures = append(failures, fmt.Errorf("%s: %w", code, err))
  516. }
  517. }
  518. m.mu.Lock()
  519. defer m.mu.Unlock()
  520. m.isUpdating = false
  521. m.config.NextUpdate = time.Now().Add(time.Duration(m.config.IntervalHours) * time.Hour)
  522. if len(failures) == 0 {
  523. m.config.LastUpdated = time.Now()
  524. }
  525. if err := m.saveConfig(); err != nil {
  526. failures = append(failures, err)
  527. }
  528. if err := errors.Join(failures...); err != nil {
  529. m.lastError = err.Error()
  530. }
  531. }()
  532. return nil
  533. }
  534. // GetCountrySubnets returns first N subnets from cached zone file for preview
  535. func (m *GeoIPManager) GetCountrySubnets(code string, limit int) []string {
  536. m.mu.RLock()
  537. _, valid := m.countries[strings.ToUpper(code)]
  538. m.mu.RUnlock()
  539. if !valid {
  540. return nil
  541. }
  542. code = strings.ToUpper(code)
  543. zoneFile := filepath.Join(GeoIPZonesDir, strings.ToLower(code)+".zone")
  544. f, err := os.Open(zoneFile)
  545. if err != nil {
  546. return nil
  547. }
  548. defer f.Close()
  549. if limit <= 0 {
  550. limit = 100
  551. }
  552. var res []string
  553. scanner := bufio.NewScanner(f)
  554. for scanner.Scan() {
  555. line := strings.TrimSpace(scanner.Text())
  556. if line != "" && !strings.HasPrefix(line, "#") {
  557. res = append(res, line)
  558. if len(res) >= limit {
  559. break
  560. }
  561. }
  562. }
  563. return res
  564. }
  565. // EnsureCountryOrPreset guarantees that the ipset mk_geo_<CODE> exists and has subnets loaded
  566. func (m *GeoIPManager) EnsureCountryOrPreset(code string) error {
  567. code = strings.TrimPrefix(code, "!")
  568. code = strings.TrimPrefix(code, "geo:")
  569. code = strings.TrimPrefix(code, "geo_")
  570. code = strings.ToUpper(strings.TrimSpace(code))
  571. if code == "" {
  572. return nil
  573. }
  574. // Check if it is a preset
  575. var targetCodes []string
  576. switch code {
  577. case "EU":
  578. 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"}
  579. case "BALKANS":
  580. targetCodes = []string{"GR", "CY", "AL", "BG", "RO", "RS", "MK", "ME", "BA", "HR", "SI", "TR"}
  581. case "BLOCKLIST", "HIGHRISK":
  582. targetCodes = []string{"CN", "RU", "IR", "KP", "BY"}
  583. }
  584. if len(targetCodes) > 0 {
  585. setName := "mk_geo_" + code
  586. var addresses []string
  587. for _, country := range targetCodes {
  588. zoneFile := filepath.Join(GeoIPZonesDir, strings.ToLower(country)+".zone")
  589. if _, err := os.Stat(zoneFile); os.IsNotExist(err) {
  590. if _, err = m.SyncCountryZone(country); err != nil {
  591. return err
  592. }
  593. }
  594. data, err := os.ReadFile(zoneFile)
  595. if err != nil {
  596. return err
  597. }
  598. for _, line := range strings.Split(string(data), "\n") {
  599. line = strings.TrimSpace(line)
  600. if line != "" && !strings.HasPrefix(line, "#") {
  601. addresses = append(addresses, line)
  602. }
  603. }
  604. }
  605. return replaceSet(setName, "inet", addresses)
  606. }
  607. // Single country
  608. zoneFile := filepath.Join(GeoIPZonesDir, strings.ToLower(code)+".zone")
  609. if _, err := os.Stat(zoneFile); os.IsNotExist(err) {
  610. if _, err := m.SyncCountryZone(code); err != nil {
  611. return err
  612. }
  613. }
  614. m.mu.Lock()
  615. if c, ok := m.countries[code]; ok {
  616. c.Enabled = true
  617. if err := m.saveConfig(); err != nil {
  618. m.mu.Unlock()
  619. return err
  620. }
  621. }
  622. m.mu.Unlock()
  623. return m.ApplyCountryIPSet(code)
  624. }