惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

U
Unit 42
C
Cybersecurity and Infrastructure Security Agency CISA
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Know Your Adversary
Know Your Adversary
S
Securelist
I
Intezer
AWS News Blog
AWS News Blog
L
LINUX DO - 热门话题
P
Privacy International News Feed
Recent Announcements
Recent Announcements
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
博客园 - 聂微东
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Attack and Defense Labs
Attack and Defense Labs
N
News and Events Feed by Topic
The GitHub Blog
The GitHub Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
Schneier on Security
Schneier on Security
N
Netflix TechBlog - Medium
爱范儿
爱范儿
B
Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
C
CERT Recently Published Vulnerability Notes
Hacker News: Ask HN
Hacker News: Ask HN
Google DeepMind News
Google DeepMind News
Engineering at Meta
Engineering at Meta
Blog — PlanetScale
Blog — PlanetScale
WordPress大学
WordPress大学
S
Secure Thoughts
K
Kaspersky official blog
N
News | PayPal Newsroom
O
OpenAI News
Last Week in AI
Last Week in AI
C
Check Point Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Cyberwarzone
Cyberwarzone
Application and Cybersecurity Blog
Application and Cybersecurity Blog
T
Tor Project blog
大猫的无限游戏
大猫的无限游戏
Vercel News
Vercel News
D
Docker
Hugging Face - Blog
Hugging Face - Blog
T
Threat Research - Cisco Blogs
Cisco Talos Blog
Cisco Talos Blog
The Register - Security
The Register - Security
博客园 - 司徒正美
Martin Fowler
Martin Fowler
人人都是产品经理
人人都是产品经理
P
Palo Alto Networks Blog

程序员笔记(huoyijie.cn)

generate new dataset with llm and knowledge graph resume deepseek applications Implement the classic huarongdao game with react Implement the classic tetris game with react Writing admin dashboard template with Nextjs How to AuthN user with OpenLDAP share whiteboard with websocket ocr overview tech notes subscribe topic with sse multi devices github an algorithm used for image text detect an algorithm used for image text detect facial expression recognition technology based on neural network oauth2 mysql store a real time collaborative whiteboard with infinite space How to perform load testing with Grafana k6 fix split of undefined with gitbook theme nodejs http proxy middleware analyze referer of express website writing a CDN origin server with Django a personal lightweight CMS tool writing in Golang profile introduce the post processing introduce the text detect method blockchain for dummies medical applications on ai facial expression recognition study facial expression recognition auth auth with oauth2 auth with jwt chat with sse react chat with websocket react http message signatures infinite whiteboard with socket.io multi factor auth with totp subscribe topic with sse capture screen to gif Golang 经典入门实践教程 · GitBook EFS 加密文件系统 · GitBook 弈杰围棋智能终端 · GitBook 如何把手机触摸屏变成触控板(Touchpad) · GitBook 基于 SOCKET.IO 搭建个人电脑 HTTP 静态服务器代理 · GitBook 微信小程序(蓝牙BLE)远程遥控树莓派小车 · GitBook 语音远程遥控树莓派小车 · GitBook 树莓派的一些使用经验 · GitBook 从零开始制作树莓派小车 · GitBook UDPack 2.0 协议 · GitBook 前言 · GitBook 升级 Ubuntu 到 20.04 LTS 长期维护版本 · GitBook 反向代理服务器实现原理 · GitBook 网站支持 HTTP 2.0 · GitBook 搭建配置邮件服务器 · GitBook 使用 Git 与 Gitbook 创建管理电子书 · GitBook 网站图片视频接入CDN · GitBook a personal lightweight self hosted git server written in Golang 快速搭建CDN回源服务器 · GitBook 基于 Git 搭建代码托管服务器 · GitBook
三百行代码搭建一个简单的 SOCKS5 代理服务器 · GitBook
huoyijie · 2024-05-24 · via 程序员笔记(huoyijie.cn)

SOCKS是一种网络传输协议,主要用于客户端与外网服务器之间通讯的中间传递。当防火墙后的客户端要访问外部的服务器时,就跟SOCKS代理服务器连接。这个代理服务器控制客户端访问外网的资格,允许的话,就将客户端的请求发往外部的服务器。

