代理开发学习

socks5

socks5位于应用层和传输层之间的会话层,是一种代理协议,Socks5代理协议广泛应用于各种基于TCP/IP的应用层协议。由于几乎所有基于TCP/IP的应用软件都使用socket进行数据通信

socks协议历史悠久,它面世时中国的互联网尚未成型,它是明文传输的,因此它并不是为翻墙而设计的协议。互联网早期,企业内部网络为了保证安全性,都是置于防火墙之后,这样带来的副作用就是访问内部资源会变得很麻烦,socks协议就是为了解决这个问题而诞生的。

scoks5是scoks4的升级版,它并不兼容socks4协议。在socks4的基础上新增UDP转发认证功能

特点:

(1)支持IPv4和IPv6:SOCKS5协议可以同时支持IPv4和IPv6地址,适应不同网络环境的需求。

**(2)用户验证:**SOCKS5支持多种用户验证方式,如用户名/密码认证、GSS-API认证等,增加了连接的安全性。

(3)数据加密:SOCKS5协议可以通过TLS/SSL等加密协议对数据进行加密,保护数据的安全性。

(4)UDP转发:相比于SOCKS4协议,SOCKS5协议支持UDP转发,可以在代理连接中传输UDP数据。

工作过程

浏览器采用socks代理:

  1. 浏览器和socks5代理建立TCP连接

    和上面不同的时,浏览器和服务器之间多了一个中间人,即socks5,因此浏览器需要跟socks5服务器建立一条连接。

  2. socks5==协商阶段==

    在浏览器正式向socks5服务器发起请求之前,双方需要协商,包括协议版本,支持的认证方式等,双方需要协商成功才能进行下一步。协商的细节将会在下一小节详细描述。

  3. socks5请求阶段

    协商成功后,浏览器向socks5代理发起一个请求。请求的内容包括,它要访问的服务器域名或ip,端口等信息。

  4. socks5 relay阶段

    scoks5收到浏览器请求后,解析请求内容,然后向目标服务器建立TCP连接。

  5. 数据传输阶段

    经过上面步骤,我们成功建立了浏览器 –> socks5,socks5–>目标服务器之间的连接。这个阶段浏览器开始把数据传输给scoks5代理,socks5代理把数据转发到目标服务器。

同样的,进行代理开发的时候也要写出协商过程

细节:

我们可以把它的的过程总结为3个阶段,分别为:==握手阶段、请求阶段,Relay阶段==。

握手阶段

握手阶段包含协商和子协商阶段,我们把它拆分为两个分别讨论

协商阶段

浏览器->代理

在这个阶段,客户端向socks5发起请求,内容如下:

+----+----------+----------+
|VER | NMETHODS | METHODS  |
+----+----------+----------+
| 1  |    1     | 1 to 255 |
+----+----------+----------+

#上方的数字表示字节数,下面的表格同理,不再赘述
  1. VER: 协议版本,socks5为0x05
  2. NMETHODS: 支持认证的方法数量
  3. METHODS: 对应NMETHODS,==NMETHODS的值为多少,METHODS就有多少个字节==。RFC预定义了一些值的含义,这里就是说通过什么方法进行认证,内容如下:
  • X’00’ NO AUTHENTICATION REQUIRED
  • X’01’ GSSAPI
  • X’02’ USERNAME/PASSWORD
  • X’03’ to X’7F’ IANA ASSIGNED
  • X’80’ to X’FE’ RESERVED FOR PRIVATE METHODS
  • X’FF’ NO ACCEPTABLE METHODS

如图,05020001: 05->socks5 02->浏览器我支持两种认证方式 0001->拆开成00和01,即支持不认证和GSSAPI认证

image-20240606111910744

代理->浏览器

socks5服务器需要选中一个METHOD返回给浏览器,格式如下:

就是说浏览器告诉socks代理我给出这些认证方式你来选一个你支持的

