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) { // 获取缓存状态 cacheIPv4.mu.RLock() cacheAgeIPv4 := time.Since(cacheIPv4.timestamp).Round(time.Second) cacheIPv4.mu.RUnlock() cacheIPv6.mu.RLock() cacheAgeIPv6 := time.Since(cacheIPv6.timestamp).Round(time.Second) cacheIPv6.mu.RUnlock() // 判断状态 statusIPv4 := "🟢" if cacheAgeIPv4 > cacheDuration { statusIPv4 = "🔴" } else if cacheAgeIPv4 > cacheDuration/2 { statusIPv4 = "🟡" } statusIPv6 := "🟢" if cacheAgeIPv6 > cacheDuration { statusIPv6 = "🔴" } else if cacheAgeIPv6 > cacheDuration/2 { statusIPv6 = "🟡" } // 自动检测访问协议和地址 protocol := "http" if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" { protocol = "https" } host := r.Host if host == "" { host = "localhost:8080" } baseURL := fmt.Sprintf("%s://%s", protocol, host) fetchMode := protocol // 根据协议生成不同的提示和脚本 var protocolNote string var protocolBadge string if protocol == "https" { protocolNote = "您正在使用 HTTPS 安全连接,适合外网环境" protocolBadge = `🔒 HTTPS` } else { protocolNote = "您正在使用 HTTP 连接,适合内网环境" protocolBadge = `🌐 HTTP` } html := fmt.Sprintf(` CHNRoute RSC 服务

🇨🇳 CHNRoute RSC 服务

自动转换 IP 列表为 RouterOS 可导入格式

📥 下载地址

🔧 RouterOS 使用方法%s

%s

一键导入脚本

# SSH 登录 RouterOS 后执行
/tool fetch url="%s/chnroute.rsc" mode=%s
/import chnroute.rsc

# 验证导入
/ip firewall address-list print count-only where list=chnroute

自动更新脚本(推荐)

# 创建更新脚本
/system script add name=update-chnroute source={
    :log info "开始更新 CHNRoute"
    /file remove [find name="chnroute.rsc"]
    /tool fetch url="%s/chnroute.rsc" mode=%s
    :delay 3s
    /ip firewall address-list remove [find list=chnroute]
    /import chnroute.rsc
    :local count [/ip firewall address-list print count-only where list=chnroute]
    :log info ("CHNRoute 更新完成,共 " . $count . " 条")
}

# 设置每周自动执行(每周日凌晨3点)
/system scheduler add name=auto-update-chnroute \\
    interval=7d \\
    on-event="/system script run update-chnroute" \\
    start-time=03:00:00

IPv6 导入脚本

# IPv6 路由列表导入
/tool fetch url="%s/chnroute_v6.rsc" mode=%s
/import chnroute_v6.rsc

# 验证导入
/ipv6 firewall address-list print count-only where list=chnroute6

📊 服务状态运行中

%s
IPv4 缓存
已缓存 %v
%s
IPv6 缓存
已缓存 %v

♻️ 更新策略

`, baseURL, baseURL, // 1-2: 下载地址 IPv4 (href + 显示文本) baseURL, baseURL, // 3-4: 下载地址 IPv6 (href + 显示文本) protocolBadge, // 5: 协议徽章 protocolNote, // 6: 协议提示 baseURL, fetchMode, // 7-8: 一键导入 (url + mode) baseURL, fetchMode, // 9-10: 自动更新 (url + mode) baseURL, fetchMode, // 11-12: IPv6 导入 (url + mode) statusIPv4, cacheAgeIPv4, // 13-14: IPv4 状态图标 + 缓存时间 statusIPv6, cacheAgeIPv6) // 15-16: IPv6 状态图标 + 缓存时间 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) } }