基於簡化SOCKS5協議的代理服務器的設計與實現

信息安全課程的一個作業,讓我們實現一個基於Sock5協議的代理服務器,看完整個實驗要求,這不就是讓我寫一個“VPN”嘛?

之前的實驗都是使用的 python3來實現,所以此次還是基於 Python 來實現這個簡單的“VPN”吧。

0x00 SOCKS5

SOCKS 是一種網絡傳輸協議,主要用於客戶端與外網服務器之間通訊的中間傳遞。SOCKS
是" SOCKetS "的縮寫。

當防火牆後的客戶端要訪問外部的服務器時,就跟 SOCKS 代理服務器連接。這個代理服務器控制客戶端訪問外網的資格,允許的話,就將客戶端的請求發往外部的服務器。這個協議最初由 David Koblas 開發,而後由
NECYing-Da Lee 將其擴展到版本 4 。最新協議是版本 5,與前一版本相比,增加支持 UDP驗證,以及 IPv6。根據 OSI 模型,SOCKS 是會話層的協議,位於表示層與傳輸層之間。

SOCKS工作在比HTTP代理更低的層次:SOCKS使用握手協議來通知代理軟件其客戶端試圖進行的連接SOCKS,然後儘可能透明地進行操作,而常規代理可能會解釋和重寫報頭(例如,使用另一種底層協議,例如FTP;然而,HTTP代理只是將HTTP請求轉發到所需的HTTP服務器)。

雖然HTTP代理有不同的使用模式,CONNECT方法允許轉發TCP連接;然而,SOCKS代理還可以轉發UDP流量和反向代理,而HTTP代理不能。

HTTP代理通常更瞭解HTTP協議,執行更高層次的過濾(雖然通常只用於GETPOST方法,而不用於CONNECT方法)。


0x01 SOCKS建立連接

VPN 就是一個正向代理,反向代理一般用作用戶不可直接訪問內網,但通過代理服務器訪問內網資源的方式,代理服務器就是一個反向代理。

反向代理,對於用戶的感知幾乎沒有,正向代理卻需要我們手動設置,比如常見的代理 IP 及端口

客戶端使用 SOCKS5 協議與代理服務器在建立連接,是如下步驟

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    一、客戶端認證請求
        +----+----------+----------+
        |VER | NMETHODS | METHODS  |
        +----+----------+----------+
        | 1  |    1     |  1~255   |
        +----+----------+----------+
    二、服務端迴應認證
        +----+--------+
        |VER | METHOD |
        +----+--------+
        | 1  |   1    |
        +----+--------+
    三、客戶端連接請求(連接目的網絡)
        +----+-----+-------+------+----------+----------+
        |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
        +----+-----+-------+------+----------+----------+
        | 1  |  1  |   1   |  1   | Variable |    2     |
        +----+-----+-------+------+----------+----------+
    四、服務端迴應連接
        +----+-----+-------+------+----------+----------+
        |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
        +----+-----+-------+------+----------+----------+
        | 1  |  1  |   1   |  1   | Variable |    2     |
        +----+-----+-------+------+----------+----------+

*數字代表字節數
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

*符號含義,可以參考:《HTTP協議和SOCKS5協議》 一文

在建立連接過程中,要確保每步都是正確完成的,如果錯誤就要拋出異常。


0x02 代理服務端代碼實現

基於 ThreadingTCPServer 創建一個多線程服務,同時自己寫一個 DYProxy的類,來實現SOCKS5的連接建立和數據傳遞。

具體代碼和步驟都寫在註釋裏啦!

# -*- coding: utf-8 -*-

import select
import socket
import struct
from socketserver import StreamRequestHandler as Tcp, ThreadingTCPServer

SOCKS_VERSION = 5                           # socks版本

"""
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++

    一、客戶端認證請求
        +----+----------+----------+
        |VER | NMETHODS | METHODS  |
        +----+----------+----------+
        | 1  |    1     |  1~255   |
        +----+----------+----------+
    二、服務端迴應認證
        +----+--------+
        |VER | METHOD |
        +----+--------+
        | 1  |   1    |
        +----+--------+
    三、客戶端連接請求(連接目的網絡)
        +----+-----+-------+------+----------+----------+
        |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
        +----+-----+-------+------+----------+----------+
        | 1  |  1  |   1   |  1   | Variable |    2     |
        +----+-----+-------+------+----------+----------+
    四、服務端迴應連接
        +----+-----+-------+------+----------+----------+
        |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
        +----+-----+-------+------+----------+----------+
        | 1  |  1  |   1   |  1   | Variable |    2     |
        +----+-----+-------+------+----------+----------+

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
"""