+----+--------+
|VER | METHOD |
+----+--------+
| 1  |   1    |
+----+--------+

当浏览器收到0x00(NO AUTHENTICATION REQUIRED)时,会跳过认证阶段直接进入请求阶段; 当收到0xFF时,直接断开连接。其他的值进入到对应的认证阶段。

如图,0502: 05->socks5 02->socks我选择了 USERNAME/PASSWORD认证方式,来开始认证吧

image-20240606112108549

认证阶段(也叫子协商)

认证阶段作为协商的一个子流程,它不是必须的。socks5服务器可以决定是否需要认证,如果不需要认证,那么认证阶段会被直接略过。

浏览器->代理

如果需要认证,浏览器向socks5服务器发起一个认证请求,这里以0x02(USERNAME/PASSWORD)的认证方式举例:

+----+------+----------+------+----------+
|VER | ULEN |  UNAME   | PLEN |  PASSWD  |
+----+------+----------+------+----------+
| 1  |  1   | 1 to 255 |  1   | 1 to 255 |
+----+------+----------+------+----------+

VER: 版本,通常为0x01

ULEN: 用户名长度

UNAME: 对应用户名的字节数据

PLEN: 密码长度

PASSWD: 密码对应的数据

如图,这里是16进制数据,不一一解释了

image-20240606112511411

代理->浏览器

socks5服务器收到浏览器的认证请求后,解析内容,验证信息是否合法,然后给客户端响应结果。响应格式如下:

这一步就很简单了,要不就1要不就0

+----+--------+
|VER | STATUS |
+----+--------+
| 1  |   1    |
+----+--------+

STATUS字段如果为0x00表示认证成功,其他的值为认证失败。当客户端收到认证失败的响应后,它将会断开连接。

image-20240606112807146

请求阶段

浏览器->代理

顺利通过协商阶段后,浏览器端向socks5服务器发起请求细节,格式如下:

+----+-----+-------+------+----------+----------+
|VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
+----+-----+-------+------+----------+----------+
| 1  |  1  | X'00' |  1   | Variable |    2     |
+----+-----+-------+------+----------+----------+
  • VER 版本号,socks5的值为0x05
  • CMD
    • 0x01表示CONNECT请求
    • 0x02表示BIND请求
    • 0x03表示UDP转发
  • RSV 保留字段,值为0x00
  • ATYP 目标地址类型,DST.ADDR的数据对应这个字段的类型。
    • 0x01表示IPv4地址,DST.ADDR为4个字节
    • 0x03表示域名,DST.ADDR是一个可变长度的域名
    • 0x04表示IPv6地址,DST.ADDR为16个字节长度
  • DST.ADDR 一个可变长度的值
  • DST.PORT 目标端口,固定2个字节

上面的值中,DST.ADDR是一个变长的数据,它的数据长度根据ATYP的类型决定。~~我们可以通过掐头去尾解析出这部分数据。~~分为下面3种情况:

  • X’01’

    一个4字节的ipv4地址

  • X’03’

    一个可变长度的域名,这种情况下DST.ADDR的第一个字节表示域名长度,剩下部分是域名内容。

  • X’04’

    一个16字节的ipv6地址

    如图

image-20240606113343906

代理->浏览器

socks5服务器收到浏览器端的请求后,需要返回一个响应,结构如下

+----+-----+-------+------+----------+----------+
|VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
+----+-----+-------+------+----------+----------+
| 1  |  1  | X'00' |  1   | Variable |    2     |
+----+-----+-------+------+----------+----------+
  • VER socks版本,这里为0x05
  • REP Relay field,内容取值如下
    • X’00’ succeeded
    • X’01’ general SOCKS server failure
    • X’02’ connection not allowed by ruleset
    • X’03’ Network unreachable
    • X’04’ Host unreachable
    • X’05’ Connection refused
    • X’06’ TTL expired
    • X’07’ Command not supported
    • X’08’ Address type not supported
    • X’09’ to X’FF’ unassigned
  • RSV 保留字段
  • ATYPE 同请求的ATYPE
  • BND.ADDR 服务绑定的地址
  • BND.PORT 服务绑定的端口DST.PORT

