geoip.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
  1. package firewall
  2. import (
  3. "bufio"
  4. "encoding/json"
  5. "fmt"
  6. "io"
  7. "net/http"
  8. "os"
  9. "os/exec"
  10. "path/filepath"
  11. "sort"
  12. "strings"
  13. "sync"
  14. "time"
  15. )
  16. const (
  17. GeoIPDir = "/etc/proxmox-router/firewall/geoip"
  18. GeoIPConfigFile = "/etc/proxmox-router/firewall/geoip/config.json"
  19. GeoIPZonesDir = "/etc/proxmox-router/firewall/geoip/zones"
  20. )
  21. // GeoIPConfig defines interval and enabled country persistence
  22. type GeoIPConfig struct {
  23. AutoUpdate bool `json:"auto_update"`
  24. IntervalHours int `json:"interval_hours"` // e.g. 24
  25. LastUpdated time.Time `json:"last_updated"`
  26. NextUpdate time.Time `json:"next_update"`
  27. EnabledCountries []string `json:"enabled_countries"`
  28. }
  29. // GeoIPManager handles country IP databases, ipset sync, and auto-update scheduling
  30. type GeoIPManager struct {
  31. mu sync.RWMutex
  32. config GeoIPConfig
  33. countries map[string]*GeoCountryInfo
  34. timer *time.Timer
  35. stopCh chan struct{}
  36. isUpdating bool
  37. }
  38. func NewGeoIPManager() *GeoIPManager {
  39. _ = os.MkdirAll(GeoIPZonesDir, 0755)
  40. m := &GeoIPManager{
  41. countries: make(map[string]*GeoCountryInfo),
  42. stopCh: make(chan struct{}),
  43. }
  44. // 1. Initialize country metadata
  45. for _, c := range StandardISOCountries {
  46. info := c
  47. info.IPSetName = "mk_geo_" + c.Code
  48. m.countries[c.Code] = &info
  49. }
  50. // 2. Load Config
  51. m.loadConfig()
  52. // 3. Scan cached zone files
  53. m.scanCachedZones()
  54. // 4. Pre-download / sync initial enabled countries if missing
  55. go func() {
  56. m.mu.RLock()
  57. enabled := make([]string, len(m.config.EnabledCountries))
  58. copy(enabled, m.config.EnabledCountries)
  59. m.mu.RUnlock()
  60. for _, code := range enabled {
  61. zonePath := filepath.Join(GeoIPZonesDir, strings.ToLower(code)+".zone")
  62. if _, err := os.Stat(zonePath); os.IsNotExist(err) {
  63. _, _ = m.SyncCountryZone(code)
  64. } else {
  65. _ = m.ApplyCountryIPSet(code)
  66. }
  67. }
  68. }()
  69. // 5. Start background auto-update scheduler
  70. m.startScheduler()
  71. return m
  72. }
  73. func (m *GeoIPManager) loadConfig() {
  74. m.config = GeoIPConfig{
  75. AutoUpdate: true,
  76. IntervalHours: 24,
  77. EnabledCountries: []string{"GR"},
  78. }
  79. if data, err := os.ReadFile(GeoIPConfigFile); err == nil {
  80. _ = json.Unmarshal(data, &m.config)
  81. }
  82. if m.config.IntervalHours <= 0 {
  83. m.config.IntervalHours = 24
  84. }
  85. // Mark enabled countries
  86. for _, code := range m.config.EnabledCountries {
  87. if c, ok := m.countries[strings.ToUpper(code)]; ok {
  88. c.Enabled = true
  89. }
  90. }
  91. }
  92. func (m *GeoIPManager) saveConfig() error {
  93. _ = os.MkdirAll(GeoIPDir, 0755)
  94. m.config.EnabledCountries = make([]string, 0)
  95. for code, c := range m.countries {
  96. if c.Enabled {
  97. m.config.EnabledCountries = append(m.config.EnabledCountries, code)
  98. }
  99. }
  100. sort.Strings(m.config.EnabledCountries)
  101. data, err := json.MarshalIndent(m.config, "", " ")
  102. if err != nil {
  103. return err
  104. }
  105. return os.WriteFile(GeoIPConfigFile, data, 0644)
  106. }
  107. func (m *GeoIPManager) scanCachedZones() {
  108. for code, c := range m.countries {
  109. zoneFile := filepath.Join(GeoIPZonesDir, strings.ToLower(code)+".zone")
  110. if stat, err := os.Stat(zoneFile); err == nil && stat.Size() > 0 {
  111. c.LastUpdated = stat.ModTime()
  112. if count, err := countLines(zoneFile); err == nil {
  113. c.SubnetCount = count
  114. }
  115. }
  116. }
  117. }
  118. func countLines(path string) (int, error) {
  119. f, err := os.Open(path)
  120. if err != nil {
  121. return 0, err
  122. }
  123. defer f.Close()
  124. count := 0
  125. scanner := bufio.NewScanner(f)
  126. for scanner.Scan() {
  127. line := strings.TrimSpace(scanner.Text())
  128. if line != "" && !strings.HasPrefix(line, "#") {
  129. count++
  130. }
  131. }
  132. return count, scanner.Err()
  133. }
  134. func (m *GeoIPManager) startScheduler() {
  135. m.mu.Lock()
  136. defer m.mu.Unlock()
  137. if m.timer != nil {
  138. m.timer.Stop()
  139. }
  140. if !m.config.AutoUpdate {
  141. return
  142. }
  143. interval := time.Duration(m.config.IntervalHours) * time.Hour
  144. now := time.Now()
  145. // Calculate delay until next run
  146. var delay time.Duration
  147. if m.config.LastUpdated.IsZero() {
  148. delay = 1 * time.Minute
  149. } else {
  150. next := m.config.LastUpdated.Add(interval)
  151. if next.After(now) {
  152. delay = next.Sub(now)
  153. } else {
  154. delay = 10 * time.Second
  155. }
  156. }
  157. m.config.NextUpdate = now.Add(delay)
  158. m.timer = time.AfterFunc(delay, func() {
  159. _ = m.UpdateNow()
  160. // Re-arm for next full interval
  161. m.startScheduler()
  162. })
  163. }
  164. // ListCountries returns all countries sorted by Name with Greece & Cyprus first
  165. func (m *GeoIPManager) ListCountries() []GeoCountryInfo {
  166. m.mu.RLock()
  167. defer m.mu.RUnlock()
  168. res := make([]GeoCountryInfo, 0, len(m.countries))
  169. for _, c := range m.countries {
  170. res = append(res, *c)
  171. }
  172. sort.Slice(res, func(i, j int) bool {
  173. // Put enabled countries at the top, then alphabetically
  174. if res[i].Enabled != res[j].Enabled {
  175. return res[i].Enabled
  176. }
  177. if res[i].Code == "GR" {
  178. return true
  179. }
  180. if res[j].Code == "GR" {
  181. return false
  182. }
  183. return res[i].Name < res[j].Name
  184. })
  185. return res
  186. }
  187. func (m *GeoIPManager) GetStatus() map[string]any {
  188. m.mu.RLock()
  189. defer m.mu.RUnlock()
  190. enabledCount := 0
  191. totalSubnets := 0
  192. for _, c := range m.countries {
  193. if c.Enabled {
  194. enabledCount++
  195. totalSubnets += c.SubnetCount
  196. }
  197. }
  198. return map[string]any{
  199. "auto_update": m.config.AutoUpdate,
  200. "interval_hours": m.config.IntervalHours,
  201. "last_updated": m.config.LastUpdated,
  202. "next_update": m.config.NextUpdate,
  203. "enabled_countries": m.config.EnabledCountries,
  204. "enabled_count": enabledCount,
  205. "total_subnets": totalSubnets,
  206. "is_updating": m.isUpdating,
  207. }
  208. }
  209. func (m *GeoIPManager) SetConfig(autoUpdate bool, intervalHours int) error {
  210. m.mu.Lock()
  211. if intervalHours <= 0 {
  212. intervalHours = 24
  213. }
  214. m.config.AutoUpdate = autoUpdate
  215. m.config.IntervalHours = intervalHours
  216. _ = m.saveConfig()
  217. m.mu.Unlock()
  218. m.startScheduler()
  219. return nil
  220. }
  221. func (m *GeoIPManager) ToggleCountry(code string, enable bool) error {
  222. code = strings.ToUpper(code)
  223. m.mu.Lock()
  224. c, ok := m.countries[code]
  225. if !ok {
  226. m.mu.Unlock()
  227. return fmt.Errorf("country %q not found", code)
  228. }
  229. c.Enabled = enable
  230. _ = m.saveConfig()
  231. m.mu.Unlock()
  232. if enable {
  233. zoneFile := filepath.Join(GeoIPZonesDir, strings.ToLower(code)+".zone")
  234. if _, err := os.Stat(zoneFile); os.IsNotExist(err) {
  235. _, err = m.SyncCountryZone(code)
  236. return err
  237. }
  238. return m.ApplyCountryIPSet(code)
  239. }
  240. // Disable -> destroy ipset
  241. setName := "mk_geo_" + code
  242. _ = exec.Command("ipset", "flush", setName).Run()
  243. _ = exec.Command("ipset", "destroy", setName).Run()
  244. return nil
  245. }
  246. func (m *GeoIPManager) BatchToggle(preset string, enable bool, customCodes []string) error {
  247. var targetCodes []string
  248. switch strings.ToUpper(preset) {
  249. case "EU":
  250. 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"}
  251. case "BALKANS":
  252. targetCodes = []string{"GR", "CY", "AL", "BG", "RO", "RS", "MK", "ME", "BA", "HR", "SI", "TR"}
  253. case "BLOCKLIST", "HIGHRISK":
  254. targetCodes = []string{"CN", "RU", "IR", "KP", "BY"}
  255. case "ALL":
  256. m.mu.RLock()
  257. for code := range m.countries {
  258. targetCodes = append(targetCodes, code)
  259. }
  260. m.mu.RUnlock()
  261. case "NONE":
  262. m.mu.RLock()
  263. for code, c := range m.countries {
  264. if c.Enabled {
  265. targetCodes = append(targetCodes, code)
  266. }
  267. }
  268. m.mu.RUnlock()
  269. enable = false
  270. default:
  271. targetCodes = customCodes
  272. }
  273. for _, code := range targetCodes {
  274. _ = m.ToggleCountry(code, enable)
  275. }
  276. return nil
  277. }
  278. // SyncCountryZone downloads the country zone file from ipdeny and refreshes ipset
  279. func (m *GeoIPManager) SyncCountryZone(code string) (int, error) {
  280. code = strings.ToUpper(code)
  281. url := fmt.Sprintf("https://www.ipdeny.com/ipblocks/data/countries/%s.zone", strings.ToLower(code))
  282. client := &http.Client{Timeout: 15 * time.Second}
  283. req, err := http.NewRequest("GET", url, nil)
  284. if err != nil {
  285. return 0, err
  286. }
  287. req.Header.Set("User-Agent", "Proxmox-Router/1.0")
  288. resp, err := client.Do(req)
  289. if err != nil {
  290. return 0, fmt.Errorf("failed to fetch zone for %s: %w", code, err)
  291. }
  292. defer resp.Body.Close()
  293. if resp.StatusCode != http.StatusOK {
  294. return 0, fmt.Errorf("ipdeny returned HTTP %d for %s", resp.StatusCode, code)
  295. }
  296. zoneFile := filepath.Join(GeoIPZonesDir, strings.ToLower(code)+".zone")
  297. tmpFile := zoneFile + ".tmp"
  298. out, err := os.Create(tmpFile)
  299. if err != nil {
  300. return 0, err
  301. }
  302. if _, err := io.Copy(out, resp.Body); err != nil {
  303. out.Close()
  304. _ = os.Remove(tmpFile)
  305. return 0, err
  306. }
  307. out.Close()
  308. _ = os.Rename(tmpFile, zoneFile)
  309. count, _ := countLines(zoneFile)
  310. m.mu.Lock()
  311. if c, ok := m.countries[code]; ok {
  312. c.SubnetCount = count
  313. c.LastUpdated = time.Now()
  314. }
  315. m.mu.Unlock()
  316. // If enabled, load into kernel ipset immediately
  317. m.mu.RLock()
  318. enabled := m.countries[code] != nil && m.countries[code].Enabled
  319. m.mu.RUnlock()
  320. if enabled {
  321. _ = m.ApplyCountryIPSet(code)
  322. }
  323. return count, nil
  324. }
  325. // ApplyCountryIPSet uses high-speed `ipset restore` to load the country's subnets
  326. func (m *GeoIPManager) ApplyCountryIPSet(code string) error {
  327. code = strings.ToUpper(code)
  328. zoneFile := filepath.Join(GeoIPZonesDir, strings.ToLower(code)+".zone")
  329. f, err := os.Open(zoneFile)
  330. if err != nil {
  331. return err
  332. }
  333. defer f.Close()
  334. setName := "mk_geo_" + code
  335. var sb strings.Builder
  336. sb.WriteString(fmt.Sprintf("create %s hash:net maxelem 262144 -exist\n", setName))
  337. sb.WriteString(fmt.Sprintf("flush %s\n", setName))
  338. scanner := bufio.NewScanner(f)
  339. for scanner.Scan() {
  340. line := strings.TrimSpace(scanner.Text())
  341. if line != "" && !strings.HasPrefix(line, "#") {
  342. sb.WriteString(fmt.Sprintf("add %s %s -exist\n", setName, line))
  343. }
  344. }
  345. cmd := exec.Command("ipset", "restore")
  346. cmd.Stdin = strings.NewReader(sb.String())
  347. out, err := cmd.CombinedOutput()
  348. if err != nil {
  349. return fmt.Errorf("ipset restore failed for %s: %s: %w", code, string(out), err)
  350. }
  351. return nil
  352. }
  353. // ApplyAllEnabledSets ensures all enabled countries are present in kernel ipset
  354. func (m *GeoIPManager) ApplyAllEnabledSets() error {
  355. m.mu.RLock()
  356. var enabled []string
  357. for code, c := range m.countries {
  358. if c.Enabled {
  359. enabled = append(enabled, code)
  360. }
  361. }
  362. m.mu.RUnlock()
  363. for _, code := range enabled {
  364. _ = m.ApplyCountryIPSet(code)
  365. }
  366. return nil
  367. }
  368. // UpdateNow updates zone files for all currently enabled countries
  369. func (m *GeoIPManager) UpdateNow() error {
  370. m.mu.Lock()
  371. if m.isUpdating {
  372. m.mu.Unlock()
  373. return fmt.Errorf("update already in progress")
  374. }
  375. m.isUpdating = true
  376. m.mu.Unlock()
  377. go func() {
  378. defer func() {
  379. m.mu.Lock()
  380. m.isUpdating = false
  381. m.config.LastUpdated = time.Now()
  382. m.config.NextUpdate = time.Now().Add(time.Duration(m.config.IntervalHours) * time.Hour)
  383. _ = m.saveConfig()
  384. m.mu.Unlock()
  385. }()
  386. m.mu.RLock()
  387. var enabled []string
  388. for code, c := range m.countries {
  389. if c.Enabled {
  390. enabled = append(enabled, code)
  391. }
  392. }
  393. m.mu.RUnlock()
  394. for _, code := range enabled {
  395. _, _ = m.SyncCountryZone(code)
  396. }
  397. }()
  398. return nil
  399. }
  400. // GetCountrySubnets returns first N subnets from cached zone file for preview
  401. func (m *GeoIPManager) GetCountrySubnets(code string, limit int) []string {
  402. code = strings.ToUpper(code)
  403. zoneFile := filepath.Join(GeoIPZonesDir, strings.ToLower(code)+".zone")
  404. f, err := os.Open(zoneFile)
  405. if err != nil {
  406. return nil
  407. }
  408. defer f.Close()
  409. if limit <= 0 {
  410. limit = 100
  411. }
  412. var res []string
  413. scanner := bufio.NewScanner(f)
  414. for scanner.Scan() {
  415. line := strings.TrimSpace(scanner.Text())
  416. if line != "" && !strings.HasPrefix(line, "#") {
  417. res = append(res, line)
  418. if len(res) >= limit {
  419. break
  420. }
  421. }
  422. }
  423. return res
  424. }
  425. // EnsureCountryOrPreset guarantees that the ipset mk_geo_<CODE> exists and has subnets loaded
  426. func (m *GeoIPManager) EnsureCountryOrPreset(code string) error {
  427. code = strings.TrimPrefix(code, "!")
  428. code = strings.TrimPrefix(code, "geo:")
  429. code = strings.TrimPrefix(code, "geo_")
  430. code = strings.ToUpper(strings.TrimSpace(code))
  431. if code == "" {
  432. return nil
  433. }
  434. // Check if it is a preset
  435. var targetCodes []string
  436. switch code {
  437. case "EU":
  438. 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"}
  439. case "BALKANS":
  440. targetCodes = []string{"GR", "CY", "AL", "BG", "RO", "RS", "MK", "ME", "BA", "HR", "SI", "TR"}
  441. case "BLOCKLIST", "HIGHRISK":
  442. targetCodes = []string{"CN", "RU", "IR", "KP", "BY"}
  443. }
  444. if len(targetCodes) > 0 {
  445. setName := "mk_geo_" + code
  446. var sb strings.Builder
  447. sb.WriteString(fmt.Sprintf("create %s hash:net maxelem 262144 -exist\n", setName))
  448. sb.WriteString(fmt.Sprintf("flush %s\n", setName))
  449. for _, tc := range targetCodes {
  450. zoneFile := filepath.Join(GeoIPZonesDir, strings.ToLower(tc)+".zone")
  451. if _, err := os.Stat(zoneFile); os.IsNotExist(err) {
  452. _, _ = m.SyncCountryZone(tc)
  453. }
  454. if f, err := os.Open(zoneFile); err == nil {
  455. scanner := bufio.NewScanner(f)
  456. for scanner.Scan() {
  457. line := strings.TrimSpace(scanner.Text())
  458. if line != "" && !strings.HasPrefix(line, "#") {
  459. sb.WriteString(fmt.Sprintf("add %s %s -exist\n", setName, line))
  460. }
  461. }
  462. f.Close()
  463. }
  464. }
  465. cmd := exec.Command("ipset", "restore")
  466. cmd.Stdin = strings.NewReader(sb.String())
  467. return cmd.Run()
  468. }
  469. // Single country
  470. zoneFile := filepath.Join(GeoIPZonesDir, strings.ToLower(code)+".zone")
  471. if _, err := os.Stat(zoneFile); os.IsNotExist(err) {
  472. if _, err := m.SyncCountryZone(code); err != nil {
  473. return err
  474. }
  475. }
  476. m.mu.Lock()
  477. if c, ok := m.countries[code]; ok {
  478. c.Enabled = true
  479. _ = m.saveConfig()
  480. }
  481. m.mu.Unlock()
  482. return m.ApplyCountryIPSet(code)
  483. }