小声de吐槽()
在上篇文章 使用 Lucky 的 STUN 内网穿透,实现公网 IPv4 访问 中,我们实现了借助 Lucky 的 STUN 内网穿透,实现了在 NAT1 环境下,通过公网 IPv4 访问本地服务。
但是,我们的 公网 IPv4 地址和端口是动态的,变更频率不固定 ,这就导致了我们每次访问 STUN 公网地址时,都需要手动更新地址和端口。
这显然不符合慵懒猫猫的作风!~
为了实现固定域名访问公网地址,我们可以借助公网 Nginx 服务器 以及 Golang,实现重定向到 STUN 公网地址。
访问流程
- 访问固定域名
example.com - Nginx 服务器收到请求,将请求转发至 Golang 服务
- Golang 服务收到请求后,根据
target_data.json文件,302 重定向至最新的 STUN 公网地址和端口 - 用户浏览器收到重定向响应,自动访问 STUN 公网地址和端口
首先,我们先来配置 Nginx
狐务器在哪里都可以,只要有公网 IP 即可。
安装 Nginx 的那些咱就不说了,自己搜索一下吧。这里直接给出反向代理的配置
127.0.0.1:8080 是 Golang 服务的监听地址,可以自定义的喵~
# 健康检查location /health { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; access_log off;}
# Webhook 更新端点 (建议使用更复杂的路径)location /internal/webhook { proxy_pass http://127.0.0.1:8080/webhook; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;}
# 主重定向逻辑location / { # 使用 proxy_pass 由 Golang 处理重定向 proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;}然后,我们来配置 Golang 服务
Golang 服务的代码如下:
package main
import ( "encoding/json" "fmt" "log" "net/http" "os" "sync" "time")
// Config 配置结构体type Config struct { Port string `json:"port"` TargetFile string `json:"target_file"` AuthToken string `json:"auth_token"`}
// Target 目标地址结构体type Target struct { IP string `json:"ip"` Port string `json:"port"` RuleName string `json:"rule_name"` UpdateTime time.Time `json:"update_time"`}
var ( config Config currentTarget Target mutex sync.RWMutex)
// 加载配置func loadConfig() error { config = Config{ Port: "8080", TargetFile: "target_data.json", AuthToken: os.Getenv("WEBHOOK_TOKEN"), // 从环境变量获取token }
// 可以从配置文件加载 if file, err := os.ReadFile("config.json"); err == nil { json.Unmarshal(file, &config) }
return nil}
// 处理 Webhook 更新func handleWebhook(w http.ResponseWriter, r *http.Request) { // 认证检查 if config.AuthToken != "" { token := r.Header.Get("Authorization") if token != "Bearer "+config.AuthToken && token != config.AuthToken { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } }
if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return }
var newTarget Target if err := json.NewDecoder(r.Body).Decode(&newTarget); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return }
if newTarget.IP == "" || newTarget.Port == "" { http.Error(w, "Missing IP or port", http.StatusBadRequest) return }
newTarget.UpdateTime = time.Now()
// 更新目标地址 mutex.Lock() currentTarget = newTarget mutex.Unlock()
// 保存到文件 if err := saveTargetToFile(newTarget); err != nil { log.Printf("Failed to save target: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) return }
log.Printf("Target updated: %s:%s (Rule: %s)", newTarget.IP, newTarget.Port, newTarget.RuleName)
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ "status": "success", "message": fmt.Sprintf("Updated to %s:%s", newTarget.IP, newTarget.Port), })}
// 获取当前目标地址func handleGetTarget(w http.ResponseWriter, r *http.Request) { mutex.RLock() target := currentTarget mutex.RUnlock()
w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(target)}
// 重定向端点 - 返回 302 重定向func handleRedirect(w http.ResponseWriter, r *http.Request) { mutex.RLock() target := currentTarget mutex.RUnlock()
if target.IP == "" || target.Port == "" { http.Error(w, "Target not configured", http.StatusServiceUnavailable) return }
// 构建重定向 URL redirectURL := fmt.Sprintf("http://%s:%s%s", target.IP, target.Port, r.URL.Path) if r.URL.RawQuery != "" { redirectURL += "?" + r.URL.RawQuery }
log.Printf("Redirecting to: %s", redirectURL) http.Redirect(w, r, redirectURL, http.StatusFound)}
// 保存目标到文件func saveTargetToFile(target Target) error { data, err := json.MarshalIndent(target, "", " ") if err != nil { return err } return os.WriteFile(config.TargetFile, data, 0644)}
// 从文件加载目标func loadTargetFromFile() error { data, err := os.ReadFile(config.TargetFile) if err != nil { if os.IsNotExist(err) { return nil // 文件不存在是正常的 } return err }
var target Target if err := json.Unmarshal(data, &target); err != nil { return err }
mutex.Lock() currentTarget = target mutex.Unlock()
log.Printf("Loaded target from file: %s:%s", target.IP, target.Port) return nil}
// 健康检查func handleHealth(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ "status": "healthy", "timestamp": time.Now().Format(time.RFC3339), })}
func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) log.Printf("%s %s %s %v", r.Method, r.URL.Path, r.RemoteAddr, time.Since(start)) }}
func main() { // 加载配置 if err := loadConfig(); err != nil { log.Fatalf("Failed to load config: %v", err) }
// 加载保存的目标数据 if err := loadTargetFromFile(); err != nil { log.Printf("Warning: Failed to load target data: %v", err) }
// 设置路由 http.HandleFunc("/webhook", loggingMiddleware(handleWebhook)) http.HandleFunc("/target", loggingMiddleware(handleGetTarget)) http.HandleFunc("/health", loggingMiddleware(handleHealth)) http.HandleFunc("/", loggingMiddleware(handleRedirect))
log.Printf("Webhook server starting on port %s", config.Port) log.Printf("Target file: %s", config.TargetFile)
if err := http.ListenAndServe(":"+config.Port, nil); err != nil { log.Fatalf("Server failed: %v", err) }}将 Golang 服务部署到服务器
将编译好的二进制文件上传到服务器上,然后创建配置文件 config.json 内容如下:
{ "port": "8080", "target_file": "target_data.json", "auth_token": "your-secret-token-here"}自行修改 port 和 auth_token 捏~
port 需要和 Nginx 配置中的 proxy_pass 端口一致。
然后就可以启动服务啦~
你可以使用 screen 或 tmux 等工具在后台运行它。
screen -S WebhookServer./WebhookServer然后,你可以使用 Ctrl+A+D 来 detach 这个 screen 会话。
或者,你也可以使用 systemd 来管理这个服务。
创建一个名为 webhook-server.service 的文件,放在 /etc/systemd/system/ 目录下,内容如下:
[Unit]Description=Webhook ServerAfter=network.target
[Service]Type=simpleWorkingDirectory=/path/toExecStart=/path/to/WebhookServerRestart=alwaysRestartSec=5
[Install]WantedBy=multi-user.target将 /path/to 替换为你实际的二进制文件路径。
然后,启用并启动这个服务:
systemctl daemon-reloadsystemctl enable webhook-server --now你可以使用 systemctl status webhook-server 来检查服务状态。
配置 Lucky STUN Webhook

| 配置项 | 说明 |
|---|---|
| 接口地址 | http://<指向Nginx的域名或IP>/webhook |
| 请求方法 | POST |
| 请求头 | Authorization: Bearer <auth_token> |
| 请求体 | JSON 格式,参考 示例 |
| 接口调用成功包含的字符串 | 填写 success |
示例请求体
{"ip":"#{ip}", "port":"#{port}", "rule_name":"#{ruleName}"}填写完成后点击 Webhook手动触发测试 来验证配置是否正确。
正确无误后,点击 修改 来保存设置。
🎉 至此,全部就配置完成啦~
现在你可以访问你的域名,看看是否能正常跳转了~