1378 字
7 分钟
使用 Nginx + Golang 实现重定向到 STUN 公网地址

小声de吐槽()#

在上篇文章 使用 Lucky 的 STUN 内网穿透,实现公网 IPv4 访问 中,我们实现了借助 Lucky 的 STUN 内网穿透,实现了在 NAT1 环境下,通过公网 IPv4 访问本地服务。

但是,我们的 公网 IPv4 地址和端口是动态的,变更频率不固定 ,这就导致了我们每次访问 STUN 公网地址时,都需要手动更新地址和端口。

这显然不符合慵懒猫猫的作风!~

为了实现固定域名访问公网地址,我们可以借助公网 Nginx 服务器 以及 Golang,实现重定向到 STUN 公网地址。

访问流程#

  1. 访问固定域名 example.com
  2. Nginx 服务器收到请求,将请求转发至 Golang 服务
  3. Golang 服务收到请求后,根据 target_data.json 文件,302 重定向至最新的 STUN 公网地址和端口
  4. 用户浏览器收到重定向响应,自动访问 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"
}

自行修改 portauth_token 捏~

port 需要和 Nginx 配置中的 proxy_pass 端口一致。

然后就可以启动服务啦~

你可以使用 screentmux 等工具在后台运行它。

Terminal window
screen -S WebhookServer
./WebhookServer

然后,你可以使用 Ctrl+A+D 来 detach 这个 screen 会话。

或者,你也可以使用 systemd 来管理这个服务。

创建一个名为 webhook-server.service 的文件,放在 /etc/systemd/system/ 目录下,内容如下:

[Unit]
Description=Webhook Server
After=network.target
[Service]
Type=simple
WorkingDirectory=/path/to
ExecStart=/path/to/WebhookServer
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target

/path/to 替换为你实际的二进制文件路径。

然后,启用并启动这个服务:

Terminal window
systemctl daemon-reload
systemctl 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手动触发测试 来验证配置是否正确。

正确无误后,点击 修改 来保存设置。

🎉 至此,全部就配置完成啦~#

现在你可以访问你的域名,看看是否能正常跳转了~

使用 Nginx + Golang 实现重定向到 STUN 公网地址
https://blog.ioll.cc/posts/2025/nginx-302-to-stun/
作者
星酱
发布于
2025-10-23
许可协议
CC BY-NC-SA 4.0