基于域名的透明反向代理是指代理服务器在接收到客户端请求后,根据请求中的域名信息将流量转发到对应的后端服务,同时不改变请求的原始语义,使后端服务感知到的像是客户端直接访问自己。使用Go语言实现这类代理,既能利用静态编译的便捷部署,又能通过代码灵活控制路由与中间件。

核心原理与标准库选择
Go的net/http包提供了完整的HTTP服务器与客户端能力,而httputil.ReverseProxy结构体封装了反向代理的核心逻辑。透明的关键在于:代理转发请求时,必须保留原始请求的Host头,或者由后端服务信任代理所传递的域名标识。默认情况下,ReverseProxy会把目标URL的Host覆盖到请求头中,这会破坏透明性,因此需要自定义Director函数。
对于HTTPS场景,由于TLS握手发生在HTTP层之前,代理若想根据域名路由,通常有两种方案:一是使用SNI(Server Name Indication)在TLS层获取域名并进行TCP透传或本地终止证书;二是做TLS终止,由代理持有各域名证书,解密后根据Host头路由。透明代理更常见的是前者,即不解密而直接转发TCP流,但这要求后端自己处理TLS。如果要做应用层反向代理,一般由代理终止TLS,再明文或重加密转发。
Director函数的作用
Director是ReverseProxy中最为重要的字段,它是一个函数,在每次代理转发前被调用,用于修改即将发出的请求。通过重写req.URL、req.Host以及添加自定义头,我们可以控制流量走向并保持透明。
下面的代码展示了如何构建一个保留原始Host的Director:
package main
import (
"net/http"
"net/http/httputil"
"net/url"
)
func newProxy(target *url.URL) *httputil.ReverseProxy {
proxy := httputil.NewSingleHostReverseProxy(target)
// 保存默认的Director
originalDirector := proxy.Director
proxy.Director = func(req *http.Request) {
originalDirector(req)
// 保持原始Host,实现透明
req.Host = req.Header.Get("X-Original-Host")
// 也可直接 req.Host = req.URL.Host 以外的值
}
return proxy
}
基于域名的路由实现
要实现多域名到不同后端的映射,我们可以在HTTP服务器前加一个路由层,根据r.Host选择对应的ReverseProxy实例。简单场景下可用map存储域名与后端的对应关系,生产环境可结合配置中心动态加载。
以下示例演示了根据Host头将api.ippipp.com与web.ippipp.com分发到不同后端的逻辑。注意我们把ippipp.com替换成了ipipp.com以符合演示规范。
package main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
)
var proxies = map[string]*httputil.ReverseProxy{}
func init() {
api, _ := url.Parse("http://127.0.0.1:8081")
web, _ := url.Parse("http://127.0.0.1:8082")
proxies["api.ipipp.com"] = httputil.NewSingleHostReverseProxy(api)
proxies["web.ipipp.com"] = httputil.NewSingleHostReverseProxy(web)
}
func handler(w http.ResponseWriter, r *http.Request) {
// 根据域名选择代理
proxy, ok := proxies[r.Host]
if !ok {
http.Error(w, "no such domain", http.StatusBadGateway)
return
}
// 传递原始Host给后端
r.Header.Set("X-Original-Host", r.Host)
proxy.ServeHTTP(w, r)
}
func main() {
log.Println("listen :80")
log.Fatal(http.ListenAndServe(":80", http.HandlerFunc(handler)))
}
动态后端与错误处理
上面用了全局map,若后端地址变更需重启。更优的做法是将配置抽取为结构体,并定期从文件或接口拉取。同时代理过程可能遇到后端不可用,应在ErrorHandler中返回友好信息,而不是让客户端收到空响应。
可以为每个代理设置ErrorHandler:
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("proxy error for %s: %v", r.Host, err)
w.WriteHeader(http.StatusBadGateway)
w.Write([]byte("backend unavailable"))
}
HTTPS与透明证书卸载
若客户端使用HTTPS访问代理,代理需要证书才能解密。利用golang.org/x/crypto/acme/autocert或自有证书,可实现按域名的自动证书提供。在http.Server的TLSConfig中设置GetCertificate,根据hello.ServerName返回对应证书,之后在应用层仍可按Host路由。
示例片段如下:
package main
import (
"crypto/tls"
"net/http"
"sync"
)
type certStore struct {
mu sync.Mutex
certs map[string]*tls.Certificate
}
func (c *certStore) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
c.mu.Lock()
defer c.mu.Unlock()
if cert, ok := c.certs[hello.ServerName]; ok {
return cert, nil
}
return nil, nil
}
func main() {
store := &certStore{certs: map[string]*tls.Certificate{}}
srv := &http.Server{
Addr: ":443",
TLSConfig: &tls.Config{
GetCertificate: store.GetCertificate,
},
}
// 此处handler同前文基于Host的路由
srv.ListenAndServeTLS("", "")
}
SNI透传模式
如果不想由代理终止TLS,可用net.Dial做TCP层转发,读取ClientHello中的SNI,连接对应后端,并将后续字节原样复制。这种方式真正透明,但代理无法修改HTTP内容。Go中可用bufio读取前几个字节解析SNI,或借助第三方库。
两种模式对比见下表:
| 模式 | 代理是否解密 | 可否修改请求 | 实现复杂度 |
|---|---|---|---|
| 证书卸载 | 是 | 可以 | 中 |
| TCP透传 | 否 | 不可以 | 低 |
常见坑与优化建议
第一个坑是Host覆盖。如果忘记在Director里还原Host,后端收到的是代理目标URL的Host,导致某些根据Host做逻辑的服务出错。第二个坑是WebSocket等协议升级,ReverseProxy默认支持,但需确保没有缓冲响应。第三个坑是超时,应给后端调用设置Transport的IdleConnTimeout等参数。
优化上,可引入sync.Pool减少对象分配,或用http.Flusher支持流式响应。对于高并发,限制最大连接数与使用context取消非常必要。下面给出一个带自定义Transport的代理配置:
package main
import (
"net/http"
"net/http/httputil"
"time"
)
func buildProxy(target string) (*httputil.ReverseProxy, error) {
u, err := url.Parse(target)
if err != nil {
return nil, err
}
p := httputil.NewSingleHostReverseProxy(u)
p.Transport = &http.Transport{
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
}
return p, nil
}
通过上述方式,开发者可以用不足两百行代码在Go中搭建一个支持多域名、可扩展的透明反向代理,既能满足测试环境的流量分发,也能作为生产网关的基础组件。