这个协议最初由David Koblas开发,而后由NEC的Ying-Da Lee将其扩展到SOCKS4。最新协议是SOCKS5,与前一版本相比,增加支持UDP、验证,以及IPv6。根据OSI模型,SOCKS是会话层的协议,位于表示层与传输层之间。

SOCKS工作在比HTTP代理更低的层次:SOCKS使用握手协议来通知代理软件其客户端试图进行的SOCKS连接,然后尽可能透明地进行操作,而常规代理可能会解释和重写报头(例如,使用另一种底层协议,例如FTP;然而,HTTP代理只是将HTTP请求转发到所需的HTTP服务器)。虽然HTTP代理有不同的使用模式,HTTP CONNECT方法允许转发TCP连接;然而,SOCKS代理还可以转发UDP流量(仅SOCKS5),而HTTP代理不能。HTTP代理通常更了解HTTP协议,执行更高层次的过滤(虽然通常只用于GET和POST方法,而不用于CONNECT方法)。

(以上摘自维基百科)

协议部分

SOCKS5 协议是 1996 年发布的,迄今为止已经 25 年了,很多软件内部都支持 SOCKS5 代理。比如浏览器一般都会支持,方便有些用户通过 SOCK5 代理浏览网页。我在开发过程中也使用 CURL 进行过测试,也是支持的。也可以在系统设置里面找到代理设置。在软件开发测试领域,也有很多工具软件支持。需要测试的设备连上代理服务器后,所有流量都会经过相关工具软件,可以方便测试、调试软件。

如通过 Charles 可以查看 HTTP Request/Response 报文信息(非 HTTPS 网站)。可以统计 URL 访问次数、返回延时、数据包大小等非常有用的信息。代理服务器本身不会查看和修改所转发的数据,只是进行简单的转发。甚至不理解转发的内容,现在大部分网站都是基于 HTTPS 加密的,代理服务器没办法了解具体通信内容。当然如果是 HTTP 网站,数据都是明文传输,所有数据都可以看到。

本文主要介绍 SOCKS Protocol Version 5协议,原文比较简单,规定的是应用程序如何与代理服务器进行握手,握手成功后就行流量的正向及反向转发。接下来会先介绍下 SOCKS5 协议,然后通过 Node.js 实现一个简单的代理服务器。

1.协议从协商认证方法开始

/**
 * The client connects to the server, and sends a version identifier/method selection message:
 * +----+----------+----------+
 * |VER | NMETHODS | METHODS  |
 * +----+----------+----------+
 * | 1  |    1     | 1 to 255 |
 * +----+----------+----------+
 */

客户端应用程序连接上服务器,发送协议版本号、客户端支持的认证方法列表给服务器。上面注释中数字代表字节长度,比如 VER 为 1 字节长,后文中出现类似的数字都代表字节长度,不再赘述。

  • VER 字段是 SOCKS 协议版本号,传 X'05',1 字节长
  • NMETHODS 字段是客户端支持的认证方法数量,每种认证方法用 1 字节进行编码,所以也决定了 METHODS 字段的长度
  • METHODS 依次写入支持的认证方法编码

2.服务器根据客户端上报的方法列表选择,回复方法编码

/**
 * The server selects from one of the methods given in METHODS, and sends a METHOD selection message
 * +----+--------+
 * |VER | METHOD |
 * +----+--------+
 * | 1  |   1    |
 * +----+--------+
 */
  • VER 字段是 SOCKS 协议版本号,传 X'05',1 字节长
  • METHOD 字段是服务器选择的认证方法编码, 1 字节长

下面定义了认证方法的编码 X'00' 代表 1 字节长的十六进制数字

  • X'00' NO AUTHENTICATION REQUIRED // 不需要认证
  • X'01' GSSAPI // GSSAPI 认证,在 RFC 1961 里规定
  • X'02' USERNAME/PASSWORD // 用户名密码认证,在 RFC 1929 里规定
  • X'03' to X'7F' IANA ASSIGNED // 未分配
  • X'80' to X'FE' RESERVED FOR PRIVATE METHODS // 保留
  • X'FF' NO ACCEPTABLE METHODS // 服务器发现客户端上报的方法列表都不合适时回复 X'FF'

一般服务器必须实现 GSSAPI 方法,建议实现 USERNAME/PASSWORD。比较安全的环境也可以不用认证。

3.客户端认证成功后发送请求信息

