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

来源:http://www.cnblogs.com/jackyspy/archive/2016/10/29/6011726.html
-Advertisement-
Play Games

本文所討論的網路埠復用並非指網路編程中採用SO_REUSEADDR選項的 Socket Bind 復用。它更像是一個帶特定路由功能的埠轉發工具,在應用層實現。可以在80埠上復用一個SSH服務。 ...


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

背景

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

比如說,你可以在80埠上復用一個SSH服務,普通用戶只知道瀏覽器訪問http://x.x.x.x/ ,而你卻可以用 ssh [email protected] -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和埠值。

func (t *MatchTree) Add(p *PREFIX) {
    for _, patternStr := range p.Patterns {
        pattern := []byte(patternStr)
        node := t.Root
        for i, b := range pattern {
            nodes := node.ChildNodes
            if next_node, ok := nodes[b]; ok {
                node = next_node
                continue
            }

            if nodes == nil {
                nodes = make(map[byte]*MatchTreeNode)
                node.ChildNodes = nodes
            }

            root, leaf := createSubTree(pattern[i+1:])
            leaf.Address = p.Address
            nodes[b] = root

            break
        }
    }
}

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

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

func (p *REGEX) Probe(header []byte) (result ProbeResult, address string) {
    if p.MinLength > 0 && len(header) < p.MinLength {
        return TRYAGAIN, ""
    }
    for _, re := range p.regexpList {
        if re.Match(header) {
            return MATCH, p.Address
        }
    }

    if p.MaxLength > 0 && len(header) >= p.MaxLength {
        return UNMATCH, ""
    }

    return TRYAGAIN, ""
}

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

    case "ssh":
        service = "prefix"
        p = &PREFIX{ps.BaseConfig, []string{"SSH"}}
    case "http":
        service = "prefix"
        p = &PREFIX{ps.BaseConfig, []string{"GET ", "POST ", "PUT ", "DELETE ", "HEAD ", "OPTIONS "}}

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

func (s *MQTT) Probe(header []byte) (result ProbeResult, address string) {
    if header[0] != 0x10 {
        return UNMATCH, ""
    }

    if len(header) < 13 {
        return TRYAGAIN, ""
    }

    i := 1
    for ; ; i++ {
        if header[i]&0x80 == 0 {
            break
        }

        if i == 4 {
            return UNMATCH, ""
        }
    }

    i++

    if bytes.Compare(header[i:i+8], []byte("\x00\x06MQIsdp")) == 0 || bytes.Compare(header[i:i+6], []byte("\x00\x04MQTT")) == 0 {
        return MATCH, s.Address
    }

    return UNMATCH, ""
}

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

{
    "listen": ":80",
    "default": "127.0.0.1:80",
    "timeout": 1,
    "connect_timeout": 1,
    "protocols": [
        {
            "service": "ssh",
            "addr": "127.0.0.1:22"
        },
        {
            "service": "mqtt",
            "addr": "127.0.0.1:1883"
        },
        {
            "name": "custom_http",
            "service": "regex",
            "addr": "127.0.0.1:8080",
            "patterns": [
                "^(GET|POST|PUT|DELETE|HEAD|\\x79PTIONS) "
            ]
        },
        {
            "service": "prefix",
            "addr": "127.0.0.1:8081",
            "patterns": [
                "GET ",
                "POST "
            ]
        }
    ]
}

性能測試

準備工作

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

package main

import (
    "bytes"
    "flag"
    "fmt"
    "log"
    "net/http"
    "regexp"
    "strconv"
    "strings"
)

func defaultHandler(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path == "/" {
        fmt.Fprintln(w, "It works.")
        return
    }

    myHandler(w, r)
}

func myHandler(w http.ResponseWriter, r *http.Request) {
    re := regexp.MustCompile(`^/(\d+)([kKmMgGtT]?)$`)
    match := re.FindStringSubmatch(r.URL.Path)
    if match == nil {
        http.NotFound(w, r)
        return
    }

    buffSize := 20480
    buff := bytes.Repeat([]byte{'X'}, buffSize)

    size, _ := strconv.ParseInt(match[1], 10, 64)
    switch strings.ToLower(match[2]) {
    case "k":
        size *= 1 << 10
    case "m":
        size *= 1 << 20
    case "g":
        size *= 1 << 30
    case "t":
        size *= 1 << 40
    }

    w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
    for buffSize := int64(buffSize); size >= buffSize; size -= buffSize {
        w.Write(buff)
    }
    if size > 0 {
        w.Write(bytes.Repeat([]byte{'X'}, int(size)))
    }

}