针对响应的结构中,BND.ADDRBND.PORT值得特别关注一下,可能有朋友在这里会产生困惑,返回的地址和端口是用来做什么的呢?

代理服务器既充当socks服务器,又充当relay服务器。实际上这两个是可以被拆开的,当我们的socks5 server和relay server不是一体的,就需要告知客户端==relay server的地址==,这个地址就是BND.ADDR和BND.PORT。

当我们的relay server和socks5 server是同一台服务器时,BND.ADDRBND.PORT的值全部为0即可。

Relay阶段

socks5服务器收到请求后,解析内容。如果是UDP请求,服务器直接转发; 如果是TCP请求,服务器向目标服务器建立TCP连接,后续负责把客户端的所有数据转发到目标服务。

golang并发

基础

在 Go 中,应用程序并发处理的部分被称作 goroutines(协程),它可以进行更有效的并发运算。在协程和操作系统线程之间并无一对一的关系:协程是根据一个或多个线程的可用性,映射(多路复用,执行于)在他们之上的;协程调度器在 Go 运行时很好的完成了这个工作。

Go 使用 channels 来同步协程

协程是通过使用关键字 go 调用(或执行)一个函数或者方法来实现的(也可以是匿名或者 lambda 函数)。

用命令行指定使用的核心数量

var numCores = flag.Int("n", 2, "number of CPU cores to use")

in main()
flag.Parse()
runtime.GOMAXPROCS(*numCores)

示例

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("In main()")
    go longWait()
    go shortWait()
    fmt.Println("About to sleep in main()")
    // sleep works with a Duration in nanoseconds (ns) !
    time.Sleep(10 * 1e9)
    fmt.Println("At the end of main()")
}

func longWait() {
    fmt.Println("Beginning longWait()")
    time.Sleep(5 * 1e9) // sleep for 5 seconds
    fmt.Println("End of longWait()")
}

func shortWait() {
    fmt.Println("Beginning shortWait()")
    time.Sleep(2 * 1e9) // sleep for 2 seconds
    fmt.Println("End of shortWait()")
}

输出

In main()
About to sleep in main()
Beginning longWait()
Beginning shortWait()
End of shortWait()
End of longWait()
At the end of main() // after 10s

我们让 main() 函数暂停 10 秒从而确定它会在另外两个协程之后结束。如果不这样(如果我们让 main() 函数停止 4 秒),main() 会提前结束,longWait() 则无法完成。如果我们不在 main() 中等待,协程会随着程序的结束而消亡。

当 main() 函数返回的时候,程序退出:它不会等待任何其他非 main 协程的结束。这就是为什么在服务器程序中,每一个请求都会启动一个协程来处理,server() 函数必须保持运行状态。通常使用一个无限循环来达到这样的目的。

另外,协程是独立的处理单元,一旦陆续启动一些协程,你无法确定他们是什么时候真正开始执行的。你的代码逻辑必须独立于协程调用的顺序。

协程通信

Go 有一个特殊的类型,通道(channel),==数据通过通道:同一时间只有一个协程可以访问数据:所以不会出现数据竞争==

所有的类型都可以用于通道,空接口 interface{} 也可以。甚至可以(有时非常有用)创建通道的通道。

它是先进先出(FIFO)的结构所以可以保证发送给他们的元素的顺序

通道也是引用类型,所以我们使用 make() 函数来给它分配内存。这里先声明了一个字符串通道 ch1,然后创建了它(实例化):

var ch1 chan string
ch1 = make(chan string)

当然可以更短: ch1 := make(chan string)。

这里我们构建一个 int 通道的通道: chanOfChans := make(chan int)。

或者函数通道:funcChan := chan func()

通信操作符 <-

