如何在Web伺服器80埠上開啟SSH服務

jackyspy發表於2016-11-04

本文所討論的網路埠複用並非指網路程式設計中採用SO_REUSEADDR選項的 Socket Bind 複用。它更像是一個帶特定路由功能的埠轉發工具,在應用層實現。

背景

筆者所處網路中防火牆只開放了一個埠,但卻希望能夠提供多種網路服務用於測試。所以需要尋求一種解決方案,能夠對TCP資料包特徵進行識別,用以實現在一個開放埠上同時提供HTTP/SSH/MQTT等多種服務。

比如說,你可以在80埠上覆用一個SSH服務,普通使用者只知道瀏覽器訪問http://x.x.x.x/ ,而你卻可以用 ssh user@x.x.x.x -p 80 這樣的方式來訪問你的伺服器,這也不失為一種隱藏SSH服務的辦法。

埠複用神器 – sslh

sslh是一款採用C語言編寫的開源埠複用軟體,目前支援 HTTP、SSL、SSH、OpenVPN、tinc、XMPP等多種協議識別。它主要執行於*nix環境,原始碼託管在GitHub上。據官網介紹,Windows系統下可在Cygwin環境中編譯執行,筆者未作測試。

編譯過程並不複雜,直接按照官方文件操作,不在此贅述。Debian使用者可直接通過sudo apt-get install sslh安裝。

編譯生成兩個可執行檔案:sslh-fork 和 sslh-select 。二者的區別在於工作模式的差異:

  • sslh-fork 採用*nix的程式fork模型,為每一個TCP連線fork一個子程式來處理包的轉發。對於長連線而言,無需頻繁建立大量新連線,fork帶來的開銷基本可以忽略。但是如果對像HTTP這樣的短連線請求,採用fork子程式的方式來進行包轉發的話,在出現大量併發請求時,效率會受到一定影響。不過fork模式經過了良好測試,執行起來穩定可靠。
  • sslh-select 採用單執行緒監控管理所有網路連線,是比較新的一種方式。但相對epool等基於事件的I/O機制來說,select的傳統輪詢模式效率還是相對較低的。

sslh支援在配置檔案中使用正規表示式來自定義協議識別規則,但是我在嘗試 MQTT v3.1 協議識別時,出現了問題。當然也有可能是我編寫的正規表示式和它使用的正則庫不匹配。

高效能負載均衡器 – HAProxy

HAProxy是一款開源高效能的 TCP/HTTP 軟體負載均衡器,目前在遊戲後端服務和Web伺服器負載均衡等方面都有著非常廣泛的應用。通過配置,可以實現多種SSL應用複用同一個埠,比如 HTTPS、SSH、OpenVPN等。這裡有一篇參考文件

雖然HAProxy效能卓越,但它不容易通過擴充套件來滿足特定的需求。

為網路而生的現代語言 – Go

Go語言是近幾年我學習研究過的優秀程式語言之一,它的簡潔和高效深深吸引了我(我喜歡簡單的東西,比如Python)。Go語言的goroutine在語言級別提供併發支援,channel又在這些協程之間提供便捷可靠的通訊機制。結合起來,Go語言非常適合編寫高併發的網路應用。之前也打算過用Python+gevent的方式,最後還是考慮到Go語言靜態編譯後的高效率,沒有選擇Python。

在Github上翻騰,找到一個Go語言實現的類sslh專案——Switcher。它很久沒有更新,支援的協議也非常少——實際上它只能識別SSH協議。Switcher的實現非常簡單,核心程式碼不到200行。於是決定在它的基礎上進行改造,實現我所需要的功能。

D——I——Y

到Github上fork了一份Switcher程式碼,在它的基礎上修改。說是修改,其實已面目全非。新的實現中調整了原有架構,去掉對SSH協議的直接支援,轉而採用更加通用的協議識別模式,以求達到可以不通過修改程式而只需簡單配置即可支援大部分協議,讓程式通用性更強一些。

首先最常見的協議匹配模式是根據packet頭幾個位元組對目標協議特徵進行比對。如果只是儲存每個協議的頭N個位元組,不加任何處理逐一比對的話,可能會存在一定的效率問題。一方面,需要對所有pattern進行遍歷,逐個與收到的packet進行比較;另一方面,如果網路延時較大,不能一次性收集到足夠多的位元組,則需要反覆多次比對。舉一個比較極端的例子,假設我有100個目標協議需要比對匹配,pattern大小都在10位元組以上,這時候我通過telnet/netcat連線伺服器,一個位元組一個位元組的傳送資料,則伺服器可能要進行10*100次字串比較。