/**
 * The SOCKS request is formed as follows:
 * +----+-----+-------+------+----------+----------+
 * |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
 * +----+-----+-------+------+----------+----------+
 * | 1  |  1  | X'00' |  1   | Variable |    2     |
 * +----+-----+-------+------+----------+----------+
 */
  • VER 字段是 SOCKS 协议版本号,传 X'05',1 字节长
  • CMD 字段是请求命令,1字节长
    • CONNECT X'01' 代表连接目标服务器命令,本文会实现 CONNECT 请求命令
    • BIND X'02' 暂时不介绍
    • UDP ASSOCIATE X'03' 暂时不介绍
  • RSV 保留字节,传 X'00',1字节长
  • ATYP 目标地址类型,1字节长
    • IP V4 address: X'01' IPV4
    • DOMAINNAME: X'03' 域名
    • IP V6 address: X'04' IPV6
  • DST.ADDR 客户端想要请求的目标地址
  • DST.PORT 客户端想连接的目标端口号,2 字节长,网络字节序

DST.ADDR 字段根据地址类型不同,长度不同。

  • 如果IPV4 是固定 4 字节长。
  • 如果是域名,则首个字节代表域名的长度(字节数),接下来的可变长度是域名字符串

4.服务端评估该请求并回复

/**
 * The server evaluates the request, and returns a reply formed as follows:
 * +----+-----+-------+------+----------+----------+
 * |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
 * +----+-----+-------+------+----------+----------+
 * | 1  |  1  | X'00' |  1   | Variable |    2     |
 * +----+-----+-------+------+----------+----------+
 */
  • VER 字段是 SOCKS 协议版本号,传 X'05',1 字节长
  • REP 回复字段:
    • X'00' 成功
    • 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' 请求命令不支持
    • X'08' 地址类型不支持
    • X'09' to X'FF' 未分配
  • RSV 保留字段,传 X'00'
  • ATYP 地址类型
    • IP V4 address: X'01'
    • DOMAINNAME: X'03'
    • IP V6 address: X'04'
  • BND.ADDR 代理服务器连接目标服务器的地址
  • BND.PORT 代理服务器连接目标服务器的端口,2 字节长,网络字节序

服务器可以评估是否允许该请求,如果允许则代表客户端连接目标服务器( DST.ADDR:DST.PORT ),连接成功后回复客户端 REP 为成功,握手过程完成,后面就可以开始代理转发数据了。如果评估不允许该请求或者因为其他原因不能连接上目标服务器,则需返回对应回复信息。

(以上为协议部分)

代码部分

consts.js 常量定义文件