class DYProxy(Tcp):
    # 用戶認證 用戶名/密碼
    username = 'dyboy'
    password = '123456'

    def handle(self):
        print("客戶端:", self.client_address, " 請求連接!")
        """
        一、客戶端認證請求
            +----+----------+----------+
            |VER | NMETHODS | METHODS  |
            +----+----------+----------+
            | 1  |    1     |  1~255   |
            +----+----------+----------+
        """
        # 從客戶端讀取並解包兩個字節的數據
        header = self.connection.recv(2)
        VER, NMETHODS = struct.unpack("!BB", header)
        # 設置socks5協議,METHODS字段的數目大於0
        assert VER == SOCKS_VERSION, 'SOCKS版本錯誤'
        
        # 接受支持的方法
        # 無需認證:0x00    用戶名密碼認證:0x02
        # assert NMETHODS > 0
        methods = self.IsAvailable(NMETHODS)
        # 檢查是否支持該方式,不支持則斷開連接
        if 0 not in set(methods):
            self.server.close_request(self.request)
            return
        
        """
        二、服務端迴應認證
            +----+--------+
            |VER | METHOD |
            +----+--------+
            | 1  |   1    |
            +----+--------+
        """
        # 發送協商響應數據包 
        self.connection.sendall(struct.pack("!BB", SOCKS_VERSION, 0))
        
        # 校驗用戶名和密碼
        # if not self.VerifyAuth():
        #    return
        

        """
        三、客戶端連接請求(連接目的網絡)
            +----+-----+-------+------+----------+----------+
            |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
            +----+-----+-------+------+----------+----------+
            | 1  |  1  |   1   |  1   | Variable |    2     |
            +----+-----+-------+------+----------+----------+
        """
        version, cmd, _, address_type = struct.unpack("!BBBB", self.connection.recv(4))
        assert version == SOCKS_VERSION, 'socks版本錯誤'
        if address_type == 1:       # IPv4
            # 轉換IPV4地址字符串(xxx.xxx.xxx.xxx)成爲32位打包的二進制格式(長度爲4個字節的二進制字符串)
            address = socket.inet_ntoa(self.connection.recv(4))
        elif address_type == 3:     # Domain
            domain_length = ord(self.connection.recv(1)[0])
            address = self.connection.recv(domain_length)
        port = struct.unpack('!H', self.connection.recv(2))[0]

        """
        四、服務端迴應連接
            +----+-----+-------+------+----------+----------+
            |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
            +----+-----+-------+------+----------+----------+
            | 1  |  1  |   1   |  1   | Variable |    2     |
            +----+-----+-------+------+----------+----------+
        """
        # 響應,只支持CONNECT請求
        try:
            if cmd == 1:  # CONNECT
                remote = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                remote.connect((address, port))
                bind_address = remote.getsockname()
                print('已建立連接:', address, port)
            else:
                self.server.close_request(self.request)
            addr = struct.unpack("!I", socket.inet_aton(bind_address[0]))[0]
            port = bind_address[1]
            reply = struct.pack("!BBBBIH", SOCKS_VERSION, 0, 0, address_type, addr, port)
        except Exception as err:
            print(err)
            # 響應拒絕連接的錯誤
            reply = self.ReplyFaild(address_type, 5)
        self.connection.sendall(reply)      # 發送回覆包

        # 建立連接成功,開始交換數據
        if reply[1] == 0 and cmd == 1:
            self.ExchangeData(self.connection, remote)
        self.server.close_request(self.request)


    def IsAvailable(self, n):
        """ 
        檢查是否支持該驗證方式 
        """
        methods = []
        for i in range(n):
            methods.append(ord(self.connection.recv(1)))
        return methods


    def VerifyAuth(self):
        """
        校驗用戶名和密碼
        """
        version = ord(self.connection.recv(1))
        assert version == 1
        username_len = ord(self.connection.recv(1))
        username = self.connection.recv(username_len).decode('utf-8')
        password_len = ord(self.connection.recv(1))
        password = self.connection.recv(password_len).decode('utf-8')
        if username == self.username and password == self.password:
            # 驗證成功, status = 0
            response = struct.pack("!BB", version, 0)
            self.connection.sendall(response)
            return True
        # 驗證失敗, status != 0
        response = struct.pack("!BB", version, 0xFF)
        self.connection.sendall(response)
        self.server.close_request(self.request)
        return False


    def ReplyFaild(self, address_type, error_number):
        """ 
        生成連接失敗的回覆包 
        """
        return struct.pack("!BBBBIH", SOCKS_VERSION, error_number, 0, address_type, 0, 0)


    def ExchangeData(self, client, remote):
        """ 
        交換數據 
        """
        while True:
            # 等待數據
            rs, ws, es = select.select([client, remote], [], [])
            if client in rs:
                data = client.recv(4096)
                if remote.send(data) <= 0:
                    break
            if remote in rs:
                data = remote.recv(4096)
                if client.send(data) <= 0:
                    break