func main() {
    portPtr := flag.Int("port", 8080, "監聽埠")

    flag.Parse()

    http.HandleFunc("/", defaultHandler)
    err := http.ListenAndServe(fmt.Sprintf(":%d", *portPtr), nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

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

$ go build test.go
$ ./test -port 9999 &
$ curl localhost:9999/1
X
$ curl localhost:9999/10
XXXXXXXXXX
$ curl -o /dev/null localhost:9999/10g
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 10.0G  100 10.0G    0     0  1437M      0  0:00:07  0:00:07 --:--:-- 1469M

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

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

$ go get -u github.com/rakyll/hey
$ $GOPATH/bin/hey http://localhost:9999/1
......
All requests done.

Summary:
  Total:        0.0223 secs
  Slowest:      0.0182 secs
  Fastest:      0.0002 secs
  Average:      0.0039 secs
  Requests/sec: 8962.9371
  Total data:   200 bytes
  Size/request: 1 bytes
......

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

$ go get github.com/jackyspy/switcher

sslh運行的命令如下:

$ sudo sslh-select -n -p 127.0.0.1:9998 --ssh 127.0.0.1:22 --http 127.0.0.1:9999

測試Switcher採用下麵的配置文件default.cfg:

{
    "listen": ":9997",
    "default": "127.0.0.1:22",
    "timeout": 1,
    "connect_timeout": 1,
    "protocols": [
        {
            "service": "ssh",
            "addr": "127.0.0.1:22"
        },
        {
            "service": "http",
            "addr": "127.0.0.1:9999"
        }
    ]
}

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

$ curl -o /dev/null localhost:9999/10g
$ $GOPATH/bin/hey -n 100000 http://localhost:9999/1

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

開始測試

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

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

# coding=utf-8
from __future__ import print_function
import itertools
from subprocess import check_output


def get_speed(port):
    cmd = 'curl -o /dev/null -s -w %{{speed_download}} localhost:{}/10g'.format(port)  # noqa
    speed = check_output(cmd.split())
    return float(speed)


def test_multi_times(port, times):
    return map(get_speed, itertools.repeat(port, times))


def format_speed(speed):
    return str(int(0.5 + speed / 1024 / 1024))


def main():
    testcases = {
        'Direct': 9999,
        'sslh': 9998,
        'switcher': 9997
    }

    count = 10

    print('| Target | {} | Avg | '.format(
        ' | '.join(str(x) for x in range(1, count + 1))))
    print(' --: '.join('|' * (count + 3)))
    for name, port in testcases.items():
        speed_list = test_multi_times(port, count)
        speed_list.append(sum(speed_list) / len(speed_list))
        print('|{}|{}|'.format(name, '|'.join(map(format_speed, speed_list))))


if __name__ == '__main__':
    main()

運行後得到結果如下(速度單位是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略低,差異不是太大。

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

# coding=utf-8
from __future__ import print_function
import itertools
from subprocess import check_output


def get_speed(url):
    cmd = "hey -n 100000 -c 50 {}  | grep 'Requests/sec'".format(url)  # noqa
    output = check_output(cmd, shell=True)
    return float(output.partition(':')[2])


def test_multi_times(url, times):
    return map(get_speed, itertools.repeat(url, times))


def main():
    testcases = {
        'Direct': 'http://x.x.x.x:9999/1',
        'sslh': 'http://x.x.x.x:9998/1',
        'switcher': 'http://x.x.x.x:9997/1'
    }

    count = 10

    print('| Target | {} | Average | '.format(
        ' | '.join(str(x) for x in range(1, count + 1))))
    print(' --: '.join('|' * (count + 3)))
    for name, port in testcases.items():
        speed_list = test_multi_times(port, count)
        speed_list.append(sum(speed_list) / len(speed_list))
        print('|{}|{}|'.format(name, '|'.join('{:.0f}'.format(x + 0.5)
                                              for x in speed_list)))


if __name__ == '__main__':
    main()

運行後得到結果如下(速度單位是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地址從訪問列表中剔除,從一定程度上加強了伺服器安全。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • [TOC] mdev說明 mdev是busybox自帶的一個簡化版的udev,適合嵌入式應用場合。其具有使用簡單的特點。它的作用就是在 系統啟動 和 熱插拔 或 動態載入驅動程式 時,自動產生驅動程式所需要的節點文件。在以busybox為基礎構建嵌入式linux根文件系統時,使用它時最優的選擇。 m ...
  • ...
  • 創建鏡像 創建鏡像的方法有三種: 基於已有的容器創建 基於本地模板導入 基於dockerfile 基於已有的容器創建 主要使用docker commit 命令,命令格式: docker commit [OPTIONS] CONTAINER [REPOSITORY[:tag]],主要包括: -a ,- ...
  • 為了加快系統運行效率,一般情況下系統會採用緩存技術,將常用信息存放到緩存中,避免頻繁的從資料庫、文件中讀寫,造成系統瓶頸,從而提高響應速度。緩存分為客戶端緩存和伺服器端緩存。 目前隨著系統的擴展,伺服器端緩存一般採取兩級緩存技術,本地緩存和分散式緩存。部分常用、公共或者小數據量的信息保存在分散式緩存 ...
  • 網上收集的這些,感覺還不錯 函數等集合在一個個的單元中(我們稱之為類)。被封裝的對象通常被稱為抽象數據類型。在本文中,我們將詳細學習屬性的特性。 封裝的概念 可以把程式按某種規則分成很多“塊“,塊與塊之間可能會有聯繫,每個塊都有一個可變部分和一個穩定的部分。我們需要把可變的部分和穩定的部分分離出來, ...
  • C 中is與as的區別 在C 中,is是用來判斷類型A能否轉化為類型B,一般用來作有繼承關係的父類與子類是否可以相互轉化的判斷。 而as是將類型A轉化為類型B,一般也是用來做子類與父類的轉換。 如下麵的例子,我們聲明三個類 class A { } class B : A { } class C { ...
  • 題目描述 聲明一個表示時間的類,可以精確表示 年,月,日,小時,分,秒。 聲明一個表示時間的類,可以精確表示 年,月,日,小時,分,秒。 輸入 輸入六個正整數,中間以空格隔開,分別表示年月日,小時,分,秒。 輸入六個正整數,中間以空格隔開,分別表示年月日,小時,分,秒。 輸出 第一行為年月日,中間以 ...
  • 本人為一所農業大學的電腦專業的學生,最近為了完成老師佈置的任務,開始入門java web,但在開發過程中遇到過許許多多的問題,但是令我最頭痛的還是導入外部css一直失效,困擾許久才找出原因。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...