/** socks 5 */
const SOCKS_VERSION = 0x05,

      STATE = {
        METHOD_NEGOTIATION: 0x00,
        AUTHENTICATION: 0x01,
        REQUEST_CONNECT: 0x02,
        PROXY_FORWARD: 0x03
      },

      /**
       * o  X'00' NO AUTHENTICATION REQUIRED
       * o  X'01' GSSAPI
       * o  X'02' USERNAME/PASSWORD
       * o  X'03' to X'7F' IANA ASSIGNED
       * o  X'80' to X'FE' RESERVED FOR PRIVATE METHODS
       * o  X'FF' NO ACCEPTABLE METHODS
       */
      METHODS = {
        NO_AUTH: [0x00, 'no_auth'],
        GSSAPI: [0x01, 'gssapi'],
        USERNAME_PASSWD: [0x02, 'username_password'],
        NO_ACCEPTABLE: [0xFF, 'no_acceptable_methods'],

        get(method) {
          switch(method) {
            case this.NO_AUTH[0]:
              return this.NO_AUTH
            case this.GSSAPI[0]:
              return this.GSSAPI
            case this.USERNAME_PASSWD[0]:
              return this.USERNAME_PASSWD
          }
          console.error(`method [${method}] is not supported`)
          return false
        }
      },

      /**
       * o  CONNECT X'01'
       * o  BIND X'02'
       * o  UDP ASSOCIATE X'03'
       */
      REQUEST_CMD = {
        CONNECT: [0x01, 'connect'],
        BIND: [0x02, 'bind'],
        UDP_ASSOCIATE: [0x03, 'udp_associate'],

        get(cmd) {
          switch(cmd) {
            case this.CONNECT[0]:
              return this.CONNECT
            case this.BIND[0]:
              return this.BIND
            case this.UDP_ASSOCIATE[0]:
              return this.UDP_ASSOCIATE
          }
          console.error(`cmd [${cmd}] is not supported`)
          return false
        }
      },

      /** reserved byte value */
      RSV = 0x00,

      /**
       * o  IP V4 address: X'01'
       * o  DOMAINNAME: X'03'
       * o  IP V6 address: X'04'
       */
      ATYP = {
        IPV4: [0x01, 'ipv4'],
        FQDN: [0x03, 'domain name'],
        IPV6: [0x04, 'ipv6'],

        get(atyp) {
          switch(atyp) {
            case this.IPV4[0]:
              return this.IPV4
            case this.FQDN[0]:
              return this.FQDN
            case this.IPV6[0]:
              return this.IPV6
          }
          console.error(`atpy [${atyp}] is not supported`)
          return false
        }
      },

      /**
       * o  X'00' succeeded
       * o  X'01' general SOCKS server failure
       * o  X'02' connection not allowed by ruleset
       * o  X'03' Network unreachable
       * o  X'04' Host unreachable
       * o  X'05' Connection refused
       * o  X'06' TTL expired
       * o  X'07' Command not supported
       * o  X'08' Address type not supported
       * o  X'09' to X'FF' unassigned
       */
      REP = {
        SUCCEEDED: [0x00, 'succeeded'],
        GENERAL_FAILURE: [0x01, 'general SOCKS server failure'],
        NOT_ALLOWED: [0x02, 'connection not allowed by ruleset'],
        NETWORK_UNREACHABLE: [0x03, 'Network unreachable'],
        HOST_UNREACHABLE: [0x04, 'Host unreachable'],
        CONNECTION_REFUSED: [0x05, 'Connection refused'],
        TTL_EXPIRED: [0x06, 'TTL expired'],
        COMMAND_NOT_SUPPORTED: [0x07, 'Command not supported'],
        ADDRESS_TYPE_NOT_SUPPORTED: [0x08, 'Address type not supported']
      },

      /**
       * The VER field contains the current version of the subnegotiation, which is X'01'
       * username/password auth version
       */
      USERNAME_PASSWD_AUTH_VERSION = 0x01,

      /**
       * auth status
       */
      AUTH_STATUS = {
        SUCCESS: 0x00,
        FAILURE: 0X01
      }

module.exports = () => {

  return {
    SOCKS_VERSION: SOCKS_VERSION,
    STATE: STATE,
    METHODS: METHODS,
    REQUEST_CMD: REQUEST_CMD,
    RSV: RSV,
    ATYP: ATYP,
    REP: REP,
    USERNAME_PASSWD_AUTH_VERSION, USERNAME_PASSWD_AUTH_VERSION,
    AUTH_STATUS: AUTH_STATUS
  }

}

config.js 程序配置

const consts = require('../consts')(),
      app = {
        port: 3000,
        host: '0.0.0.0',
        auth_method: consts.METHODS.NO_AUTH
      }

module.exports = () => {
  return app
}

如上,代理服务器监听在 3000 端口,采用无认证方式

app.js 程序启动

const net = require('net'),
      server = new net.createServer(),
      config = require('./config')(server),
      Proxy = require('./proxy')(server)

server.listen(config.port, config.host)
      .on('listening', () => {
        console.log(`simple-proxy server listening on ${config.port}`)
      })
      .on('close', () => {
        console.log('simple-proxy server closed')
      })
      .on('error', err => {
        console.error('simple-proxy server throw error', err)
      })
      .on('connection', socket => {
        var proxy = Proxy(socket)
        // data package come in
        socket.on('data', buf => {
          proxy.handle(buf)
        })
        .on('end', () => {
          console.log(`socket ${proxy._session.id} end`)
        })
        .on('close', hadError => {
          console.log(`socket ${proxy._session.id} closed with error ${hadError}`)
        })
        .on('error', err => {
          console.error(`socket ${proxy._session.id} throw error`, err)
        })
        .on('timeout', () => {
          console.log(`socket ${proxy._session.id} timeout`)
        })

      })

可以看到其中依赖了核心 Proxy 类完成代理功能

proxy.js 核心代理实现逻辑

下面代码是核心代理逻辑,也实现了简单的用户名密码认证方式。如果去掉用户名密码认证和注释,核心代码就 300 行左右。Proxy 类主要是实现了 SOCKS5 握手协议,与目标服务器建立连接进行数据转发。每个方法上面都有注释,应该比较容易看懂。

