commit d7383472273a92b1f02d145be315634894595162 Author: XOF Date: Tue Dec 30 02:51:16 2025 +0800 添加 main.go diff --git a/main.go b/main.go new file mode 100644 index 0000000..788fcdc --- /dev/null +++ b/main.go @@ -0,0 +1,219 @@ +package main + +import ( + "bufio" + "fmt" + "io" + "log" + "net/http" + "strings" + "sync" + "time" +) + +const ( + // 数据源 URL + urlChnrouteIPv4 = "https://raw.githubusercontent.com/mayaxcn/china-ip-list/master/chnroute.txt" + urlChnrouteIPv6 = "https://raw.githubusercontent.com/mayaxcn/china-ip-list/master/chnroute_v6.txt" + + // 缓存时间(1小时) + cacheDuration = 1 * time.Hour +) + +// 缓存结构 +type Cache struct { + data string + timestamp time.Time + mu sync.RWMutex +} + +var ( + cacheIPv4 = &Cache{} + cacheIPv6 = &Cache{} +) + +// 从 URL 获取数据 +func fetchData(url string) (string, error) { + resp, err := http.Get(url) + if err != nil { + return "", fmt.Errorf("fetch error: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("HTTP %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("read error: %w", err) + } + + return string(body), nil +} + +// 转换为 RouterOS RSC 格式 +func convertToRSC(cidrList string, listName string, ipVersion string) string { + var sb strings.Builder + scanner := bufio.NewScanner(strings.NewReader(cidrList)) + + // 添加头部注释 + sb.WriteString(fmt.Sprintf("# China IP List - RouterOS Script\n")) + sb.WriteString(fmt.Sprintf("# Generated: %s\n", time.Now().Format("2006-01-02 15:04:05"))) + sb.WriteString(fmt.Sprintf("# Source: https://github.com/mayaxcn/china-ip-list\n\n")) + + // 根据 IP 版本选择命令前缀 + prefix := "/ip firewall address-list" + if ipVersion == "6" { + prefix = "/ipv6 firewall address-list" + } + + count := 0 + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + sb.WriteString(fmt.Sprintf("%s add list=%s address=%s\n", prefix, listName, line)) + count++ + } + + sb.WriteString(fmt.Sprintf("\n# Total: %d entries\n", count)) + return sb.String() +} + +// 获取或更新缓存 +func getOrUpdateCache(cache *Cache, url string, listName string, ipVersion string) (string, error) { + cache.mu.RLock() + if time.Since(cache.timestamp) < cacheDuration && cache.data != "" { + defer cache.mu.RUnlock() + return cache.data, nil + } + cache.mu.RUnlock() + + // 需要更新缓存 + cache.mu.Lock() + defer cache.mu.Unlock() + + // 双重检查 + if time.Since(cache.timestamp) < cacheDuration && cache.data != "" { + return cache.data, nil + } + + log.Printf("Fetching data from %s...", url) + cidrData, err := fetchData(url) + if err != nil { + return "", err + } + + rscData := convertToRSC(cidrData, listName, ipVersion) + cache.data = rscData + cache.timestamp = time.Now() + + log.Printf("Cache updated: %d bytes", len(rscData)) + return rscData, nil +} + +// HTTP 处理器 +func handleIPv4(w http.ResponseWriter, r *http.Request) { + data, err := getOrUpdateCache(cacheIPv4, urlChnrouteIPv4, "chnroute", "4") + if err != nil { + http.Error(w, fmt.Sprintf("Error: %v", err), http.StatusInternalServerError) + log.Printf("IPv4 error: %v", err) + return + } + + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("Content-Disposition", "attachment; filename=chnroute.rsc") + w.Write([]byte(data)) + log.Printf("IPv4 request served: %s", r.RemoteAddr) +} + +func handleIPv6(w http.ResponseWriter, r *http.Request) { + data, err := getOrUpdateCache(cacheIPv6, urlChnrouteIPv6, "chnroute6", "6") + if err != nil { + http.Error(w, fmt.Sprintf("Error: %v", err), http.StatusInternalServerError) + log.Printf("IPv6 error: %v", err) + return + } + + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("Content-Disposition", "attachment; filename=chnroute_v6.rsc") + w.Write([]byte(data)) + log.Printf("IPv6 request served: %s", r.RemoteAddr) +} + +func handleHealth(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + fmt.Fprintf(w, `{"status":"ok","cache_age_ipv4":"%s","cache_age_ipv6":"%s"}`, + time.Since(cacheIPv4.timestamp).Round(time.Second), + time.Since(cacheIPv6.timestamp).Round(time.Second)) +} + +func handleRoot(w http.ResponseWriter, r *http.Request) { + html := ` + + + + CHNRoute RSC 服务 + + + +

🇨🇳 CHNRoute RSC 转换服务

+

自动从 mayaxcn/china-ip-list 获取最新数据并转换为 RouterOS 格式

+ +

📥 下载地址

+ + + +

🔧 RouterOS 使用方法

+
/tool fetch url="http://你的服务器IP:8080/chnroute.rsc" mode=http
+/import chnroute.rsc
+/ip firewall address-list print count-only where list=chnroute
+ +

♻️ 更新策略

+ + +

📊 健康检查

+ + +` + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Write([]byte(html)) +} + +func main() { + // 启动时预热缓存 + go func() { + log.Println("Warming up cache...") + getOrUpdateCache(cacheIPv4, urlChnrouteIPv4, "chnroute", "4") + getOrUpdateCache(cacheIPv6, urlChnrouteIPv6, "chnroute6", "6") + log.Println("Cache warmed up") + }() + + // 路由 + http.HandleFunc("/", handleRoot) + http.HandleFunc("/chnroute.rsc", handleIPv4) + http.HandleFunc("/chnroute_v6.rsc", handleIPv6) + http.HandleFunc("/health", handleHealth) + + port := ":8080" + log.Printf("🚀 Server starting on http://0.0.0.0%s", port) + if err := http.ListenAndServe(port, nil); err != nil { + log.Fatal(err) + } +}