这个操作符直观的标示了数据的传输:信息按照箭头的方向流动。

流向通道(发送)

  • ch <- int1 表示:用通道 ch 发送变量 int1(双目运算符,中缀 = 发送)

从通道流出(接收),三种方式:

  • int2 = <- ch 表示:变量 int2 从通道 ch(一元运算的前缀操作符,前缀 = 接收)接收数据(获取新值);假设 int2 已经声明过了,如果没有的话可以写成:int2 := <- ch。

  • <- ch 可以单独调用获取通道的(下一个)值,当前值会被丢弃,但是可以用来验证,所以以下代码是合法的:

操作符 <- 也被用来发送和接收,Go 尽管不必要,为了可读性,通道的命名通常以 ch 开头或者包含 chan。通道的发送和接收操作都是自动的:它们通常一气呵成。

此外,golang程序中如果出现死锁,程序在编译阶段就会报错。

通道阻塞

默认情况下,通信是同步且无缓冲的:在有接收者接收数据之前,发送不会结束。可以想象一个无缓冲的通道在没有空间来保存数据的时候:必须要一个接收者准备好接收通道的数据然后发送者可以直接把数据发送给接收者。所以通道的发送 / 接收操作在对方准备好之前是阻塞的:

1)对于同一个通道,发送操作(协程或者函数中的),在接收者准备好之前是阻塞的:如果 ch 中的数据无人接收,就无法再给通道传入其他数据:新的输入无法在通道非空的情况下传入。所以发送操作会等待 ch 再次变为可用状态:就是通道值被接收时(可以传入变量)。

2)对于同一个通道,接收操作是阻塞的(协程或函数中的),直到发送者可用:如果通道中没有数据,接收者就阻塞了。

带缓冲的通道

一个无缓冲通道只能包含 1 个元素,有时显得很局限。我们给通道提供了一个缓存,可以在扩展的 make 命令中设置它的容量,如下:

buf := 100
ch1 := make(chan string, buf)

buf 是通道可以同时容纳的元素(这里是 string)个数

在缓冲满载(缓冲被全部使用)之前,给一个带缓冲的通道发送数据是不会阻塞的,而从通道读取数据也不会阻塞,直到缓冲空了。

缓冲容量和类型无关,所以可以(尽管可能导致危险)给一些通道设置不同的容量,只要他们拥有同样的元素类型。内置的 cap 函数可以返回缓冲区的容量。

==如果容量大于 0,通道就是异步的了==:缓冲满载(发送)或变空(接收)之前通信不会阻塞,元素会按照发送的顺序被接收。如果容量是 0 或者未设置,通信仅在收发双方准备好的情况下才可以成功。

信号量模式

协程通过在通道 ch 中放置一个值来处理结束的信号。main 协程等待 <-ch 直到从中获取到值。

func compute(ch chan int){
    ch <- someComputation() // when it completes, signal on the channel.
}

func main(){
    ch := make(chan int)     // allocate a channel.
    go compute(ch)        // stat something in a goroutines
    doSomethingElseForAWhile()
    result := <- ch
}

协程同步

通道可以被显式的关闭;尽管它们和文件不同:不必每次都关闭。

只有在当需要告诉接收者不会再提供新的值的时候,才需要关闭通道。

==只有发送者需要关闭通道,接收者永远不会需要==。

我们如何在通道的 sendData() 完成的时候发送一个信号,getData() 又如何检测到通道是否关闭或阻塞?

第一个可以通过函数 close(ch) 来完成:这个将通道标记为无法通过发送操作 <- 接受更多的值;给已经关闭的通道发送或者再次关闭都会导致运行时的 panic。在创建一个通道后使用 defer 语句是个不错的办法(类似这种情况):

ch := make(chan float64)
defer close(ch)

第二个问题可以使用逗号,ok 操作符:用来检测通道是否被关闭。

如何来检测通道收到没有被阻塞(或者通道没有被关闭)?