const net = require('net'),
      dns = require('dns'),
      { assert } = require('console'),
      uuid = require('uuid'),

      utils = require('../utils'),
      consts = require('../consts')(),
      config = require('./config')()

function Proxy(socket) {
  return {
    /**
     * proxy socket
     */
    _socket: socket,

    /**
     * session
     */
    _session: {
      id: uuid.v1(),
      buffer: Buffer.alloc(0),
      offset: 0,
      state: consts.STATE.METHOD_NEGOTIATION
    },

    /**
     * The client connects to the server, and sends a version identifier/method selection message:
     * +----+----------+----------+
     * |VER | NMETHODS | METHODS  |
     * +----+----------+----------+
     * | 1  |    1     | 1 to 255 |
     * +----+----------+----------+
     */
    parseMethods() {
      let buf = this._session.buffer
      let offset = this._session.offset

      var checkNull = offset => {
        return typeof buf[offset] === undefined
      }

      if(checkNull(offset)) {
        return false
      }
      let socksVersion = buf[offset++]
      assert(socksVersion == consts.SOCKS_VERSION, `socket ${this._session.id} only support socks version 5, got [${socksVersion}]`)
      if(socksVersion != consts.SOCKS_VERSION) {
        this._socket.end()
        return false
      }

      if(checkNull(offset)) {
        return false
      }
      let methodLen = buf[offset++]
      assert(methodLen >= 1 && methodLen <= 255, `socket ${this._session.id} methodLen's value [${methodLen}] is invalid`)

      if(checkNull(offset + methodLen - 1)) {
        return false
      }
      let methods = []
      for(let i = 0; i < methodLen; i++) {
        let method = consts.METHODS.get(buf[offset++])
        if (!!method) {
          methods.push(method)
        }
      }

      console.log(`socket ${this._session.id} SOCKS_VERSION: ${socksVersion}`)
      console.log(`socket ${this._session.id} METHODS: `, methods)

      this._session.offset = offset

      return methods
    },

    /** socks server select auth method */
    selectMethod(methods) {
      let method = consts.METHODS.NO_ACCEPTABLE
      for(let i = 0; i < methods.length; i++) {
        if (methods[i] == config.auth_method) {
          method = config.auth_method
        }
      }

      console.log(`SELECT METHOD [${method}]`)

      this._session.method = method

      return method
    },

    /**
     * The server selects from one of the methods given in METHODS, and sends a METHOD selection message
     * +----+--------+
     * |VER | METHOD |
     * +----+--------+
     * | 1  |   1    |
     * +----+--------+
     * @param {*} method auth method selected
     */
    replyMethod(method) {
      this._socket.write(Buffer.from([consts.SOCKS_VERSION, method[0]]))
    },

    /**
     * This begins with the client producing a Username/Password request:
     * +----+------+----------+------+----------+
     * |VER | ULEN |  UNAME   | PLEN |  PASSWD  |
     * +----+------+----------+------+----------+
     * | 1  |  1   | 1 to 255 |  1   | 1 to 255 |
     * +----+------+----------+------+----------+
     */
    parseUsernamePasswd() {
      let buf = this._session.buffer
      let offset = this._session.offset

      var req = {}

      var checkNull = offset => {
        return typeof buf[offset] === undefined
      }

      if(checkNull(offset)) {
        return false
      }
      let authVersion = buf[offset++]
      assert(authVersion == consts.USERNAME_PASSWD_AUTH_VERSION,
        `socket ${this._session.id} only support auth version ${consts.USERNAME_PASSWD_AUTH_VERSION}, got [${authVersion}]`)
      if(authVersion != consts.USERNAME_PASSWD_AUTH_VERSION) {
        this._socket.end()
        return false
      }

      if(checkNull(offset)) {
        return false
      }
      let uLen = buf[offset++]
      assert(uLen >= 1 && uLen <= 255, `socket ${this._session.id} got wrong ULEN [${uLen}]`)
      if(uLen >= 1 && uLen <= 255) {
        if(checkNull(offset + uLen - 1)) {
          return false
        }
        req.username = buf.slice(offset, offset + uLen).toString('utf8')
        offset += uLen
      } else {
        this._socket.end()
        return false
      }

      if(checkNull(offset)) {
        return false
      }
      let pLen = buf[offset++]
      assert(pLen >= 1 && pLen <= 255, `socket ${this._session.id} got wrong PLEN [${pLen}]`)
      if(pLen >= 1 && pLen <= 255) {
        if(checkNull(offset + pLen - 1)) {
          return false
        }
        req.passwd = buf.slice(offset, offset + pLen).toString('utf8')
        offset += pLen
      } else {
        this._socket.end()
        return false
      }

      this._session.offset = offset

      return req
    },

    /**
     * The server verifies the supplied UNAME and PASSWD, and sends the following response:
     *  +----+--------+
     *  |VER | STATUS |
     *  +----+--------+
     *  | 1  |   1    |
     *  +----+--------+
     */
    replyAuth(succeeded) {
      let reply = [
        consts.USERNAME_PASSWD_AUTH_VERSION,
        succeeded ? consts.AUTH_STATUS.SUCCESS : consts.AUTH_STATUS.FAILURE
      ]
      if (succeeded) {
        this._socket.write(Buffer.from(reply))
      } else {
        this._socket.end(Buffer.from(reply))
      }
    },

    /**
     * The SOCKS request is formed as follows:
     * +----+-----+-------+------+----------+----------+
     * |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
     * +----+-----+-------+------+----------+----------+
     * | 1  |  1  | X'00' |  1   | Variable |    2     |
     * +----+-----+-------+------+----------+----------+
     */
    parseRequests() {
      let buf = this._session.buffer
      let offset = this._session.offset

      let req = {}

      var checkNull = offset => {
        return typeof buf[offset] === undefined
      }

      if(checkNull(offset)) {
        return false
      }
      let socksVersion = buf[offset++]
      assert(socksVersion == consts.SOCKS_VERSION, `socket ${this._session.id} only support socks version 5, got [${socksVersion}]`)
      if(socksVersion != consts.SOCKS_VERSION) {
        this._socket.end()
        return false
      }

      if(checkNull(offset)) {
        return false
      }
      req.cmd = consts.REQUEST_CMD.get(buf[offset++])
      if(!req.cmd || req.cmd != consts.REQUEST_CMD.CONNECT) {
        // 不支持的 cmd || 暂时只支持 connect
        this._socket.end()
        return false
      }

      if(checkNull(offset)) {
        return false
      }
      req.rsv = buf[offset++]
      assert(req.rsv == consts.RSV, `socket ${this._session.id} rsv should be ${consts.RSV}`)

      if(checkNull(offset)) {
        return false
      }
      req.atyp = consts.ATYP.get(buf[offset++])
      if(!req.atyp) {
        // 不支持的 atyp
        this._socket.end()
        return false
      } else if(req.atyp == consts.ATYP.IPV4) {
        let ipLen = 4
        if(checkNull(offset + ipLen - 1)) {
          return false
        }
        req.ip = `${buf[offset++]}.${buf[offset++]}.${buf[offset++]}.${buf[offset++]}`
      } else if(req.atyp == consts.ATYP.FQDN) {
        if(checkNull(offset)) {
          return false
        }
        let domainLen = buf[offset++]
        if(checkNull(offset + domainLen - 1)) {
          return false
        }
        req.domain = buf.slice(offset, offset + domainLen).toString('utf8')
        offset += domainLen
      } else {
        // 其他暂时不支持
        this._socket.end()
        return false
      }

      let portLen = 2
      if(checkNull(offset + portLen - 1)) {
        return false
      }
      req.port = buf.readUInt16BE(offset)
      offset += portLen

      console.log(`socket ${this._session.id} parse requests succeeded`, req)

      this._session.offset = offset

      return req
    },

    /**
     * The server evaluates the request, and returns a reply formed as follows:
     * +----+-----+-------+------+----------+----------+
     * |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
     * +----+-----+-------+------+----------+----------+
     * | 1  |  1  | X'00' |  1   | Variable |    2     |
     * +----+-----+-------+------+----------+----------+
     * @param {*} req client requests
     */
    dstConnect(req) {
      let dstHost = req.domain || req.ip
      dns.lookup(dstHost, { family: 4 }, (err, ip) => {
        if (err || !ip) {
          // failure reply
          let reply = [
            consts.SOCKS_VERSION,
            consts.REP.HOST_UNREACHABLE[0],
            consts.RSV,
            consts.ATYP.IPV4[0]
          ]
          .concat(utils.ipbytes('127.0.0.1')) // ip: 127.0.0.1
          .concat([0x00, 0x00]) // port: 0x0000
          // close connection
          this._socket.end(Buffer.from(reply))
        } else {
          // connect target host
          const dstSocket = net.createConnection({
            port: req.port, // port from client's requests
            host: ip // ip from dns lookup of socks proxy server
          })

          dstSocket.on('connect', () => {
            // success reply
            let bytes = [
              consts.SOCKS_VERSION,
              consts.REP.SUCCEEDED[0],
              consts.RSV,
              consts.ATYP.IPV4[0]
            ]
            // dstSocket.localAddress or default 127.0.0.1
            .concat(utils.ipbytes(dstSocket.localAddress || '127.0.0.1'))
            // default port 0x00
            .concat([0x00, 0x00])

            let reply = Buffer.from(bytes)

            // use dstSocket.localPort override default port 0x0000
            reply.writeUInt16BE(dstSocket.localPort, reply.length - 2)

            this._socket.write(reply)

            // pipe for proxy forward
            this._socket.pipe(dstSocket).pipe(this._socket)
          })
          .on('error', err => {
            console.error(`socket ${this._session.id} -> dstSocket`, err)
          })
          .on('end', () => {
            console.log(`socket ${this._session.id} -> dstSocket end`)
          })
          .on('close', () => {
            console.log(`socket ${this._session.id} -> dstSocket close`)
          })

          // save dstSocket to session
          this._session.dstSocket = dstSocket
        }
      })
    },

    /**
     * called by socket's 'data' event listener
     * @param {Buffer} buf data buffer
     */
    handle(buf) {
      // before proxy forward phase, otherwise do nothing
      if(this._session.state < consts.STATE.PROXY_FORWARD) {
        // append data to session.buffer
        this._session.buffer = Buffer.concat([this._session.buffer, buf])

        // discard processed bytes and move on to the next phase
        const discardProcessedBytes = (nextState) => {
          this._session.buffer = this._session.buffer.slice(this._session.offset)
          this._session.offset = 0
          this._session.state = nextState
        }

        switch(this._session.state) {
          case consts.STATE.METHOD_NEGOTIATION:
            let methods = this.parseMethods()
            if(!!methods) { // read complete data
              let method = this.selectMethod(methods)
              this.replyMethod(method)
              switch(method) {
                case consts.METHODS.USERNAME_PASSWD:
                  discardProcessedBytes(consts.STATE.AUTHENTICATION)
                  break
                case consts.METHODS.NO_AUTH:
                  discardProcessedBytes(consts.STATE.REQUEST_CONNECT)
                  break
                case consts.METHODS.NO_ACCEPTABLE:
                  this._socket.end()
                  break
                default:
                  this._socket.end()
              } 
            }
            break
          // curl www.baidu.com --socks5 127.0.0.1:3000 --socks5-basic --proxy-user  oiuytre:yhntgbrfvedc
          case consts.STATE.AUTHENTICATION:
            // add gssapi support
            // need check this._session.method for parse data
            let userinfo = this.parseUsernamePasswd()
            if(!!userinfo) { // read complete data
              let succeeded = (
                userinfo.username === config.username &&
                userinfo.passwd === config.passwd
              )
              discardProcessedBytes(
                succeeded ? consts.STATE.REQUEST_CONNECT : consts.STATE.AUTHENTICATION
              )
              this.replyAuth(succeeded)
            }
            break
          case consts.STATE.REQUEST_CONNECT:
            let req = this.parseRequests()
            if(!!req) { // read complete data
              this.dstConnect(req)
              discardProcessedBytes(consts.STATE.PROXY_FORWARD)
            }
            break
          case consts.STATE.PROXY_FORWARD:
          default:
            console.log(`handle state [${this._session.state}]`, this._session)
        }
      }
    }
  }
}

module.exports = server => {
  return Proxy
}

启动服务

$ node app.js

此时打开 Chrome 浏览器,安装 SwitchyOmega 插件,配置代理

配置代理

配置完成后,后面所有的请求都是通过 SOCKS5 服务器 127.0.0.1:3000 转发完成的。本文主要内容介绍完了,如果想要一份完整的项目代码,可以与我联系。