网络代理基础知识
网络代理基础知识
代理服务器在网络通信中扮演着重要的角色,它不仅能帮助突破服务器的限制和封锁,还能实现系统安全、负载均衡等功能。本文将详细介绍代理服务器的分类、实现机制以及在实际项目中的应用。
代理的分类和实现机制
正向代理
当我们谈论代理服务器时,通常指正向代理。正向代理会向一个客户端或一组客户端提供代理服务,这些客户端通常属于同一个内部网络。当客户端尝试访问外部服务器时,请求必须首先通过正向代理。正向代理能够监控每个请求与回复,鉴权、控制访问权限并隐藏客户端实际地址。在隐藏了客户端的真实地址后,正向代理可以绕过一些防火墙的网络限制,这样一些开发者可以实现匿名。
一个简单的http正向代理服务如下所示。在这个例子中,代理服务器接受来自客户端的http请求,并通过handleHTTP函数对请求进行处理。处理的方式比较简单:当前代理服务器获取客户端的请求,并用自己的省份发送请求到服务器,代理服务器获取服务器的回复后利用io.Copy将回复发送至客户端。
package main
import (
"io"
"log"
"net/http"
)
func main() {
server := &http.Server{
Addr: ":8888",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
handleHTTP(w,r)
}),
}
log.Fatal(server.ListenAndServe())
}
func handleHTTP(w http.ResponseWriter,req *http.Request) {
resp,_ := http.DefaultTransport.RoundTrip(req)
defer resp.Body.Close()
copyHeader(w.Header(),resp.Header)
w.WriteHeader(resp.StatusCode)
io.Copy(w,resp.Body)
}
func copyHeader(dst,src http.Header) {
for k, vv := range src {
for _, v := range vv {
dst.Add(k,v)
}
}
}
代理服务器除了要在客户端与服务器之间搭建一个管道,有时还需要处理一些特殊的http请求头——hop-by-hop请求头(不是给目标服务器使用的,而是专门给中间的代理服务器使用的)。代理服务器需要根据情况对hop-by-hop请求头做一些特殊处理,并在发送给目标服务器之前删除hop-by-hop请求头。
var hopHeaders = []string{
"Connection",
"Proxy-Connection", // non-standard but still sent by libcurl and rejected by e.g. google
"Keep-Alive",
"Proxy-Authenticate",
"Proxy-Authorization",
"Te", // canonicalized version of "TE"
"Trailer", // not Trailers per URL above; https://www.rfc-editor.org/errata_search.php?eid=4522
"Transfer-Encoding",
"Upgrade",
}
HTTP隧道代理
在上面的例子中,代理服务器是直接与目标服务器进行http通信的。但是在一些更复杂的情况下,客户端还希望与服务器进行https和http隧道形式的通信,以防止中间人攻击并隐藏http的特征。
在http隧道技术中,客户端会在第一次连接代理服务器时向代理服务器发送一个指令,通常是一个http请求,这里可将http请求头中的method设置为connect。
代理服务器收到指令后,将与目标服务器建立tcp连接。连接建立后,代理服务器会将之后收到的请求通过tcp连接转发给目标服务器。因此,只有初始连接请求是http请求,此后代理服务器将不再嗅探到任何数据,只是完成一个转发的动作。现在我们去查看其他开源的代理库,就会明白为什么要对connect方法进行单独处理了。
下面在前一个例子的基础上实现http隧道:
package main
import (
"io"
"log"
"net"
"net/http"
"time"
)
func main() {
server := &http.Server{
Addr: ":8888",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodConnect{
}else{
handleHTTP(w,r)
}
}),
}
log.Fatal(server.ListenAndServe())
}
func handleTunneling(w http.ResponseWriter,req *http.Request) {
dest_conn,err := net.DialTimeout("tcp",req.Host,10*time.Second)
w.WriteHeader(http.StatusOK)
hijacker,ok:=w.(http.Hijacker)
client_conn,_,err :=hijacker.Hijack()//获取客户端与服务器之间的底层tcp连接
if err != nil{
http.Error(w,err.Error(),http.StatusServiceUnavailable)
}
go transfer(dest_conn,client_conn)
go transfer(client_conn,dest_conn)
}
func transfer(dest io.WriteCloser,source io.ReadCloser) {
defer dest.Close()
defer source.Close()
io.Copy(dest,source)//一般不会这么粗暴,因为传输量巨大,会使用一个for循环,控制每次转发的数据包大小
}
func handleHTTP(w http.ResponseWriter,req *http.Request) {
resp,_ := http.DefaultTransport.RoundTrip(req)
defer resp.Body.Close()
copyHeader(w.Header(),resp.Header)
w.WriteHeader(resp.StatusCode)
io.Copy(w,resp.Body)
}
func copyHeader(dst,src http.Header) {
for k, vv := range src {
for _, v := range vv {
dst.Add(k,v)
}
}
}
MITM代理
除了上面提到的http隧道代理,代理服务器还可以使用https来处理数据,即让代理服务器直接与目标服务器建立https连接,同时在客户端与服务器之间建立另一个https连接。
但是https天然阻止了这种中间人攻击,而要突破这种封锁,就需要让客户端完全信任代理服务器颁布的证书,因此这种代理服务器也被称为MITM。MITM就像一个中间人,能够看到所有经过它的http和https流量,这也是一些代理软件(例如Charles)能够嗅探到https数据的原因。
透明代理
在上面的代理中,客户端需要感知到代理服务器的存在。还有一类代理,客户端不用感知到代理服务器,只需要直接向目标服务器中发送消息,通过os或路由设置强制将请求发送到代理服务器中。举一个例子,在macOS操作系统上就可以设置系统代理,这样,在浏览器上发送的所有http/https请求都会被转发到代理服务器的地址127.0.0.1:8888中,如下图所示。而在Linux服务器中,我们可以使用iptables、ipvs等技术强制将请求转发到代理服务器上。
反向代理
反向代理和正向代理的区别在于,反向代理位于服务器和防火墙(Firewall)之间,客户端不能直接与服务器通信,需要通过反向代理进行通信。我们比较熟悉的Nginx一般就是用于实现反向代理的。
反向代理有以下好处:
- 负载均衡:对于大型分布式系统来说,反向代理可以提供一种负载均衡方案,在不同服务器之间平均分配传入流量,防止单个服务器过载。如果某台服务器完全无法运转,那么可以将流量转发给其他服务器。
- 防范攻击:配备反向代理后,服务器无须暴露真实的IP地址,这就让攻击者难以进行针对性攻击(例如DDoS攻击),同时,反向代理通常拥有更高的安全性和更多抵御网络攻击的资源。
- 缓存数据:代理服务器可以缓存服务器的响应数据,大大提升请求的速度。
- SSL加密解密:反向代理可以对客户端发出的HTTPS请求进行解密,对服务器发出的http请求进行加密,从而节约目标服务器的资源。
在Go语言中,实现反向代理非常简单,Go语言标准库httputil提供了封装好的反向代理实现方式。