v, ok := <-ch   // ok is true if v received value

通常和 if 语句一起使用:

if v, ok := <-ch; ok {
  process(v)
}

或者在 for 循环中接收的时候,当关闭或者阻塞的时候使用 break:

v, ok := <-ch
if !ok {
  break
}
process(v)

使用 select 切换协程

select 监听进入通道的数据,也可以是用通道发送值的时候。

select {
case u:= <- ch1:
        ...
case v:= <- ch2:
        ...
        ...
default: // no value ready to be received
        ...
}

select 做的就是:选择处理列出的多个通信情况中的一个。

  • 如果都阻塞了,会等待直到其中一个可以处理

  • 如果多个可以处理,随机选择一个

  • 如果没有通道操作可以处理并且写了 default 语句,它就会执行:default 永远是可运行的(这就是准备好了,可以执行)。

golang网络编程

实现简单正向代理

https://blog.csdn.net/m0_63230155/article/details/131621640

https://segmentfault.com/a/1190000038247560

轮廓

用 Go 实现一个 TCP Server 实在是太简单了,什么 c10k problem、select、poll、epoll、kqueue、iocp、libevent,通通不需要(但为了通过面试你还是得去看呀),只需要这样两步:

  • 监听端口 1080(socks5的默认端口)
  • 每收到一个请求,启动一个 goroutine 来处理它

搭起这样一个架子,实现一个 Hello world,大约需要 30 行代码:

func main() {
  server, err := net.Listen("tcp", ":1080")
  if err != nil {
    fmt.Printf("Listen failed: %v\n", err)
    return
  }

  for {
    client, err := server.Accept()
    if err != nil {
      fmt.Printf("Accept failed: %v", err)
      continue
    }
    go process(client)
  }
}

func process(client net.Conn) {
  remoteAddr := client.RemoteAddr().String()
  fmt.Printf("Connection from %s\n", remoteAddr)
  client.Write([]byte("Hello world!\n"))
  client.Close()
}

image-20240606161128627

image-20240606161118172

我们只需 16 行就能把 socks5 的架子搭起来:

func process(client net.Conn) {
  if err := Socks5Auth(client); err != nil {
    fmt.Println("auth error:", err)
    client.Close()
    return
  }

  target, err := Socks5Connect(client)
  if err != nil {
    fmt.Println("connect error:", err)
    client.Close()
    return
  }

  Socks5Forward(client, target)
}

只要把 Socks5Auth、Socks5Connect 和 Socks5Forward 给补上,一个完整的 socks5 代理就完成啦

AUth 握手阶段-协商阶段

+----+----------+----------+
|VER | NMETHODS | METHODS  |
+----+----------+----------+
| 1  |    1     | 1 to 255 |
+----+----------+----------+

浏览器端->代理端

func Socks5Auth(client net.Conn) (err error) {
  buf := make([]byte, 256)

  // 读取 VER 和 NMETHODS
  n, err := io.ReadFull(client, buf[:2])
  if n != 2 {
    return errors.New("reading header: " + err.Error())
  }

  ver, nMethods := int(buf[0]), int(buf[1])
  if ver != 5 {
    return errors.New("invalid version")
  }

  // 读取 METHODS 列表
  n, err = io.ReadFull(client, buf[:nMethods])
  if n != nMethods {
    return errors.New("reading methods: " + err.Error())
  }

  //TO BE CONTINUED...

代理端->浏览器端

就是告诉浏览器,我选择了哪种认证方式

  //无需认证
  n, err = client.Write([]byte{0x05, 0x00})
  if n != 2 || err != nil {
    return errors.New("write rsp err: " + err.Error())
  }

  return nil
}

当浏览器收到0x00(NO AUTHENTICATION REQUIRED)时,会跳过认证阶段直接进入请求阶段;

所以说这里不需要进行握手阶段-认证阶段了

Connect 请求阶段

这边浏览器的socks包已经发过来了,格式如下

