geoip.go 16 KB

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