Go语言反向代理的姿势

先重温一下什么叫反向代理,正向代理。
鹅厂二面,nginx回忆录

所谓正向,反向代理取决于代理的是出站请求,还是入站请求

正向代理: 代理的出站请求, 客户端能感知到代理程序,架构上距离客户端更近。
反向代理: 代理的是入站请求,客户端认为代理程序就是服务器,客户端感知不到代理逻辑,架构上距离服务端更近。 

反向代理的血案

前几天打算使用golang做一个代理程序,golang标准库net/http/httputil已经提供了这样的能力。

一把梭之后发现必然返回403 Forbidden, 我直接在target里面填上游服务实例ip就可以正确返回。

给一个向代理百度官网的简化示例,大家可以体会一下:

package main

import (
    "fmt"
    "log"
    "net/http"
    "net/http/httputil"
)

func ReverseProxyHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Println("receive a request from:", r.RemoteAddr, r.Header)

    target := "www.baidu.com"
    director := func(req *http.Request) {
        req.URL.Scheme = "https"
        req.URL.Host = target
        // req.Host = target
    }
    proxy := &httputil.ReverseProxy{Director: director}
    proxy.ServeHTTP(w, r)
}

func main() {
    fmt.Printf("Starting server at port 8080\n")
    if err := http.ListenAndServe(":8080", http.HandlerFunc(ReverseProxyHandler)); err != nil {
        log.Fatal(err)
    }
}

郁闷了很久,wireshark抓包也看不出端倪(其实是知识有漏洞,那肯定找不到原因)。 

头脑风暴

调试httputil的源代码:

  • 在代理后url中的host已经变成指定域名,但header中的host值没有发生变化还是localhost:8000;

  •  此时我并没有发现问题,因为我笃定url中的Host应该决定了请求的具体地址,抱着死马当活马医的态度,我重写了header中的Host为目标百度域名

req.Host = target // 上面被注释

竟然真的成功了

小板凳好好摆一摆

知识漏洞的关键点在于 :

  •  url中已经有Host了,为什么header中还要有Host?

  •  url中的Host与request.header中的Host到底什么关系?

rfc规范(这是个宝藏站点)[1]:

  1. Host请求头是在http1.1作为必选被引入,如果请求头没有Host或有多个Host请求头, 将会返回400错误。

  2. 请求中的“Host”提供了目标URI的主机和端口信息。

最关键的第三点:

     3.  设计Host请求头的动机[2]:在请求(为多个网站服务的)共享主机时,使共享主机能够区分目标资源。

The "Host" header field in a request provides the host and port information from the target URI, enabling the origin server to distinguish among resources while servicing requests for multiple host names

什么意思呢?

在微服务架构下,请求在打到业务应用之前都会流经负载均衡器,例如nginx/网关,这些负载均衡器提供了单负载节点,配置多个域名的能力。但是请求打到负载主机,需要有信息能区分目标服务域名,这就依赖请求头中的Host。

0cad2e2007b30cbd43a4ebbc4eeca1ae.png 上图来自 阿里云应用型负载均衡[3]  

我们来看在nginx配置基于名字的多虚拟主机[4]的写法:

cc380c2222c249348a8ce2d56e7959e1.png

在这个配置中,nginx会检查请求的Host头== server_name指令值 以决定该请求应由哪个虚拟主机来处理。   

如果Host头没有匹配任意一个虚拟主机,或者请求中根本没有包含Host头,那nginx会将请求分发到定义在此端口上的默认虚拟主机。  

在以上配置中,第一个被列出的虚拟主机即nginx的默认虚拟主机——这是nginx的默认行为。而且,可以显式地设置某个主机为默认虚拟主机,即在"listen"指令中设置"default_server"参数。

server {
    listen      80 default_server;
    server_name example.net www.example.net;  // 可以有多个,匹配请求头的Host值
    ...
}

回到最开始的问题,我们写的反向代理程序其实是客户端,虽然重写了url Host, 但是请求打到虚拟主机的时候,请求头中的Host还是最开始的localhost:8080, 这个Host根本无法在虚拟主机中被识别, 所以我们还需要重写请求头中的Host为目标域名。

httputil内置了一反向代理到固定地址的实现:NewSingleHostReverseProxy ,这个实现也没有重写Host header, 我上面其实就是一个自定义实现。

NewSingleHostReverseProxy returns a new ReverseProxy that routes URLs to the scheme, host, and base path provided in target. If the target's path is "/base" and the incoming request was for "/dir",the target request will be for /base/dir. NewSingleHostReverseProxy does not rewrite the Host header. To rewrite Host headers, use ReverseProxy directly with a custom Director policy.

结束语

本文通过一个简单的反向代理程序的错误姿势,引出了Host请求头的作用,更重要的是认识了主流负载均衡服务器在请求链路中的行为。

Host请求头用于在单负载节点支撑多域名。

引用链接

[1] rfc规范(这是一个宝藏站点): https://www.rfc-editor.org/rfc/rfc9110.html
[2] 设计Host请求头的动机: https://www.rfc-editor.org/rfc/rfc9110.html#name-host-and-authority
[3] 阿里云应用型负载均衡: https://help.aliyun.com/document_detail/223900.html
[4] 在nginx配置基于名字的多虚拟主机: https://tengine.taobao.org/nginx_docs/cn/docs/http/request_processing.html

[5]  鹅厂二面,Nginx回忆录

[6] 一条nginx命令引发的对于容器的思考

点“”戳“在看

体现态度很有必要!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在Golang中实现WebSocket反向代理相对简单。你可以使用`gorilla/websocket`包来处理WebSocket连接,并使用`net/http/httputil`包来实现反向代理。 首先,你需要安装`gorilla/websocket`包。可以使用以下命令来安装: ``` go get github.com/gorilla/websocket ``` 接下来,你可以使用以下代码来创建一个WebSocket反向代理: ```go package main import ( "log" "net/http" "net/http/httputil" "net/url" "github.com/gorilla/websocket" ) func main() { // 创建一个反向代理的目标URL backendURL := "ws://localhost:8081/ws" targetURL, err := url.Parse(backendURL) if err != nil { log.Fatal(err) } // 创建WebSocket代理 proxy := httputil.NewSingleHostReverseProxy(targetURL) // WebSocket处理函数 websocketHandler := func(w http.ResponseWriter, r *http.Request) { // 升级HTTP连接到WebSocket连接 upgrader := websocket.Upgrader{} conn, err := upgrader.Upgrade(w, r, nil) if err != nil { http.Error(w, "Failed to upgrade to WebSocket", http.StatusInternalServerError) return } defer conn.Close() // 反向代理WebSocket连接 proxy.ServeHTTP(w, r) } // 注册WebSocket处理函数 http.HandleFunc("/ws", websocketHandler) // 启动HTTP服务器 log.Println("Starting server on :8080") err = http.ListenAndServe(":8080", nil) if err != nil { log.Fatal(err) } } ``` 以上代码创建了一个简单的WebSocket反向代理,将所有连接到`/ws`路径的WebSocket连接转发到`ws://localhost:8081/ws`。你可以根据实际需求修改代理的目标URL和端口。 请注意,这只是一个简单的示例,你可能需要根据你的具体需求进行修改和扩展。此外,你还可以添加身份验证、错误处理等功能来完善代理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

有态度的马甲

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值