+----+-----+-------+------+----------+----------+
|VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
+----+-----+-------+------+----------+----------+
| 1  |  1  | X'00' |  1   | Variable |    2     |
+----+-----+-------+------+----------+----------+

我们直接处理

func Socks5Connect(client net.Conn) (net.Conn, error) {
  buf := make([]byte, 256)

  n, err := io.ReadFull(client, buf[:4])
  if n != 4 {
    return nil, errors.New("read header: " + err.Error())
  }

  ver, cmd, _, atyp := buf[0], buf[1], buf[2], buf[3]
  if ver != 5 || cmd != 1 {
    return nil, errors.New("invalid ver/cmd")
  }

  //TO BE CONTINUED...

可见这就是一个简单的socks代理,限定死了cmd、socks版本等,上面是检查浏览器发来的socks包是否符合我们的通讯要求,然后要给浏览器作出响应

继续解析

ATYP 目标地址类型,这里只实现了ipv4和域名

  addr := ""
  switch atyp {
  case 1:
    n, err = io.ReadFull(client, buf[:4])
    if n != 4 {
      return nil, errors.New("invalid IPv4: " + err.Error())
    }
    addr = fmt.Sprintf("%d.%d.%d.%d", buf[0], buf[1], buf[2], buf[3])

  case 3:
    n, err = io.ReadFull(client, buf[:1])
    if n != 1 {
      return nil, errors.New("invalid hostname: " + err.Error())
    }
    addrLen := int(buf[0])

    n, err = io.ReadFull(client, buf[:addrLen])
    if n != addrLen {
      return nil, errors.New("invalid hostname: " + err.Error())
    }
    addr = string(buf[:addrLen])

  case 4:
    return nil, errors.New("IPv6: no supported yet")

  default:
    return nil, errors.New("invalid atyp")
  }

既然 ADDR 和 PORT 都就位了,我们马上创建一个到 dst 的连接:

 destAddrPort := fmt.Sprintf("%s:%d", addr, port)
 dest, err := net.Dial("tcp", destAddrPort)
 if err != nil {
   return nil, errors.New("dial dst: " + err.Error())
 }

代理还要给浏览器发消息,结构如下

+----+-----+-------+------+----------+----------+
|VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
+----+-----+-------+------+----------+----------+
| 1  |  1  | X'00' |  1   | Variable |    2     |
+----+-----+-------+------+----------+----------+

这句话再来一次:

代理服务器既充当socks服务器,又充当relay服务器。实际上这两个是可以被拆开的,当我们的socks5 server和relay server不是一体的,就需要告知客户端==relay server的地址==,这个地址就是BND.ADDR和BND.PORT。

当我们的relay server和socks5 server是同一台服务器时,BND.ADDRBND.PORT的值全部为0即可。

  n, err = client.Write([]byte{0x05, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0})
  if err != nil {
  dest.Close()
    return nil, errors.New("write rsp: " + err.Error())
  }
  return dest, nil
}

这里就直接用0了,这样这个代理只能在本地使用

Forward

万事俱备,剩下的事情就是转发、转发、转发。

所谓“转发”,其实就是从一头读,往另一头写。

需要注意的是,由于 TCP 连接是双工通信,我们需要创建两个 goroutine,用于完成“双工转发”。

由于 golang 有一个 io.Copy 用来做转发的事情,代码只要 9 行,简单到难以形容:

func Socks5Forward(client, target net.Conn) {
  forward := func(src, dest net.Conn) {
    defer src.Close()
    defer dest.Close()
    io.Copy(src, dest)
  }
  go forward(client, target)
  go forward(target, client)
}

注意:在发送完以后需要关闭连接。

实现反向代理

https://blog.5a6c.me/posts/build-a-reverse-proxy-use-golang/

image-20240611101608991

