├── .env.example ├── .github └── workflows │ └── docker.yaml ├── .gitignore ├── Dockerfile ├── README.md ├── go.mod └── main.go /.env.example: -------------------------------------------------------------------------------- 1 | # Domain mapping configuration 2 | # Format: one domain mapping per line 3 | # domain->target1,target2 4 | DOMAIN_MAPPING_1=old-domain1.com->https://target1.com,https://target2.com 5 | DOMAIN_MAPPING_2=old-domain2.com->https://target3.com,https://target4.com 6 | DOMAIN_MAPPING_3=localhost:3010->https://nexmoe.com,https://i.nexmoe.com 7 | 8 | # Server port 9 | PORT=8080 -------------------------------------------------------------------------------- /.github/workflows/docker.yaml: -------------------------------------------------------------------------------- 1 | name: Docker Image Publish 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | 8 | jobs: 9 | docker: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - 13 | name: Login to Docker Hub 14 | uses: docker/login-action@v3 15 | with: 16 | username: ${{ secrets.DOCKERHUB_USERNAME }} 17 | password: ${{ secrets.DOCKERHUB_TOKEN }} 18 | - 19 | name: Set up QEMU 20 | uses: docker/setup-qemu-action@v3 21 | - 22 | name: Set up Docker Buildx 23 | uses: docker/setup-buildx-action@v3 24 | - 25 | name: Build and push 26 | uses: docker/build-push-action@v6 27 | with: 28 | push: true 29 | tags: | 30 | nexmoe/domain-redirect:latest 31 | nexmoe/domain-redirect:${{ github.sha }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS system files 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | 6 | # Go specific 7 | *.exe 8 | *.exe~ 9 | *.dll 10 | *.so 11 | *.dylib 12 | *.test 13 | *.out 14 | *.prof 15 | 16 | # IDE specific files 17 | .idea/ 18 | .vscode/ 19 | *.swp 20 | *.swo 21 | 22 | # Build output 23 | domain-redirect 24 | myapp -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Build stage 2 | FROM golang:1.21-alpine AS builder 3 | 4 | # Set working directory 5 | WORKDIR /app 6 | 7 | # Copy only the necessary files for building 8 | COPY . . 9 | 10 | # Build the application with optimizations 11 | RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o domain-redirect main.go 12 | 13 | # Stage 2: Runtime stage 14 | FROM scratch 15 | 16 | # Set working directory 17 | WORKDIR /app 18 | 19 | # Copy only the built binary from builder stage 20 | COPY --from=builder /app/domain-redirect . 21 | 22 | # Expose the application port 23 | EXPOSE 8080 24 | 25 | # Run the application 26 | CMD ["./domain-redirect"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 轻量级域名重定向服务开源发布 2 | 3 | 各位小伙伴们好!今天给大家安利一个我刚刚撸完的域名重定向服务工具。起因是手头闲置了不少域名,想用来做跳转服务。之前用 Caddy 虽然不错,但配置略显繁琐,功能也不够趁手,索性自己用 Golang 撸了个轻量级方案! 4 | 5 | ✨ 项目亮点: 6 | 7 | - 🚀 轻量化设计:Docker 镜像仅 4.2MiB,内存占用约 1.277 MiB 8 | - ⚡ 即开即用:支持 Docker 一键部署,配置简单直观 9 | - 🔗 智能跳转:支持多目标轮询、来源追踪、路径保留等实用功能 10 | - 📊 数据分析:自带来源参数,轻松对接 Google Analytics 统计流量 11 | 12 | 现已开源并发布到 Docker Hub,欢迎各位大佬在 GitHub 点个 Star 支持一下!你的星星就是我爆肝的动力~ 13 | 14 | ## 配置说明 15 | 16 | 服务通过环境变量进行配置,支持多个域名映射规则。每个映射规则使用 `DOMAIN_MAPPING_*` 格式的环境变量进行配置。 17 | 18 | ### 环境变量格式 19 | 20 | ``` 21 | DOMAIN_MAPPING_<任意名称>=<域名>-><目标地址1>,<目标地址2>,... 22 | ``` 23 | 24 | 例如: 25 | 26 | ``` 27 | DOMAIN_MAPPING_1=example.com->https://target1.com,https://target2.com 28 | ``` 29 | 30 | ### 其他环境变量 31 | 32 | - `PORT`: 服务监听端口,默认为 8080 33 | - `PRESERVE_PATH`: 是否保持原始路径,默认为 false。设置为 "true" 时,重定向时会保留原始请求路径。 34 | - `INCLUDE_REFERRAL`: 是否在重定向时携带来源域名信息,默认为 false。设置为 "true" 时,重定向时会添加 ref 参数,值为来源域名。 35 | - `ENABLE_TIMESTAMP`: 是否启用时间戳参数,默认为 false。设置为 "true" 时,重定向时会添加 _t 参数。 36 | 37 | ## 部署方式 38 | 39 | ### Docker 部署 40 | 41 | 1. 拉取镜像: 42 | 43 | ```bash 44 | docker pull nexmoe/domain-redirect 45 | ``` 46 | 47 | 2. 构建镜像(可选): 48 | 49 | ```bash 50 | docker build -t domain-redirect . 51 | ``` 52 | 53 | 3. 运行容器: 54 | 55 | ```bash 56 | docker run -d \ 57 | -p 8080:8080 \ 58 | -e DOMAIN_MAPPING_1=example.com->https://target1.com,https://target2.com \ 59 | nexmoe/domain-redirect 60 | ``` 61 | 62 | ### Docker Compose 部署 63 | 64 | 1. 创建 `docker-compose.yml` 文件: 65 | 66 | ```yaml 67 | version: '3' 68 | services: 69 | domain-redirect: 70 | image: nexmoe/domain-redirect 71 | container_name: domain-redirect 72 | ports: 73 | - "8080:8080" 74 | environment: 75 | - DOMAIN_MAPPING_1=example.com->https://target1.com,https://target2.com 76 | restart: unless-stopped 77 | ``` 78 | 79 | 2. 启动服务: 80 | 81 | ```bash 82 | docker-compose up -d 83 | ``` 84 | 85 | 3. 停止服务: 86 | 87 | ```bash 88 | docker-compose down 89 | ``` 90 | 91 | ### 直接运行 92 | 93 | 1. 编译: 94 | 95 | ```bash 96 | go build -o domain-redirect main.go 97 | ``` 98 | 99 | 2. 运行: 100 | 101 | ```bash 102 | export DOMAIN_MAPPING_1=example.com->https://target1.com,https://target2.com 103 | ./domain-redirect 104 | ``` 105 | 106 | ## 使用示例 107 | 108 | 假设配置了以下映射: 109 | 110 | ``` 111 | DOMAIN_MAPPING_1=example.com->https://target1.com,https://target2.com 112 | ``` 113 | 114 | 当访问 `http://example.com/any/path` 时,服务会: 115 | 116 | 1. 在目标地址之间轮询选择 117 | 2. 将请求重定向到选中的目标地址 118 | 3. 保持原始路径(如果配置了 PRESERVE_PATH 为 true) 119 | 4. 添加时间戳参数防止缓存(如果配置了 ENABLE_TIMESTAMP 为 true) 120 | 5. 添加来源域名信息(如果配置了 INCLUDE_REFERRAL 为 true) 121 | 122 | ## 注意事项 123 | 124 | - 确保配置的域名映射规则格式正确 125 | - 目标地址必须是有效的 URL 126 | - 服务默认监听 8080 端口,可以通过 PORT 环境变量修改 127 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yourusername/redirect-app 2 | 3 | go 1.21 -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | "os" 8 | "strings" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | // DomainConfig represents the configuration for a domain and its targets 14 | type DomainConfig struct { 15 | Domain string 16 | Targets []string 17 | } 18 | 19 | // TargetIndices keeps track of the last used target index for each domain 20 | var targetIndices = make(map[string]int) 21 | var mutex = &sync.Mutex{} 22 | 23 | // parseDomainMapping parses a domain mapping string into a DomainConfig 24 | func parseDomainMapping(mappingStr string) DomainConfig { 25 | parts := strings.Split(mappingStr, "->") 26 | if len(parts) < 2 { 27 | return DomainConfig{} 28 | } 29 | domain := parts[0] 30 | targets := strings.Split(parts[1], ",") 31 | return DomainConfig{ 32 | Domain: domain, 33 | Targets: targets, 34 | } 35 | } 36 | 37 | // getDomainConfigs retrieves all domain configurations from environment variables 38 | func getDomainConfigs() []DomainConfig { 39 | var configs []DomainConfig 40 | for _, env := range os.Environ() { 41 | if strings.HasPrefix(env, "DOMAIN_MAPPING_") { 42 | parts := strings.SplitN(env, "=", 2) 43 | if len(parts) == 2 { 44 | configs = append(configs, parseDomainMapping(parts[1])) 45 | } 46 | } 47 | } 48 | return configs 49 | } 50 | 51 | // findMatchingConfig finds the configuration that matches the given host 52 | func findMatchingConfig(host string, configs []DomainConfig) *DomainConfig { 53 | for _, config := range configs { 54 | if host == config.Domain || host == fmt.Sprintf("%s:%s", config.Domain, os.Getenv("PORT")) { 55 | return &config 56 | } 57 | } 58 | return nil 59 | } 60 | 61 | // getNextTarget gets the next target in round-robin fashion 62 | func getNextTarget(domain string, targets []string) string { 63 | mutex.Lock() 64 | defer mutex.Unlock() 65 | 66 | currentIndex := targetIndices[domain] 67 | nextIndex := (currentIndex + 1) % len(targets) 68 | targetIndices[domain] = nextIndex 69 | 70 | return targets[currentIndex] 71 | } 72 | 73 | func main() { 74 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 75 | host := r.Host 76 | path := r.URL.Path 77 | 78 | // Get domain configurations 79 | configs := getDomainConfigs() 80 | 81 | // Find matching configuration 82 | config := findMatchingConfig(host, configs) 83 | if config != nil { 84 | // Get next target 85 | target := getNextTarget(config.Domain, config.Targets) 86 | 87 | // Construct target URL 88 | targetURL, err := url.Parse(target) 89 | if err != nil { 90 | http.Error(w, "Invalid target URL", http.StatusInternalServerError) 91 | return 92 | } 93 | 94 | // Add path to target URL if preserve_path is enabled 95 | if os.Getenv("PRESERVE_PATH") == "true" { 96 | targetURL.Path = path 97 | } 98 | 99 | // Add cache prevention parameter if enabled 100 | query := targetURL.Query() 101 | if os.Getenv("ENABLE_TIMESTAMP") == "true" { 102 | query.Set("_t", fmt.Sprintf("%d", time.Now().UnixNano())) 103 | } 104 | 105 | // Add referral information if enabled 106 | if os.Getenv("INCLUDE_REFERRAL") == "true" { 107 | query.Set("ref", r.Host) 108 | } 109 | 110 | targetURL.RawQuery = query.Encode() 111 | 112 | // Redirect 113 | http.Redirect(w, r, targetURL.String(), http.StatusFound) 114 | return 115 | } 116 | 117 | // Default response 118 | fmt.Fprintf(w, "Domain redirect service is running") 119 | }) 120 | 121 | port := os.Getenv("PORT") 122 | if port == "" { 123 | port = "8080" 124 | } 125 | 126 | fmt.Printf("Server starting on port %s\n", port) 127 | http.ListenAndServe(":"+port, nil) 128 | } --------------------------------------------------------------------------------