geoip.go 13 KB

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