agent端模拟内网主机,主动向外网的server端发起socks5连接,user也向server端发起连接,server 端监听两个端口, 其中一个是面向用户使用的socks5服务端口, 另一个是与 agent 通信的端口,然后server端把这两个连接怼到一起,实现user和agent的通信

agent

这里使用了这个 socks5 的库(https://github.com/armon/go-socks5), 因为它有一个比较方便的ServeConn方法。在TCP连接建立之后,就将连接交给处理 socks5 协议的库去处理

package main

import (
	"fmt"
	"net"
	"time"

	"github.com/armon/go-socks5"
)

var server *socks5. Server 

func main() {
	// 起一个简单的 socks5 服务
	var err error
	 server , err = socks5.New(&socks5.Config{})
	if err != nil {
		panic(err)
	}
	// 不断向 server 发起连接请求, server 的连接池满了之后, 会阻塞在 dial 这一步
	for {
		conn, err := net.Dial("tcp", "127.0.0.1:8989")
		if err != nil {
			continue
		}
		// 连接成功之后, 使用 socks5 库处理该连接
		go handleSocks5(conn)
	}
}

func handleSocks5(conn net.Conn) {
	defer conn.Close()
	_ = conn.SetDeadline(time.Time{})
	// 使用该 socks5 库提供的 ServeConn 方法
	err :=  server .ServeConn(conn)
	if err != nil {
		fmt.Println(err)
	}
}

server

server 这边的实现也很简单首先监听两个端口,一个供 user 连接使用,另一个供 agent 回连使用。 在 agent 成功回连之后,再取一条 user 的连接,调用 golang 的 io.Copy 方法,将两个连接的输入输出互相复制,即可将流量转发到 agent 进行处理。

package main

import (
	"fmt"
	"io"
	"net"
	"sync"
	"time"
)

func main() {
	// 使用两个 channel 来暂存 agent 和 user 的连接请求
	userConnChan := make(chan net.Conn, 10)
	 agent ConnChan := make(chan net.Conn, 10)
	// 监听 agent 服务端口
	go ListenService( agent ConnChan, "127.0.0.1:8989")
	// 监听 user 服务端口
	go ListenService(userConnChan, "127.0.0.1:1080")
	for  agent Conn := range  agent ConnChan {
		userConn := <-userConnChan
		go copyConn(userConn,  agent Conn)
	}
}

func ListenService(c chan net.Conn, ListenAddress string) {
	listener, err := net.Listen("tcp", ListenAddress)
	if err != nil {
		panic(err)
	}
	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println(err)
			continue
		}
		c <- conn
	}
}

func copyConn(srcConn, dstConn net.Conn) {
	_ = srcConn.SetDeadline(time.Time{})
	_ = dstConn.SetDeadline(time.Time{})
	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		defer srcConn.Close()
		defer dstConn.Close()
		_, err := io.Copy(srcConn, dstConn)
		if err != nil {
			return
		}
	}()
	go func() {
		defer wg.Done()
		defer dstConn.Close()
		defer srcConn.Close()
		_, err := io.Copy(dstConn, srcConn)
		if err != nil {
			return
		}
	}()
	wg.Wait()
}

代理池

代理理解的差不多了,代理池怎么实现呢?

代理池应该说已经是比较成熟的技术,而且在飞速发展,比如现在主流的“秒拨”技术,代理池技术目前被广泛用于爬虫、灰黑产、SEO、网络攻击、刷单、薅羊毛等等领域。

在github上有许多搭建代理池的项目,原理一般是==通过爬取各个免费的代理资源中的代理IP信息,存储到本地,进行可用性验证,维护可用代理IP列表,然后在需要使用的时候取出代理IP==。

免费的资源最大的问题就是代理IP不可用、不稳定,测试下来,虽然能爬取几千个代理IP,但真正用的可能在10个以内,利用率在1%左右。 另外就是付费模式,这里国内也有很多的服务提供商,现在提供的服务模式多种多样,长期的,包月的,甚至可以按次服务。一般来说代理的匿名度越高,时效越长,价格也越贵。