if __name__ == '__main__':
    # 服務器上創建一個TCP多線程服務,監聽2019端口
    Server = ThreadingTCPServer(('0.0.0.0', 2019), DYProxy)
    print("**********************************************************")
    print("************************* DYPROXY ************************")
    print("*************************   1.0   ************************")
    print("********************  IP:xxx.xxx.xxx.xxx  ******************")
    print("***********************  PORT:2019  **********************")
    print("**********************************************************")
    Server.serve_forever();

這個是服務端,基本不需要改動,這個直接在服務器上跑起來即可,缺什麼就安裝什麼模塊。

服務器會監聽所有連接到服務器IP端口2019TCP請求。

這個文件並沒有接入用戶賬號密碼認證的,其中給註釋了,因爲認證方法不一樣,涉及的其後返回數據包的方法參數不一樣,所以寫了兩個,大家可以在文末 Github 地址參考。


0x03 如何使用

一個簡單的VPN在服務器上跑起來了,我們該怎麼使用吶?

由於需要客戶端,一個簡單使用 socks 代理的 python 客戶端還是比較好測試的

# -*- coding: utf-8 -*-
# author: DYBOY
# time: 2019-5-18 17:27:49
# desc: 測試使用socks5代理訪問

import socket
import socks
import requests

# 設置代理
socks.set_default_proxy(socks.SOCKS5, "服務器IP", 2019)
# 如果使用賬號密碼驗證,那麼使用下面這行連接方式
# socks.set_default_proxy(socks.SOCKS5, "服務器IP", 2019,username='dyboy', password='123456')
socket.socket = socks.socksocket

# 測試訪問 重慶大學
test_url = 'http://cqu.edu.cn'
html = requests.get(test_url,timeout=8)
html.encoding = 'utf-8'
print(html.text)

運行是可以直接訪問,在命令行下輸出 重慶大學 首頁的 HTML 源碼

帶登錄口令的代理方式

但這不應該是我們想要的,我們想看到花花綠綠的東西,對不對?


0x04 接入瀏覽器

缺少客戶端,Python 本身並沒有什麼可以將 HTML 源碼渲染的模塊,那麼就可以藉助瀏覽器。自己可以寫一個本地客戶端,轉發瀏覽器的流量,似乎過於麻煩,又是一個代理。

藉助 火狐瀏覽器,自帶可以設置 代理服務器的功能,即可實現我們的目的,所以沒必要去寫個客戶端,站在巨人肩膀上瀏覽網頁,哈哈~

設置 基於 SOCKS5 的代理

火狐瀏覽器設置代理.png

開始使用前,服務端的 server.py 得先運!(不運行,客戶端怎麼訪問???)

設置完成即可以代理服務器的身份瀏覽網頁

測試訪問重慶大學(cqu.edu.cn

訪問重慶大學官網.png

百度看看,咱的IP是不是代理服務器的IP吶?

查看IP情況.png

OK,大功告成!回家吃飯?

似乎…


0x05 一些思考

雖然小東寫的比較簡單,其背後還是需要去了解各個模塊的使用,當然最主要還是要知道 SOCKS5 協議連接過程,以及各參數的大致含義。其中留了一些坑,在口令校驗中,瀏覽器不會自動幫我們輸入賬號密碼,所以需要一個插件autoproxy,這個插件內提前設置好,賬號密碼即可,這樣安全性似乎提高了一些。

在比較 ShadowSocks 這個軟件中,我們發現還有一些加密的方式,進一步提高了安全性,這都是需要改進的,小東負責挖坑,代碼上傳 Github。各位路過的大佬,不妨填一填坑?

走開啦,死基佬

最後,歡迎各位大佬、愛學習的同學關注 小東博客

博客:https://blog.dyboy.cn

Github代碼託放:https://github.com/dyboy2017/DYPROXY (歡迎 star

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章