為了解決這個問題,簡單設計了一個樹形結構,把所有的pattern都以位元組為單位填充到這棵樹上,直至末梢。葉節點上儲存協議對應的目標IP和埠值。

也許是我想太多,在需要比對的協議數量很少的情況下,可能這樣的設計並不能帶來根本上的效率提升。不過我喜歡這種為了可能的效率提升而不斷努力的趕腳 ^_^

相比packet prefix匹配的模式,正規表示式會更加靈活。所以我採取類似sslh的方式,加入了對正規表示式的支援。考慮到效率和具體實現的問題,對正規表示式匹配規則加入了一定的限制,比如需要知道目標字串的最大長度。正規表示式只能在packet buffer達到一定長度要求的情況下逐一匹配。

基於上述兩種簡單的匹配規則,很容易可以構造出ssh、http等常用的協議。在實現中,我加入了一些常用協議的支援,省去使用者自定義的麻煩。

特殊的協議還是需要單獨實現的。比如說,我所需要的MQTT協議就無法通過簡單的字串比對或者正規表示式方式來進行識別。因為它沒有既定的模式,結構也不是固定長度。MQTT協議識別實現如下:

配置檔案採用了json格式,主要是為了方便和靈活。下面是一個示例:

效能測試

準備工作

首先準備一個簡單的Web服務應用。之前用Python+bjoern寫過一個簡易指令碼,用來自己測試網路頻寬,但找了半天沒找著。乾脆用Go語言重新弄了一個,功能是根據傳入引數值N,返回N個字元。

編譯執行,測試Web伺服器執行正常。

類似於上面的演示過程,我用curl下載大檔案來測試網路I/O速率。當然本次測試過程並沒有經過物理網路卡,而是直接通過了loopback介面。這樣可以更客觀的比對在經過代理後,速度下降的幅度。

另外,採用類似Apache的ab壓力測試工具,測試高併發情況下的Web響應速度。這裡,我使用了比ab更變態的boom。它是一款Go語言實現的開源壓測軟體,最近剛更名為hey,主頁上稱因其與Python版壓力測試工具Boom!名稱衝突。安裝和使用都很簡單:

下載安裝我修改過的Switcher版本:

sslh執行的命令如下:

測試Switcher採用下面的配置檔案default.cfg:

測試過程主要用到下面兩條命令,測試sslh和switcher時更改埠號即可。

OK,萬事俱備,只待開測。

開始測試

測試分為兩塊,一是測試大檔案下載速率,為了不受限於網路卡速率,在本機測試。另一塊是測試Web請求併發量,在另一臺電腦上發起測試。

為了減少人工操作量,簡單用Python寫了一段程式碼,用於多次測試速度並輸出結果:

執行後得到結果如下(速度單位是MB/s):

Target 1 2 3 4 5 6 7 8 9 10 Avg
switcher 870 876 924 915 885 928 904 880 909 898 899
sslh 866 865 860 880 865 861 866 863 864 856 865
Direct 1446 1505 1392 1362 1423 1419 1395 1492 1412 1427 1427

可以看出經過代理後,下行速率有明顯下降。其中sslh比switcher略低,差異不是太大。

同樣的,為了方便測試併發請求響應,也寫了一個指令碼來完成:

執行後得到結果如下(速度單位是Requests/s):

Target 1 2 3 4 5 6 7 8 9 10 Average
switcher 14367 14886 15144 14289 15456 14834 14871 14951 14610 14865 14827
sslh 13892 14281 14469 14352 14468 14132 14510 14565 14633 14555 14386
Direct 20494 20110 20558 19519 19467 19891 19777 19682 20737 20396 20063

類似前面的測試,RPS在經過代理後也存在較明顯下降。sslh比switcher略低,差異不大。

更多應用場景 ??

本文描述的網路埠複用,其實現方式本質上還是一個TCP應用代理。基於這一點,我們還可以擴充套件出很多其他的應用場景。

我想到的一種場景是動態IP認證。我們對HTTP和SSH進行復用,預設情況下HTTP可以被所有人訪問,但SSH卻需要通過IP地址認證後才會進行包轉發。跟iptables等防火牆實現的IP地址訪問規則不同,它是在應用層面來進行限制的,具有很強的靈活性,可以通過程式動態增加和刪除。比如說,我通過手機瀏覽器訪問特定的鑑權頁面,通過驗證後,系統自動將我當前在用的公網IP地址加入到訪問列表,然後就能夠順利地通過SSH訪問伺服器了。連線建立後,可以將臨時IP地址從訪問列表中剔除,從一定程度上加強了伺服器安全。

相關文章