攻防

自从企业开始在IP层面根据一些阻断的规则(如设定单位时间内IP的访问次数阈值、限制触发特定行为的IP等)起,攻防的较量就开始了。为了绕过企业的IP阻断规则,早期黑产获取代理IP的方式主要利用高性能的服务器对全网进行扫描,扫描开放代理服务的服务器,或者是直接爬取其他代理网站的数据,收录有效代理IP和端口。全网的代理IP数量相对有限,稳定性也堪忧,而通过VPN的方式成本又太高。同时,不少甲方也慢慢开始积累代理IP威胁情报信息,进一步打压了黑产使用代理IP的效果。

攻防升级,黑产迅速研发出“秒拨”技术。通俗的讲,秒拨的底层思路就是利用国内家用宽带拨号上网(PPPoE)的原理,每一次断线重连就会获取一个新的IP。黑产掌握大量宽带线路资源,部署自动断线重连切换IP以及攻击的工具后,便可发起攻击。

现在很多付费的代理池资源,都是利用的“秒拨”技术,“秒拨”对基于ip的安全防护和风控措施构成了严峻的挑战:一方面秒拨ip数量巨大,因为地区级的宽带ip池往往有十万甚至百万级别的ip数;另一方面,秒拨ip可做到秒级切换,且与正常用户共用ip池,使用者可能在极短时间内从正常用户变为黑灰产,导致识别难度较大,而且一旦大规模封禁,很有可能造成误伤,引起客户投诉。

攻击方如果采用代理池技术,可能会对防守方产生两个极端影响:

1、如果攻击方将攻击流量分散在大量IP中,极端情况下每一个ip一次攻击,SIEM这类的综合分析系统可能根本不会产生告警,因为攻击没有达到告警阈值,只能人工去分析各类安全告警日志,发现可能异常,类似于威胁狩猎。

2、攻击者利用海量代理IP产生海量攻击,则防守方也会产生海量告警和海量阻断IP的请求,造成Alert DOS和Block DOS,一方面消耗防守大量的精力进行分析,另一方面甚至造成封禁IP的个数超过防守方设备上限,使其防御体系失效,而此时再进行针对性的攻击,就可以达到声东击西的效果。

总之,以“秒拨”为代表的代理池技术已然成为当下安全行业的痛点之一,仅仅依靠威胁情报、网络层封IP的方式已经难以应对,需要重点从应用层面去解决。对于这个痛点,行业内也在积极探索解决方案,出现了“动态防御”的理念,通过动态封装、动态混淆,动态令牌和动态验证来有效进行人机识别。

实现

去网上找了一些资料,知道代理池的实现大概有这些部分:

  • 通过免费代理网站API爬取代理IP
  • 检测IP可用性
  • 持久化存储
  • 代理池的外部接口

而我要做的应该还要再套一层,就是上面写的golang实现的socks代理,我本地代理到上面说的正向代理,然后工具内部去生成、维护一个代理池

也就是说,上面实现的socks代理的最后一步 :Forward需要变,原本是直接转出去,现在是转给我们代理池的IP

第三方代理池的使用参考这个:https://www.zdaye.com/doc/api/ShortProxy_getip

实现参考这个https://studygolang.com/articles/12691

优化

解读代码

这个项目是对go-socks5的二开,我要解决的问题有

  • go-socks5的使用
  • 二开实现了那些功能,具体去读二开的代码

image-20240611105918940

pool.go

实现了一个代理池管理系统,主要用于管理和维护一组代理服务器。

dialer.go

基于代理池(ProxyPool)实现的自定义拨号器(ProxyDialer),主要目的是通过代理池中的代理服务器连接到目标地址。

get_proxy.gp

用于与一个代理服务(proxy.qg.net)进行交互,添加IP到白名单以及获取代理列表。使用了一个名为 request 的库来处理 HTTP 请求和响应。

qgnet_test.go

0%