package main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
)
func main() {
//初始化反向代理服务
proxy, _ := NewProxy()
http.HandleFunc("/", ProxyRequestHandler(proxy))
log.Fatal(http.ListenAndServe(":8888", nil))
}
func NewProxy() (*httputil.ReverseProxy, error) {
targetHost := "http://my-api-server.com"
url, _ := url.Parse(targetHost)
proxy := httputil.NewSingleHostReverseProxy(url)
return proxy, nil
}
// 使用代理处理http请求
func ProxyRequestHandler(proxy *httputil.ReverseProxy) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
proxy.ServeHTTP(w, r)
}
}
在这个例子中,NewProxy函数借助了NewSingleHostReverseProxy函数生成一个反向代理服务器。NewSingleHostReverseProxy函数的参数是实际的后端服务器地址,如果后端有多个服务器,如果有多个服务器,那么可以用一些策略来选择某一个合适的后端服务地址,从而实现负载均衡。NewSingleHostReverseProxy内部封装了数据转发等操作,当客户端访问代理服务器时,请求会被转发到对应的目标服务器中。httputil对于反向代理的实现并不复杂,和正向代理的逻辑类似,主要包含了修改客户端的请求、处理特殊请求头、将请求转发到目标服务器,以及将目标服务器的数据转发回客户端等操作。
如何在实际项目中实现代理
我们在开发实际项目时可能使用自己搭建的代理服务器,也可能使用外部付费或免费的代理池。假设我们已经拥有了众多代理服务器地址,客户端应该如何实现对代理的访问呢?这里涉及两个问题,一个是如何访问代理服务器,二是在众多代理服务器中,怎样选择一个最合适的代理地址。
如何访问代理服务器
go的http标准库封装了代理访问的机制。在Transport结构体中,有一个Proxy函数用于返回当前应该使用的代理地址。
如何选择代理地址
代理地址的策略类似于调度策略,调度策略有很多,包括轮询、加权轮询、一致哈希算法等。下面使用轮询调度来实现对代理服务器的访问。
package proxy
import (
"errors"
"net/http"
"net/url"
"sync/atomic"
)
type ProxyFunc func(*http.Request) (*url.URL, error)
type roundRobinSwitcher struct {
proxyURLs []*url.URL
index uint32
}
//取余算法实现轮询调度
func (r *roundRobinSwitcher) GetProxy(pr *http.Request) (*url.URL, error) {
index := atomic.AddUint32(&r.index, 1) - 1
u := r.proxyURLs[index%uint32(len(r.proxyURLs))]
return u, nil
}
// RoundRobinProxySwitcher creates a proxy switcher function which rotates
// ProxyURLs on every request.
// The proxy type is determined by the URL scheme. "http", "https"
// and "socks5" are supported. If the scheme is empty,
// "http" is assumed.
func RoundRobinProxySwitcher(ProxyURLs ...string) (ProxyFunc, error) {
if len(ProxyURLs) < 1 {
return nil, errors.New("Proxy URL list is empty")
}
urls := make([]*url.URL, len(ProxyURLs))
for i, u := range ProxyURLs {
parsedU, err := url.Parse(u)
if err != nil {
return nil, err
}
urls[i] = parsedU
}
return (&roundRobinSwitcher{urls, 0}).GetProxy, nil
}