Chisel-1.9.1逆向分析

前言

为了写针对Chisel的内网穿透工具的流量报警,这里对该工具进行流量分析和逆向分析

逆向

解析命令行

1
2
3
4
5
6
version := flag.Bool("version", false, "")
v := flag.Bool("v", false, "")
flag.Bool("help", false, "")
flag.Bool("h", false, "")
flag.Usage = func() {}
flag.Parse()

这段代码用于处理命flag包的作用是帮助Go程序处理命令行参数和选项。

flag包的作用是帮助Go程序处理命令行参数和选项。它允许程序员定义各种命令行标志,然后在运行程序时根据传递的参数来设置这些标志的值。这有助于使命令行工具更加灵活,并且能够接受用户提供的参数来控制程序的行为。

一旦使用flag.Parse()解析了命令行参数,程序就可以根据这些参数来执行不同的逻辑。

1
args := flag.Args()

这段代码用于获取在命令行参数之后的非标志(non-flag)参数列表。在Go的flag包中,命令行参数通常分为标志(flags)和非标志参数。

  • 标志(flags)是那些以---开头的参数,通常用于传递配置选项和参数值。例如,-version--verbose
  • 非标志参数是那些没有标志前缀的参数,它们通常用于传递操作对象或其他非配置性参数。这些参数按照它们在命令行中出现的顺序保存在flag.Args()返回的字符串切片中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
subcmd := ""
if len(args) > 0 {
subcmd = args[0]
args = args[1:]
}

switch subcmd {
case "server":
server(args)
case "client":
client(args)
default:
fmt.Print(help)
os.Exit(0)

这段代码首先对第一个参数进行判断是否为server或者client,如果是则将后面的命令行参数当成参数传递给对应的函数,否则打印help字符

server函数

解析命令行

首先解析传入的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
flags := flag.NewFlagSet("server", flag.ContinueOnError)

config := &chserver.Config{}
flags.StringVar(&config.KeySeed, "key", "", "")
flags.StringVar(&config.KeyFile, "keyfile", "", "")
flags.StringVar(&config.AuthFile, "authfile", "", "")
flags.StringVar(&config.Auth, "auth", "", "")
flags.DurationVar(&config.KeepAlive, "keepalive", 25*time.Second, "")
flags.StringVar(&config.Proxy, "proxy", "", "")
flags.StringVar(&config.Proxy, "backend", "", "")
flags.BoolVar(&config.Socks5, "socks5", false, "")
flags.BoolVar(&config.Reverse, "reverse", false, "")
flags.StringVar(&config.TLS.Key, "tls-key", "", "")
flags.StringVar(&config.TLS.Cert, "tls-cert", "", "")
flags.Var(multiFlag{&config.TLS.Domains}, "tls-domain", "")
flags.StringVar(&config.TLS.CA, "tls-ca", "", "")

host := flags.String("host", "", "")
p := flags.String("p", "", "")
port := flags.String("port", "", "")
pid := flags.Bool("pid", false, "")
verbose := flags.Bool("v", false, "")
keyGen := flags.String("keygen", "", "")

flags.Usage = func() {
fmt.Print(serverHelp)
os.Exit(0)
}
flags.Parse(args)

将结构体里面的一些变量与命令行参数进行连接,和本地的一些局部变量进行连接,最后通过flags.parse(arfs)进行解析,Config结构体定义了服务器的一些设置,结构体如下

1
2
3
4
5
6
7
8
9
10
11
12
// Config is the configuration for the chisel service
type Config struct {
KeySeed string
KeyFile string
AuthFile string
Auth string
Proxy string
Socks5 bool
Reverse bool
KeepAlive time.Duration
TLS TLSConfig
}

密钥初始化和解析其他选项

1
2
3
4
5
6
7
8
9
10
11
if *keyGen != "" {
if err := ccrypto.GenerateKeyFile(*keyGen, config.KeySeed); err != nil {
log.Fatal(err)
}
return
}

if config.KeySeed != "" {
log.Print("Option `--key` is deprecated and will be removed in a future version of chisel.")
log.Print("Please use `chisel server --keygen /file/path`, followed by `chisel server --keyfile /file/path` to specify the SSH private key")
}

这段代码的作用是检查用户是否请求生成密钥文件,如果是,则生成密钥文件并退出程序。这通常用于初始化或更新程序所需的密钥文件。

更据后面的输出消息可以知道选项 --key 已弃用,并将在 chisel 的未来版本中删除。请使用chisel server --keygen /file/path,后跟chisel server --keyfile /file/path指定SSH私钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if *host == "" {
*host = os.Getenv("HOST")
}
if *host == "" {
*host = "0.0.0.0"
}
if *port == "" {
*port = *p
}
if *port == "" {
*port = os.Getenv("PORT")
}
if *port == "" {
*port = "8080"
}

设置端口和IP,如果不设置,则默认为0.0.0.0:8080。

1
2
3
4
5
6
7
8
9
if config.KeyFile == "" {
config.KeyFile = settings.Env("KEY_FILE")
} else if config.KeySeed == "" {
config.KeySeed = settings.Env("KEY")
}
s, err := chserver.NewServer(config)
if err != nil {
log.Fatal(err)
}

总结起来,这段代码的目的是在用户没有在命令行参数中指定密钥文件路径 (KeyFile) 或密钥的种子 (KeySeed) 的情况下,尝试从环境变量中获取这些值作为默认值,以便程序在没有显式配置这些参数的情况下仍然能够正常运行。如果环境变量中也没有定义这些值,那么这些字段仍然会保持为空。

然后调用NewServer函数去根据config结构体初始化服务器,最后调用StartContext函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func generatePidFile() {
pid := []byte(strconv.Itoa(os.Getpid()))
if err := ioutil.WriteFile("chisel.pid", pid, 0644); err != nil {
log.Fatal(err)
}
}
/*-------------------------------*/
s.Debug = *verbose
if *pid {
generatePidFile()
}
go cos.GoStats()
ctx := cos.InterruptContext()
if err := s.StartContext(ctx, *host, *port); err != nil {
log.Fatal(err)
}
if err := s.Wait(); err != nil {
log.Fatal(err)
}

总的来说,这段代码的目的是启动Chisel服务器并管理它的运行。它可以处理调试模式、生成PID文件、收集性能统计信息以及处理服务器的启动和关闭。

NewServer函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server := &Server{
config: c,
httpServer: cnet.NewHTTPServer(),
Logger: cio.NewLogger("server"),
sessions: settings.NewUsers(),
}
server.Info = true
server.users = settings.NewUserIndex(server.Logger)
if c.AuthFile != "" {
if err := server.users.LoadUsers(c.AuthFile); err != nil {
return nil, err
}
}
if c.Auth != "" {
u := &settings.User{Addrs: []*regexp.Regexp{settings.UserAllowAll}}
u.Name, u.Pass = settings.ParseAuth(c.Auth)
if u.Name != "" {
server.users.AddUser(u)
}
}

总的来说,这段代码用于初始化Chisel服务器的配置,包括日志记录、用户管理和身份验证配置。它确保服务器在启动时具有必要的配置信息,以便能够接受连接并验证用户身份。

首先判断是否有AuthFileAuth这两个参数,如果有则解析对应的Authfile文件或者Auth所携带的namepass

server := &Server{
    config:     c,
    httpServer: cnet.NewHTTPServer(),
    Logger:     cio.NewLogger("server"),
    sessions:   settings.NewUsers(),
}

这段代码表示实现一个server服务器实例,然后对对应服务进行初始化

  1. server := &Server{...}:创建一个名为 serverServer 结构体实例。这是Chisel服务器的主要配置和运行对象。
  2. server.config = c:将传递给函数的配置 c 赋值给 server 结构体的 config 字段,以便后续使用。
  3. server.httpServer = cnet.NewHTTPServer():创建一个新的 HTTP 服务器实例,并将其分配给 server 结构体的 httpServer 字段。这将用于处理HTTP连接。
  4. server.Logger = cio.NewLogger("server"):创建一个名为 “server” 的日志记录器,并将其分配给 server 结构体的 Logger 字段。这用于记录服务器的日志消息。
  5. server.sessions = settings.NewUsers():创建一个新的用户集合实例,并将其分配给 server 结构体的 sessions 字段。这用于跟踪与服务器建立的会话。
  6. server.Info = true:设置 server 结构体的 Info 字段为 true,表示服务器应该记录详细的信息日志。
  7. server.users = settings.NewUserIndex(server.Logger):创建一个新的用户索引实例,并将其分配给 server 结构体的 users 字段。这用于管理服务器允许连接的用户。

配置ssh密钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var pemBytes []byte
var err error
if c.KeyFile != "" {
var key []byte

if ccrypto.IsChiselKey([]byte(c.KeyFile)) {
key = []byte(c.KeyFile)
} else {
key, err = os.ReadFile(c.KeyFile)
if err != nil {
log.Fatalf("Failed to read key file %s", c.KeyFile)
}
}

pemBytes = key
if ccrypto.IsChiselKey(key) {
pemBytes, err = ccrypto.ChiselKey2PEM(key)
if err != nil {
log.Fatalf("Invalid key %s", string(key))
}
}
} else {
//generate private key (optionally using seed)
pemBytes, err = ccrypto.Seed2PEM(c.KeySeed)
if err != nil {
log.Fatal("Failed to generate key")
}
}

这段代码的主要目的是获取Chisel服务器所需的私钥,并确保它以PEM编码形式可用。如果密钥文件已经包含了PEM编码的密钥,它会直接使用,否则会根据种子生成新的私钥并进行PEM编码。无论哪种情况,最终的私钥都以PEM编码形式存储在 pemBytes 变量中,供服务器使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//convert into ssh.PrivateKey
private, err := ssh.ParsePrivateKey(pemBytes)
if err != nil {
log.Fatal("Failed to parse key")
}
//fingerprint this key,计算 SSH 公钥的 SHA256 哈希值
server.fingerprint = ccrypto.FingerprintKey(private.PublicKey())
//create ssh config
server.sshConfig = &ssh.ServerConfig{
ServerVersion: "SSH-" + chshare.ProtocolVersion + "-server",
PasswordCallback: server.authUser,
}
server.sshConfig.AddHostKey(private)
//setup reverse proxy
if c.Proxy != "" {
u, err := url.Parse(c.Proxy)
if err != nil {
return nil, err
}
if u.Host == "" {
return nil, server.Errorf("Missing protocol (%s)", u)
}
server.reverseProxy = httputil.NewSingleHostReverseProxy(u)
//always use proxy host
server.reverseProxy.Director = func(r *http.Request) {
//enforce origin, keep path
r.URL.Scheme = u.Scheme
r.URL.Host = u.Host
r.Host = u.Host
}
}

总之,这段代码负责配置Chisel服务器的SSH密钥和(如果已配置)反向代理。密钥用于SSH连接的身份验证,而反向代理用于转发传入的连接到其他主机。

  1. private, err := ssh.ParsePrivateKey(pemBytes):将之前生成的PEM编码形式的密钥 pemBytes 解析为SSH私钥。如果解析失败,将记录错误并终止程序。
  2. server.fingerprint = ccrypto.FingerprintKey(private.PublicKey()):计算SSH密钥的指纹(fingerprint)并将其存储在服务器结构体的 fingerprint 字段中。指纹通常用于验证密钥的唯一性。
  3. server.sshConfig = &ssh.ServerConfig{ ... }:创建SSH服务器配置,其中包括以下设置:
    • ServerVersion:指定SSH服务器的版本信息,这里使用Chisel的协议版本。
    • PasswordCallback:指定一个回调函数 server.authUser,用于验证SSH用户的用户名和密码组合。
    • AddHostKey(private):将之前解析的私钥 private 添加为SSH服务器的主机密钥,以用于SSH连接。
  4. if c.Proxy != "" { ... }:检查是否已配置代理。如果已配置代理,执行以下操作:
    • u, err := url.Parse(c.Proxy):解析代理URL,将其存储在 u 变量中。如果解析失败,将记录错误并终止程序。
    • if u.Host == "" { ... }:检查代理URL是否包含主机信息,如果不包含,将返回一个错误消息。
    • server.reverseProxy = httputil.NewSingleHostReverseProxy(u):创建一个反向代理,将传入的HTTP请求代理到指定的URL u
    • server.reverseProxy.Director:配置反向代理的Director函数,用于处理传入请求。此函数将强制设置请求的URL方案和主机,以确保请求正确代理到目标主机。

user-password callback函数

上面出现了校验user和pass的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (s *Server) authUser(c ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
// check if user authentication is enabled and if not, allow all
if s.users.Len() == 0 {
return nil, nil
}
// check the user exists and has matching password
n := c.User()
user, found := s.users.Get(n)
if !found || user.Pass != string(password) {
s.Debugf("Login failed for user: %s", n)
return nil, errors.New("Invalid authentication for username: %s")
}
// insert the user session map
// TODO this should probably have a lock on it given the map isn't thread-safe
s.sessions.Set(string(c.SessionID()), user)
return nil, nil
}

这个函数的作用是验证SSH用户的身份和密码组合。如果用户列表为空,表示用户认证未启用,允许所有连接。否则,它会检查用户名和密码是否匹配,并在成功认证时将用户信息存储在会话映射中。

果用户成功认证,将用户信息插入到用户会话映射中。这个映射用于跟踪用户会话。

1
2
3
4
//print when reverse tunnelling is enabled
if c.Reverse {
server.Infof("Reverse tunnelling enabled")
}

如果有Reverse这个选项,直接打印就行

StartContext函数

接下来看startContext函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
	ctx := cos.InterruptContext()
if err := s.StartContext(ctx, *host, *port); err != nil {
log.Fatal(err)
}
if err := s.Wait(); err != nil {
log.Fatal(err)
}
/*----------------------------------------*/

func (s *Server) StartContext(ctx context.Context, host, port string) error {
s.Infof("Fingerprint %s", s.fingerprint)
if s.users.Len() > 0 {
s.Infof("User authentication enabled")
}
if s.reverseProxy != nil {
s.Infof("Reverse proxy enabled")
}
l, err := s.listener(host, port)
if err != nil {
return err
}
h := http.Handler(http.HandlerFunc(s.handleClientHandler))
if s.Debug {
o := requestlog.DefaultOptions
o.TrustProxy = true
h = requestlog.WrapWith(h, o)
}
return s.httpServer.GoServe(ctx, l, h)
}
/*----------------------------------------*/


func (s *Server) Wait() error {
return s.httpServer.Wait()
}

这三段代码一起协同工作,用于启动和运行服务器以提供服务。以下是它们的详细解释:

第一段代码

  1. cos.InterruptContext() 创建了一个用于处理中断信号(例如Ctrl+C)的上下文对象ctx,这样服务器可以在接收到中断信号时优雅地关闭。
  2. s.StartContext(ctx, *host, *port) 调用了Server结构体的StartContext方法,传递了上下文ctx、主机host和端口port作为参数,开始启动服务器。
  3. 如果启动服务器时出现错误,log.Fatal(err) 将记录错误信息并终止程序。

第二段代码

  1. 这个方法首先记录服务器的指纹信息和是否启用了用户认证和反向代理。
  2. 然后,它通过调用 s.listener(host, port) 创建监听器,该监听器绑定到指定的主机和端口。
  3. 接下来,它创建一个HTTP请求处理程序,将请求传递给handleClientHandler方法进行处理。如果服务器的调试模式已启用,将包装处理程序以记录请求和响应日志。
  4. 最后,它调用s.httpServer.GoServe(ctx, l, h)来启动HTTP服务器,使用传递的上下文ctx、监听器l和HTTP处理程序h。这将使服务器开始监听传入的连接,处理HTTP请求。

第三段代码

  • s.httpServer.Wait() 是等待HTTP服务器关闭的操作。当HTTP服务器启动后,它将一直运行并监听传入连接,直到某个条件满足,例如通过调用服务器的关闭方法或发生错误。
  • s.httpServer.Wait() 返回一个错误,表示等待过程中发生的任何错误。如果没有发生错误,它将返回nil,否则返回一个描述错误的错误对象。

GoServer函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (h *HTTPServer) GoServe(ctx context.Context, l net.Listener, handler http.Handler) error {
if ctx == nil {
return errors.New("ctx must be set")
}
h.waiterMux.Lock()
defer h.waiterMux.Unlock()
h.Handler = handler
h.waiter, ctx = errgroup.WithContext(ctx)
h.waiter.Go(func() error {
return h.Serve(l)
})
go func() {
<-ctx.Done()
h.Close()
}()
return nil
}

总的来说,这段代码的作用是启动HTTP服务器,同时处理HTTP请求,并使用上下文对象来支持启动和关闭操作的管理。它使用了 errgroup 来跟踪HTTP服务器的运行状态和错误。

  • if ctx == nil { return errors.New("ctx must be set") }:首先检查传入的上下文对象 ctx 是否为空,如果为空则返回一个错误,要求上下文对象必须设置。
  • h.waiterMux.Lock()defer h.waiterMux.Unlock():这两行代码用于加锁和解锁 h.waiterMux,这是一个互斥锁,用于保护多个goroutine同时访问 HTTPServer 结构体的相关字段。
  • h.Handler = handler:将传入的 handler 赋值给 HTTPServer 结构体的 Handler 字段,以指定处理HTTP请求的处理程序。
  • h.waiter, ctx = errgroup.WithContext(ctx):使用 errgroup.WithContext 函数创建一个错误组,并将传入的上下文对象 ctx 分配给 h.waiter,以便在启动HTTP服务器时跟踪错误。
  • h.waiter.Go(func() error { return h.Serve(l) }):使用 errgroupGo 方法启动一个新的goroutine,该goroutine将调用 HTTPServer 结构体的 Serve 方法来启动HTTP服务器,并将 l 作为参数传递给 Serve 方法。
  • go func() { <-ctx.Done(); h.Close() }():在新的goroutine中,启动一个匿名函数,该函数在 ctx.Done() 通道关闭时调用 h.Close() 方法来关闭HTTP服务器。
  • return nil:最后,返回nil表示成功启动HTTP服务器。

handleClientHandler函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// handleClientHandler is the main http websocket handler for the chisel server
func (s *Server) handlxeClientHandler(w http.ResponseWriter, r *http.Request) {
//websockets upgrade AND has chisel prefix
upgrade := strings.ToLower(r.Header.Get("Upgrade"))
protocol := r.Header.Get("Sec-WebSocket-Protocol")
if upgrade == "websocket" {
if protocol == chshare.ProtocolVersion {
s.handleWebsocket(w, r)
return
}
//print into server logs and silently fall-through
s.Infof("ignored client connection using protocol '%s', expected '%s'",
protocol, chshare.ProtocolVersion)
}
//proxy target was provided
if s.reverseProxy != nil {
s.reverseProxy.ServeHTTP(w, r)
return
}
//no proxy defined, provide access to health/version checks
switch r.URL.Path {
case "/health":
w.Write([]byte("OK\n"))
return
case "/version":
w.Write([]byte(chshare.BuildVersion))
return
}
//missing :O
w.WriteHeader(404)
w.Write([]byte("Not found"))
}

由注释可以看出handleClientHandlerchisel服务器的主要http websocket处理程序

这段代码的作用是根据 HTTP 请求的内容和头部信息来处理不同类型的请求,包括 WebSocket 连接、反向代理、健康检查和版本信息。

该处理程序负责处理传入的 HTTP 请求,并根据请求的不同内容执行不同的操作。

  • func (s *Server) handleClientHandler(w http.ResponseWriter, r *http.Request):这是 Server 结构体中的一个方法,用于处理传入的 HTTP 请求。它接受两个参数,w 表示 HTTP 响应写入器,r 表示 HTTP 请求对象。
  • upgrade := strings.ToLower(r.Header.Get("Upgrade"))protocol := r.Header.Get("Sec-WebSocket-Protocol"):这两行代码分别从 HTTP 请求头中获取 UpgradeSec-WebSocket-Protocol 字段的值,并将它们转换为小写。这些字段通常用于检测是否存在 WebSocket 连接和 WebSocket 协议版本。
  • if upgrade == "websocket" :这是一个条件语句,用于检查是否存在 WebSocket 升级请求。
  • if protocol == chshare.ProtocolVersion:这是另一个条件语句,用于检查 WebSocket 协议版本是否与预期的版本(chshare.ProtocolVersion)匹配。
  • s.handleWebsocket(w, r):如果升级请求是 WebSocket 且协议版本匹配,那么调用 s.handleWebsocket 方法来处理 WebSocket 连接。
  • s.Infof("ignored client connection using protocol '%s', expected '%s'", protocol, chshare.ProtocolVersion):如果 WebSocket 协议版本不匹配,记录一条信息到服务器日志,指示客户端连接使用了不匹配的协议版本。
  • if s.reverseProxy != nil:这是另一个条件语句,用于检查是否存在反向代理配置。
  • s.reverseProxy.ServeHTTP(w, r):如果存在反向代理配置,则将请求转发给反向代理处理。
  • switch r.URL.Path:这是一个 switch 语句,用于根据请求的 URL 路径执行不同的操作。
  • case "/health"::如果请求的路径是 “/health”,则返回 “OK\n” 表示健康检查通过。
  • case "/version"::如果请求的路径是 “/version”,则返回 chisel 的版本号。
  • default::如果请求的路径不匹配任何已知路径,返回状态码 404 表示未找到资源,并返回 “Not found”。

handleWebsocket函数

在上面的handleClientHandler函数中对于websocket的数据包交给了这个函数进行处理,从函数的注释中也可以看出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
//handleWebsocket负责处理websocket连接
func (s *Server) handleWebsocket(w http.ResponseWriter, req *http.Request) {
id := atomic.AddInt32(&s.sessCount, 1)
l := s.Fork("session#%d", id)
wsConn, err := upgrader.Upgrade(w, req, nil)
if err != nil {
l.Debugf("Failed to upgrade (%s)", err)
return
}
conn := cnet.NewWebSocketConn(wsConn)
//在网络上执行SSH握手
l.Debugf("Handshaking with %s...", req.RemoteAddr)
sshConn, chans, reqs, err := ssh.NewServerConn(conn, s.sshConfig)
if err != nil {
s.Debugf("Failed to handshake (%s)", err)
return
}
//从session map 中提取用户
var user *settings.User
if s.users.Len() > 0 {
sid := string(sshConn.SessionID())
u, ok := s.sessions.Get(sid)
if !ok {
panic("bug in ssh auth handler")
}
user = u
s.sessions.Del(sid)
}
//chisel 服务器握手(与客户端握手相反)
//验证配置
l.Debugf("Verifying configuration")
//等待请求,超时
var r *ssh.Request
select {
case r = <-reqs:
case <-time.After(settings.EnvDuration("CONFIG_TIMEOUT", 10*time.Second)):
l.Debugf("Timeout waiting for configuration")
sshConn.Close()
return
}
failed := func(err error) {
l.Debugf("Failed: %s", err)
r.Reply(false, []byte(err.Error()))
}
if r.Type != "config" {
failed(s.Errorf("expecting config request"))
return
}
c, err := settings.DecodeConfig(r.Payload)
if err != nil {
failed(s.Errorf("invalid config"))
return
}
//如果客户端和服务器版本不匹配,则打印
if c.Version != chshare.BuildVersion {
v := c.Version
if v == "" {
v = "<unknown>"
}
l.Infof("Client version (%s) differs from server version (%s)",
v, chshare.BuildVersion)
}
//验证远程
for _, r := range c.Remotes {
//如果提供了用户,请确保他们有
//访问所需的遥控器
if user != nil {
addr := r.UserAddr()
if !user.HasAccess(addr) {
failed(s.Errorf("access to '%s' denied", addr))
return
}
}
//确认允许反向隧道
if r.Reverse && !s.config.Reverse {
l.Debugf("Denied reverse port forwarding request, please enable --reverse")
failed(s.Errorf("Reverse port forwaring not enabled on server"))
return
}
//确认反向通道可用
if r.Reverse && !r.CanListen() {
failed(s.Errorf("Server cannot listen on %s", r.String()))
return
}
}
//成功验证配置!
r.Reply(true, nil)
//每个ssh连接的隧道
tunnel := tunnel.New(tunnel.Config{
Logger: l,
Inbound: s.config.Reverse,
Outbound: true, //server always accepts outbound
Socks: s.config.Socks5,
KeepAlive: s.config.KeepAlive,
})
//bind
eg, ctx := errgroup.WithContext(req.Context())
eg.Go(func() error {
//已连接,切换ssh连接以供隧道使用,并阻塞
return tunnel.BindSSH(ctx, sshConn, reqs, chans)
})
eg.Go(func() error {
//已连接,设置反向遥控器?
serverInbound := c.Remotes.Reversed(true)
if len(serverInbound) == 0 {
return nil
}
//block
return tunnel.BindRemotes(ctx, serverInbound)
})
err = eg.Wait()
if err != nil && !strings.HasSuffix(err.Error(), "EOF") {
l.Debugf("Closed connection (%s)", err)
} else {
l.Debugf("Closed connection")
}
}

这段代码的主要作用是将 WebSocket 连接升级为 SSH 连接,并根据客户端请求的配置信息进行验证和隧道传输的设置,以便在客户端和服务器之间进行安全通信。

  • func (s *Server) handleWebsocket(w http.ResponseWriter, req *http.Request):这是 Server 结构体中的一个方法,用于处理 WebSocket 连接。它接受两个参数,w 表示 HTTP 响应写入器,req 表示 HTTP 请求对象。
  • id := atomic.AddInt32(&s.sessCount, 1):这一行代码用于为每个 WebSocket 连接生成一个唯一的会话标识符。会话计数器 s.sessCount 是一个原子计数器,每次调用 AddInt32 都会增加其值。
  • l := s.Fork("session#%d", id):这一行代码创建一个新的日志记录器(Logger),用于记录与当前 WebSocket 会话相关的日志。Fork 方法会根据提供的前缀和会话标识符创建一个新的日志记录器。
  • wsConn, err := upgrader.Upgrade(w, req, nil):这一行代码使用 upgrader 将 HTTP 连接升级为 WebSocket 连接,并返回一个 WebSocket 连接对象 wsConn。如果升级失败,将会返回一个错误。
  • conn := cnet.NewWebSocketConn(wsConn):这一行代码创建一个 WebSocketConn 对象,用于封装 WebSocket 连接。
  • l.Debugf("Handshaking with %s...", req.RemoteAddr):这一行代码记录一个调试级别的日志,表示正在进行 WebSocket 连接的握手。
  • sshConn, chans, reqs, err := ssh.NewServerConn(conn, s.sshConfig):这一行代码使用 SSH 包中的 NewServerConn 函数,将 WebSocket 连接升级为 SSH 连接,并返回 SSH 连接对象 sshConn、通道(channel)和请求(request)。
  • var user *settings.User:这一行代码声明了一个变量 user,用于存储连接的用户信息。
  • if s.users.Len() > 0:这是一个条件语句,用于检查是否存在用户验证功能。
  • sid := string(sshConn.SessionID()):这一行代码获取 SSH 连接的会话 ID,用于从会话映射中获取用户信息。
  • u, ok := s.sessions.Get(sid):这一行代码从会话映射中获取与会话 ID 相关联的用户信息。
  • user = u:如果找到与会话 ID 相关联的用户信息,则将其分配给变量 user
  • s.sessions.Del(sid):从会话映射中删除已经使用的会话信息。
  • select 语句:这个 select 语句用于等待 SSH 客户端发送的请求,包括配置请求和其他类型的请求。
  • case r = <-reqs::如果收到了一个请求,将其分配给变量 r
  • case <-time.After(settings.EnvDuration("CONFIG_TIMEOUT", 10*time.Second))::如果在指定的时间内未收到请求,将会触发超时。
  • failed := func(err error):这是一个匿名函数,用于记录连接失败的详细信息,并回复请求。
  • if r.Type != "config":这是一个条件语句,用于检查请求的类型是否为配置请求。
  • c, err := settings.DecodeConfig(r.Payload):这一行代码用于解码客户端发送的配置信息,并将其存储在变量 c 中。
  • if r.Type != "config"c, err := settings.DecodeConfig(r.Payload):如果请求类型不是配置请求或配置信息无效,则触发失败函数,记录失败信息。
  • l.Infof("Client version (%s) differs from server version (%s)", v, chshare.BuildVersion):如果客户端版本与服务器版本不匹配,记录一条信息到服务器日志,指示版本不一致。
  • for _, r := range c.Remotes:这是一个循环,用于迭代客户端发送的所有远程配置项。
  • addr := r.UserAddr():获取远程配置项的地址信息。
  • if !user.HasAccess(addr):如果存在用户验证并且用户没有访问权限,触发失败函数,拒绝访问。
  • if r.Reverse && !s.config.Reverse:如果客户端请求了反向端口转发,但服务器没有启用反向模式,则触发失败函数,拒绝请求。
  • if r.Reverse && !r.CanListen():如果客户端请求的反向端口转发无法在服务器上监听,则触发失败函数,拒绝请求。
  • r.Reply(true, nil):如果所有验证都通过,回复客户端请求,表示验证成功。
  • tunnel := tunnel.New(tunnel.Config{...}):创建一个名为 tunnel 的隧道对象,用于处理 SSH 隧道的传输。
  • eg, ctx := errgroup.WithContext(req.Context()):创建一个 errgroup 对象,用于管理多个并发任务,并传入请求的上下文。
  • eg.Go(func() error { ... }):使用 errgroup 启动一个协程,用于处理 SSH 连接的绑定和隧道的传输。
  • eg.Go(func() error { ... }):启动另一个协程,用于处理反向隧道的绑定(如果存在)。
  • err = eg.Wait():等待所有并发任务完成,如果出现错误(除了正常关闭连接时的 “EOF” 错误),则记录错误日志。

client函数

解析命令行

首先解析传入的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
flags := flag.NewFlagSet("client", flag.ContinueOnError)
config := chclient.Config{Headers: http.Header{}}
flags.StringVar(&config.Fingerprint, "fingerprint", "", "")
flags.StringVar(&config.Auth, "auth", "", "")
flags.DurationVar(&config.KeepAlive, "keepalive", 25*time.Second, "")
flags.IntVar(&config.MaxRetryCount, "max-retry-count", -1, "")
flags.DurationVar(&config.MaxRetryInterval, "max-retry-interval", 0, "")
flags.StringVar(&config.Proxy, "proxy", "", "")
flags.StringVar(&config.TLS.CA, "tls-ca", "", "")
flags.BoolVar(&config.TLS.SkipVerify, "tls-skip-verify", false, "")
flags.StringVar(&config.TLS.Cert, "tls-cert", "", "")
flags.StringVar(&config.TLS.Key, "tls-key", "", "")
flags.Var(&headerFlags{config.Headers}, "header", "")
hostname := flags.String("hostname", "", "")
sni := flags.String("sni", "", "")
pid := flags.Bool("pid", false, "")
verbose := flags.Bool("v", false, "")
flags.Usage = func() {
fmt.Print(clientHelp)
os.Exit(0)
}
flags.Parse(args)
//pull out options, put back remaining args
args = flags.Args()
if len(args) < 2 {
log.Fatalf("A server and least one remote is required")
}
config.Server = args[0]
config.Remotes = args[1:]
//default auth
if config.Auth == "" {
config.Auth = os.Getenv("AUTH")
}
//move hostname onto headers
if *hostname != "" {
config.Headers.Set("Host", *hostname)
config.TLS.ServerName = *hostname
}

if *sni != "" {
config.TLS.ServerName = *sni
}

和server一样的,将结构体里面的一些变量与命令行参数进行连接,和本地的一些局部变量进行连接,最后通过flags.parse(arfs)进行解析,Config结构体定义了服务器的一些设置,但是结构体不一样,结构体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Config represents a client configuration
type Config struct {
Fingerprint string
Auth string
KeepAlive time.Duration
MaxRetryCount int
MaxRetryInterval time.Duration
Server string
Proxy string
Remotes []string
Headers http.Header
TLS TLSConfig
DialContext func(ctx context.Context, network, addr string) (net.Conn, error)
Verbose bool
}

启动客户端初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
c, err := chclient.NewClient(&config)
if err != nil {
log.Fatal(err)
}
c.Debug = *verbose
if *pid {
generatePidFile()
}
go cos.GoStats()
ctx := cos.InterruptContext()
if err := c.Start(ctx); err != nil {
log.Fatal(err)
}
if err := c.Wait(); err != nil {
log.Fatal(err)
}

这段代码用与启动一个client客户端的初始化,根据Config结构体去初始化客户端,然后运行Start函数

NewClient函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// NewClient creates a new client instance
func NewClient(c *Config) (*Client, error) {
//apply default scheme
if !strings.HasPrefix(c.Server, "http") {
c.Server = "http://" + c.Server
}
if c.MaxRetryInterval < time.Second {
c.MaxRetryInterval = 5 * time.Minute
}
u, err := url.Parse(c.Server)
if err != nil {
return nil, err
}
//swap to websockets scheme
u.Scheme = strings.Replace(u.Scheme, "http", "ws", 1)
//apply default port
if !regexp.MustCompile(`:\d+$`).MatchString(u.Host) {
if u.Scheme == "wss" {
u.Host = u.Host + ":443"
} else {
u.Host = u.Host + ":80"
}
}
hasReverse := false
hasSocks := false
hasStdio := false
client := &Client{
Logger: cio.NewLogger("client"),
config: c,
computed: settings.Config{
Version: chshare.BuildVersion,
},
server: u.String(),
tlsConfig: nil,
}
//set default log level
client.Logger.Info = true
//configure tls
if u.Scheme == "wss" {
tc := &tls.Config{}
if c.TLS.ServerName != "" {
tc.ServerName = c.TLS.ServerName
}
//certificate verification config
if c.TLS.SkipVerify {
client.Infof("TLS verification disabled")
tc.InsecureSkipVerify = true
} else if c.TLS.CA != "" {
rootCAs := x509.NewCertPool()
if b, err := ioutil.ReadFile(c.TLS.CA); err != nil {
return nil, fmt.Errorf("Failed to load file: %s", c.TLS.CA)
} else if ok := rootCAs.AppendCertsFromPEM(b); !ok {
return nil, fmt.Errorf("Failed to decode PEM: %s", c.TLS.CA)
} else {
client.Infof("TLS verification using CA %s", c.TLS.CA)
tc.RootCAs = rootCAs
}
}
//provide client cert and key pair for mtls
if c.TLS.Cert != "" && c.TLS.Key != "" {
c, err := tls.LoadX509KeyPair(c.TLS.Cert, c.TLS.Key)
if err != nil {
return nil, fmt.Errorf("Error loading client cert and key pair: %v", err)
}
tc.Certificates = []tls.Certificate{c}
} else if c.TLS.Cert != "" || c.TLS.Key != "" {
return nil, fmt.Errorf("Please specify client BOTH cert and key")
}
client.tlsConfig = tc
}
//validate remotes
for _, s := range c.Remotes {
r, err := settings.DecodeRemote(s)
if err != nil {
return nil, fmt.Errorf("Failed to decode remote '%s': %s", s, err)
}
if r.Socks {
hasSocks = true
}
if r.Reverse {
hasReverse = true
}
if r.Stdio {
if hasStdio {
return nil, errors.New("Only one stdio is allowed")
}
hasStdio = true
}
//confirm non-reverse tunnel is available
if !r.Reverse && !r.Stdio && !r.CanListen() {
return nil, fmt.Errorf("Client cannot listen on %s", r.String())
}
client.computed.Remotes = append(client.computed.Remotes, r)
}
//outbound proxy
if p := c.Proxy; p != "" {
client.proxyURL, err = url.Parse(p)
if err != nil {
return nil, fmt.Errorf("Invalid proxy URL (%s)", err)
}
}
//ssh auth and config
user, pass := settings.ParseAuth(c.Auth)
client.sshConfig = &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{ssh.Password(pass)},
ClientVersion: "SSH-" + chshare.ProtocolVersion + "-client",
HostKeyCallback: client.verifyServer,
Timeout: settings.EnvDuration("SSH_TIMEOUT", 30*time.Second),
}
//prepare client tunnel
client.tunnel = tunnel.New(tunnel.Config{
Logger: client.Logger,
Inbound: true, //client always accepts inbound
Outbound: hasReverse,
Socks: hasReverse && hasSocks,
KeepAlive: client.config.KeepAlive,
})
return client, nil
}

最终,函数返回一个新的Client实例作为结果,如果在创建过程中发生任何错误,将返回相应的错误信息。这个函数的主要作用是根据配置信息创建一个客户端实例,并进行一些配置参数的处理和验证。

  1. func NewClient(c *Config) (*Client, error) { ... }:这是一个名为NewClient的函数,它接受一个*Config类型的参数c,并返回一个指向Client类型的指针和一个错误。这个函数用于创建一个新的客户端实例。
  2. if !strings.HasPrefix(c.Server, "http") { ... }:这个条件语句检查配置中的服务器地址c.Server是否以”http”或”https”开头。如果不是的话,它会自动在地址前添加”http://“,以确保服务器地址的正确格式。
  3. if c.MaxRetryInterval < time.Second { ... }:这个条件语句检查配置中的最大重试间隔c.MaxRetryInterval是否小于1秒。如果小于1秒,它将最大重试间隔设置为5分钟,以确保不会太频繁地进行重试。
  4. u, err := url.Parse(c.Server):这一行代码使用Go标准库中的url.Parse函数来解析服务器地址c.Server,并将结果存储在变量u中。同时,它也会检查是否有解析错误,并将错误存储在变量err中。
  5. u.Scheme = strings.Replace(u.Scheme, "http", "ws", 1):这一行代码将解析得到的URL的协议方案从”http”替换为”ws”,以便后续使用WebSocket连接。
  6. if !regexp.MustCompile(:\d+$).MatchString(u.Host) { ... }:这个条件语句检查解析得到的URL的主机部分是否包含端口号。如果不包含端口号,则根据协议方案添加默认端口号(80或443)。
  7. hasReverse := falsehasSocks := falsehasStdio := false:这些变量用于标记配置中是否包含反向隧道、Socks代理或标准输入/输出通道。
  8. 创建一个Client结构体实例,并设置了各种字段,包括日志记录器、配置信息、服务器地址、TLS配置等。
  9. for _, s := range c.Remotes { ... }:这个循环迭代配置中的远程设置,对每个远程设置进行解码和验证,并根据其类型设置相关标志。
  10. if p := c.Proxy; p != "" { ... }:如果配置中指定了代理服务器地址,则解析代理URL并存储在client.proxyURL中。
  11. user, pass := settings.ParseAuth(c.Auth):这一行代码解析配置中的身份验证信息,将用户名和密码提取出来,然后设置SSH客户端配置。
  12. 创建一个Client结构体实例的tunnel字段,用于处理隧道相关的配置。

start函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Start client and does not block
func (c *Client) Start(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx)
c.stop = cancel
eg, ctx := errgroup.WithContext(ctx)
c.eg = eg
via := ""
if c.proxyURL != nil {
via = " via " + c.proxyURL.String()
}
c.Infof("Connecting to %s%s\n", c.server, via)
//connect to chisel server
eg.Go(func() error {
return c.connectionLoop(ctx)
})
//listen sockets
eg.Go(func() error {
clientInbound := c.computed.Remotes.Reversed(false)
if len(clientInbound) == 0 {
return nil
}
return c.tunnel.BindRemotes(ctx, clientInbound)
})
return nil
}

总的来说,这段代码的作用是启动客户端并尝试连接到chisel服务器,同时监听本地的Socket连接(如果需要)。它使用goroutines来并发执行这些操作,不会阻塞当前线程,并可以在需要时取消。连接和监听操作的详细细节在匿名函数中执行,需要查看匿名函数内部的代码来了解具体的实现。

  1. func (c *Client) Start(ctx context.Context) error { ... }:这是一个Client结构体的方法,用于启动客户端并连接到服务器,但不会阻塞当前线程。它接受一个context.Context类型的参数ctx,并返回一个错误。
  2. ctx, cancel := context.WithCancel(ctx):这一行代码创建了一个新的上下文ctx,并返回一个cancel函数,用于在需要时取消这个上下文。
  3. c.stop = cancel:这一行代码将cancel函数赋值给c结构体的stop字段,以便稍后可以使用它来停止客户端。
  4. eg, ctx := errgroup.WithContext(ctx):这一行代码使用errgroup包创建一个新的errgroup.Group实例,同时基于传入的上下文ctx创建一个新的上下文ctxerrgroup是一个用于处理一组goroutine的包,可以等待它们全部完成或任何一个失败。
  5. via := ""if c.proxyURL != nil { ... }:这些行代码用于创建一个字符串via,其中包含代理服务器的信息,如果c.proxyURL不为nil,则将代理服务器的URL添加到via字符串中。
  6. c.Infof("Connecting to %s%s\n", c.server, via):这一行代码使用c结构体的日志记录器打印连接服务器的信息,包括服务器地址和(如果存在)代理服务器信息。
  7. eg.Go(func() error { ... }):这一行代码启动一个goroutine,其中包含一个匿名函数,用于执行连接到chisel服务器的操作。这个匿名函数返回连接操作的错误(如果有)。
  8. eg.Go(func() error { ... }):这一行代码启动另一个goroutine,其中包含一个匿名函数,用于监听本地的Socket连接。它会检查客户端配置中是否有需要监听的Socket连接,如果有的话,会将这些连接绑定到chisel服务器上。如果没有需要监听的Socket连接,则不执行任何操作。
  9. return nil:最后,这个方法返回nil,表示启动过程不会返回错误。这意味着该方法会立即返回,而不会阻塞。

connectionLoop函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
func (c *Client) connectionLoop(ctx context.Context) error {
//connection loop!
b := &backoff.Backoff{Max: c.config.MaxRetryInterval}
for {
connected, err := c.connectionOnce(ctx)
//reset backoff after successful connections
if connected {
b.Reset()
}
//connection error
attempt := int(b.Attempt())
maxAttempt := c.config.MaxRetryCount
//dont print closed-connection errors
if strings.HasSuffix(err.Error(), "use of closed network connection") {
err = io.EOF
}
//show error message and attempt counts (excluding disconnects)
if err != nil && err != io.EOF {
msg := fmt.Sprintf("Connection error: %s", err)
if attempt > 0 {
maxAttemptVal := fmt.Sprint(maxAttempt)
if maxAttempt < 0 {
maxAttemptVal = "unlimited"
}
msg += fmt.Sprintf(" (Attempt: %d/%s)", attempt, maxAttemptVal)
}
c.Infof(msg)
}
//give up?
if maxAttempt >= 0 && attempt >= maxAttempt {
c.Infof("Give up")
break
}
d := b.Duration()
c.Infof("Retrying in %s...", d)
select {
case <-cos.AfterSignal(d):
continue //retry now
case <-ctx.Done():
c.Infof("Cancelled")
return nil
}
}
c.Close()
return nil
}

这段代码是Client结构体中的一个方法,名为connectionLoop。它的主要作用是在一个循环中尝试连接到chisel服务器,如果连接失败,根据一定策略进行重试,同时记录连接尝试的次数和错误信息。

  1. b := &backoff.Backoff{Max: c.config.MaxRetryInterval}:创建一个backoff.Backoff的实例b,并设置最大的重试间隔为c.config.MaxRetryInterval,这是在客户端配置中定义的最大重试间隔。
  2. for { ... }:这是一个无限循环,表示不断尝试连接到chisel服务器。
  3. connected, err := c.connectionOnce(ctx):调用c.connectionOnce(ctx)方法,该方法尝试一次连接到chisel服务器,并返回两个值,connected表示是否成功连接,err表示连接中出现的错误。
  4. if connected { ... }:如果成功连接到服务器,重置backoff实例b,以便在下一次连接尝试时使用最小的重试间隔。
  5. attempt := int(b.Attempt()):获取当前的连接尝试次数,这是backoff实例的属性。
  6. maxAttempt := c.config.MaxRetryCount:从客户端配置中获取最大重试次数。
  7. 如果err表示了关闭网络连接的错误(”use of closed network connection”),则将err重置为io.EOF,以避免打印此类错误。
  8. 如果err不为空且不是io.EOF,则打印连接错误消息,包括错误信息和连接尝试次数。
  9. 如果达到了最大连接尝试次数,并且maxAttempt大于等于0(表示有限次数的重试),则打印”Give up”消息,表示放弃连接,并退出循环。
  10. 计算下一次重试的等待时间d,使用backoff实例的Duration方法来获取。
  11. 打印”Retrying in %s…”消息,其中%s会被替换为等待时间d的字符串表示。
  12. 使用select语句等待以下两种事件中的任何一个发生:
    • 当前goroutine等待d时间后继续执行,表示进行下一次连接尝试。
    • 当前上下文ctx被取消(可能是由于外部要求取消连接),则打印”Cancelled”消息,并返回nil表示取消连接尝试,然后退出循环。
  13. 最后,如果退出循环,调用c.Close()方法来关闭客户端的连接,然后返回nil

connectionOnce函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// connectionOnce connects to the chisel server and blocks
func (c *Client) connectionOnce(ctx context.Context) (connected bool, err error) {
//already closed?
select {
case <-ctx.Done():
return false, errors.New("Cancelled")
default:
//still open
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
//prepare dialer
d := websocket.Dialer{
HandshakeTimeout: settings.EnvDuration("WS_TIMEOUT", 45*time.Second),
Subprotocols: []string{chshare.ProtocolVersion},
TLSClientConfig: c.tlsConfig,
ReadBufferSize: settings.EnvInt("WS_BUFF_SIZE", 0),
WriteBufferSize: settings.EnvInt("WS_BUFF_SIZE", 0),
NetDialContext: c.config.DialContext,
}
//optional proxy
if p := c.proxyURL; p != nil {
if err := c.setProxy(p, &d); err != nil {
return false, err
}
}
wsConn, _, err := d.DialContext(ctx, c.server, c.config.Headers)
if err != nil {
return false, err
}
conn := cnet.NewWebSocketConn(wsConn)
// perform SSH handshake on net.Conn
c.Debugf("Handshaking...")
sshConn, chans, reqs, err := ssh.NewClientConn(conn, "", c.sshConfig)
if err != nil {
e := err.Error()
if strings.Contains(e, "unable to authenticate") {
c.Infof("Authentication failed")
c.Debugf(e)
} else {
c.Infof(e)
}
return false, err
}
defer sshConn.Close()
// chisel client handshake (reverse of server handshake)
// send configuration
c.Debugf("Sending config")
t0 := time.Now()
_, configerr, err := sshConn.SendRequest(
"config",
true,
settings.EncodeConfig(c.computed),
)
if err != nil {
c.Infof("Config verification failed")
return false, err
}
if len(configerr) > 0 {
return false, errors.New(string(configerr))
}
c.Infof("Connected (Latency %s)", time.Since(t0))
//connected, handover ssh connection for tunnel to use, and block
err = c.tunnel.BindSSH(ctx, sshConn, reqs, chans)
c.Infof("Disconnected")
connected = time.Since(t0) > 5*time.Second
return connected, err
}

总的来说,这个方法的作用是进行一次连接到chisel服务器的尝试,包括WebSocket握手、SSH握手和与服务器的通信。如果连接成功,返回truenil;如果连接失败,返回false和相应的错误信息。这个方法通常由connectionLoop方法调用,用于进行连接的具体实现。

数据传输部分

创建连接

BindSSH函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//BindSSH provides an active SSH for use for tunnelling
func (t *Tunnel) BindSSH(ctx context.Context, c ssh.Conn, reqs <-chan *ssh.Request, chans <-chan ssh.NewChannel) error {
//link ctx to ssh-conn
go func() {
<-ctx.Done()
if c.Close() == nil {
t.Debugf("SSH cancelled")
}
t.activatingConn.DoneAll()
}()
//mark active and unblock
t.activeConnMut.Lock()
if t.activeConn != nil {
panic("double bind ssh")
}
t.activeConn = c
t.activeConnMut.Unlock()
t.activatingConn.Done()
//optional keepalive loop against this connection
if t.Config.KeepAlive > 0 {
go t.keepAliveLoop(c)
}
//block until closed
go t.handleSSHRequests(reqs)
go t.handleSSHChannels(chans)
t.Debugf("SSH connected")
err := c.Wait()
t.Debugf("SSH disconnected")
//mark inactive and block
t.activatingConn.Add(1)
t.activeConnMut.Lock()
t.activeConn = nil
t.activeConnMut.Unlock()
return err
}

总的来说,这段代码的主要作用是为SSH隧道提供一个活跃的SSH连接,通过启动多个goroutines来处理SSH请求和通道,以及定期发送ping请求以保持SSH连接的活跃状态。连接关闭后,返回连接关闭时的错误。这个方法用于建立和维护SSH连接,以便进行数据传输。

  1. func (t *Tunnel) BindSSH(ctx context.Context, c ssh.Conn, reqs <-chan *ssh.Request, chans <-chan ssh.NewChannel) error { ... }:这是一个Tunnel结构体的方法,用于为SSH隧道提供一个活跃的SSH连接,接受三个参数:上下文ctx、SSH连接c、SSH请求通道reqs和SSH新通道通道chans
  2. go func() { ... }():这是一个goroutine,用于将上下文ctx与SSH连接c关联起来。当上下文ctx被取消时,它会尝试关闭SSH连接c,并通知调试信息表明SSH连接被取消。同时,它调用t.activatingConn.DoneAll()来减少激活连接计数,表示连接已完成激活。
  3. t.activeConnMut.Lock():获取Tunnel结构体中的activeConnMut互斥锁,用于保护对activeConn字段的并发访问。
  4. if t.activeConn != nil { ... }:检查activeConn字段是否已经被设置,如果已经设置,则抛出panic,表示不应该出现多次绑定SSH连接。
  5. t.activeConn = c:将SSH连接c设置为activeConn字段,表示当前SSH连接是活跃的。
  6. t.activeConnMut.Unlock():释放activeConnMut互斥锁。
  7. t.activatingConn.Done():通知t.activatingConn计数减少一个,表示SSH连接已完成激活。
  8. 如果配置中指定了保持活动的时间间隔t.Config.KeepAlive > 0,则启动一个goroutine,调用t.keepAliveLoop(c)来定期发送ping请求以保持SSH连接的活跃状态。
  9. 启动两个goroutines,一个用于处理SSH请求通道reqs,另一个用于处理SSH新通道通道chans。这两个goroutines将负责处理SSH连接的请求和通道创建。
  10. 打印调试信息表示SSH连接已连接成功。
  11. err := c.Wait():阻塞等待SSH连接的关闭,一旦连接关闭,将返回一个错误(如果有错误的话)。
  12. 打印调试信息表示SSH连接已断开。
  13. 重新增加t.activatingConn计数,表示SSH连接已完成激活。
  14. 获取activeConnMut互斥锁,将activeConn字段重置为nil,表示SSH连接不再活跃。
  15. 返回连接关闭时的错误(如果有错误的话)。

BindRemotes函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//BindRemotes converts the given remotes into proxies, and blocks
//until the caller cancels the context or there is a proxy error.
func (t *Tunnel) BindRemotes(ctx context.Context, remotes []*settings.Remote) error {
if len(remotes) == 0 {
return errors.New("no remotes")
}
if !t.Inbound {
return errors.New("inbound connections blocked")
}
proxies := make([]*Proxy, len(remotes))
for i, remote := range remotes {
p, err := NewProxy(t.Logger, t, t.proxyCount, remote)
if err != nil {
return err
}
proxies[i] = p
t.proxyCount++
}
//TODO: handle tunnel close
eg, ctx := errgroup.WithContext(ctx)
for _, proxy := range proxies {
p := proxy
eg.Go(func() error {
return p.Run(ctx)
})
}
t.Debugf("Bound proxies")
err := eg.Wait()
t.Debugf("Unbound proxies")
return err
}

总的来说,这段代码的主要作用是将给定的远程配置转换为代理对象,并在后台运行这些代理对象,用于处理入站连接请求。它会等待所有代理运行结束,如果有任何代理出现错误,则返回其中一个代理的错误。这个方法用于管理和运行代理,以便允许入站连接请求并将它们转发到相应的远程目标。

  1. func (t *Tunnel) BindRemotes(ctx context.Context, remotes []*settings.Remote) error { ... }:这是一个Tunnel结构体的方法,用于将给定的远程配置转换为代理并阻塞等待代理的运行,接受两个参数:上下文ctx和一个包含远程配置的切片remotes
  2. if len(remotes) == 0 { ... }:检查remotes切片是否为空,如果没有配置远程代理,返回一个错误,表示没有远程配置可用。
  3. if !t.Inbound { ... }:检查t.Inbound字段,如果设置为false,表示入站连接被阻止,返回一个错误,表示无法创建入站连接。
  4. 创建一个长度等于remotes切片长度的切片proxies,用于存储代理对象。
  5. 使用循环遍历remotes切片,并为每个远程配置创建一个代理对象。这些代理对象会存储在proxies切片中。同时,会为每个代理对象分配一个唯一的标识t.proxyCount,用于标识代理。
  6. 使用errgroup包创建一个错误组eg,用于管理并发运行的代理对象。
  7. 针对每个代理对象,启动一个goroutine,调用p.Run(ctx)来运行代理。这些代理会在后台运行,并等待入站连接请求。
  8. 打印调试信息表示代理已经成功创建。
  9. 使用eg.Wait()等待所有代理对象的运行结束。如果任何一个代理对象返回错误,eg.Wait()会返回其中的一个错误。
  10. 打印调试信息表示代理已经全部解绑(即代理运行结束)。
  11. 返回代理运行结束后的错误(如果有错误的话)。

handleSSHChannels函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
func (t *Tunnel) handleSSHChannels(chans <-chan ssh.NewChannel) {
for ch := range chans {
go t.handleSSHChannel(ch)
}
}

func (t *Tunnel) handleSSHChannel(ch ssh.NewChannel) {
if !t.Config.Outbound {
t.Debugf("Denied outbound connection")
ch.Reject(ssh.Prohibited, "Denied outbound connection")
return
}
remote := string(ch.ExtraData())
//extract protocol
hostPort, proto := settings.L4Proto(remote)
udp := proto == "udp"
socks := hostPort == "socks"
if socks && t.socksServer == nil {
t.Debugf("Denied socks request, please enable socks")
ch.Reject(ssh.Prohibited, "SOCKS5 is not enabled")
return
}
sshChan, reqs, err := ch.Accept()
if err != nil {
t.Debugf("Failed to accept stream: %s", err)
return
}
stream := io.ReadWriteCloser(sshChan)
//cnet.MeterRWC(t.Logger.Fork("sshchan"), sshChan)
defer stream.Close()
go ssh.DiscardRequests(reqs)
l := t.Logger.Fork("conn#%d", t.connStats.New())
//ready to handle
t.connStats.Open()
l.Debugf("Open %s", t.connStats.String())
if socks {
err = t.handleSocks(stream)
} else if udp {
err = t.handleUDP(l, stream, hostPort)
} else {
err = t.handleTCP(l, stream, hostPort)
}
t.connStats.Close()
errmsg := ""
if err != nil && !strings.HasSuffix(err.Error(), "EOF") {
errmsg = fmt.Sprintf(" (error %s)", err)
}
l.Debugf("Close %s%s", t.connStats.String(), errmsg)
}

总的来说,这段代码的主要作用是根据SSH通道的类型和目标来将数据流量路由到相应的处理函数,并记录连接的信息以及处理过程中的调试信息。根据不同的协议类型,会调用不同的处理函数来处理数据流量,例如处理SOCKS代理请求、UDP流量或TCP流量。这有助于在SSH隧道中根据不同的通道类型进行灵活的数据传输。

  1. func (t *Tunnel) handleSSHChannel(ch ssh.NewChannel) { ... }:这是一个Tunnel结构体的方法,用于处理SSH通道。
  2. if !t.Config.Outbound { ... }:检查t.Config.Outbound字段是否为false,如果是,则表示出站连接被禁止,打印调试信息并拒绝通道,返回ssh.Prohibited错误。
  3. remote := string(ch.ExtraData()):从通道的额外数据中获取远程地址信息,该额外数据通常包含了目标地址信息。
  4. 解析远程地址信息,提取出主机和端口以及协议类型(TCP或UDP),同时检查是否为SOCKS代理请求。udp表示协议类型为UDP,socks表示是SOCKS代理请求。
  5. 如果是SOCKS代理请求,并且SOCKS代理服务器未启用(t.socksServer == nil),则打印调试信息并拒绝通道,返回ssh.Prohibited错误。
  6. 通过ch.Accept()接受SSH通道,获取一个SSH通道和请求通道。如果无法接受通道,打印调试信息并返回。
  7. 创建一个stream,用于表示与SSH通道相关联的读写流。这个流将用于传输数据。
  8. 启动一个goroutine,通过ssh.DiscardRequests(reqs)来处理SSH请求通道,这里的请求通道主要是用于处理SSH的远程请求(例如端口转发)。
  9. 创建一个记录器(Logger)l,用于记录通道相关的调试信息。然后,通过t.connStats.Open()表示新建一个连接,打印连接信息。
  10. 根据通道的类型(SOCKS、UDP或TCP),分别调用相应的处理函数来处理数据流量。
    • 如果是SOCKS代理请求,调用t.handleSocks(stream)来处理SOCKS代理请求。
    • 如果是UDP协议,调用t.handleUDP(l, stream, hostPort)来处理UDP流量。
    • 如果是TCP协议,调用t.handleTCP(l, stream, hostPort)来处理TCP流量。
  11. 在处理完数据流后,关闭连接计数(t.connStats.Close()),并在连接关闭时打印相应的调试信息,包括连接状态和错误信息(如果有错误)。

数据包转发

TCP

1
2
3
4
5
6
7
8
9
func (t *Tunnel) handleTCP(l *cio.Logger, src io.ReadWriteCloser, hostPort string) error {
dst, err := net.Dial("tcp", hostPort)
if err != nil {
return err
}
s, r := cio.Pipe(src, dst)
l.Debugf("sent %s received %s", sizestr.ToString(s), sizestr.ToString(r))
return nil
}

总的来说,这段代码用于将TCP流量从SSH通道的数据流传输到目标主机和端口的TCP连接,并记录传输的数据量。这种方式可以用于在SSH隧道中转发TCP流量。、

  1. dst, err := net.Dial("tcp", hostPort):使用Go标准库的net.Dial函数建立一个TCP连接到指定的目标主机和端口(hostPort)。如果建立连接过程中出现错误,将返回错误信息。
  2. s, r := cio.Pipe(src, dst):使用自定义的cio.Pipe函数,将源数据流(src,来自SSH通道)和目标TCP连接(dst)连接起来,创建一个数据管道,允许数据从源传输到目标。sr分别代表已发送和已接收的数据量。
  3. l.Debugf("sent %s received %s", sizestr.ToString(s), sizestr.ToString(r)):使用记录器的Debugf方法,记录已发送和已接收的数据量,以便在调试时查看连接的数据流量。
  4. 最后,函数返回nil,表示成功处理TCP连接。

UDP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func (t *Tunnel) handleUDP(l *cio.Logger, rwc io.ReadWriteCloser, hostPort string) error {
conns := &udpConns{
Logger: l,
m: map[string]*udpConn{},
}
defer conns.closeAll()
h := &udpHandler{
Logger: l,
hostPort: hostPort,
udpChannel: &udpChannel{
r: gob.NewDecoder(rwc),
w: gob.NewEncoder(rwc),
c: rwc,
},
udpConns: conns,
maxMTU: settings.EnvInt("UDP_MAX_SIZE", 9012),
}
h.Debugf("UDP max size: %d bytes", h.maxMTU)
for {
p := udpPacket{}
if err := h.handleWrite(&p); err != nil {
return err
}
}
}

总的来说,这段代码的主要作用是建立一个UDP数据包处理器,不断接收UDP数据包并将它们转发到目标主机和端口。这种方式可以用于在SSH隧道中转发UDP流量。UDP数据包的收发和转发是通过udpHandler和相关的结构体来管理的。

  1. 创建一个udpConns结构体,用于管理UDP连接,包括记录每个UDP连接的状态。
  2. 在函数末尾使用defer conns.closeAll()确保在函数返回时关闭所有UDP连接。
  3. 创建一个udpHandler结构体,该结构体用于处理UDP数据包的收发和转发。它包括以下属性:
    • Logger:记录器,用于记录调试信息。
    • hostPort:目标主机和端口,表示要将UDP数据包转发到的目标地址。
    • udpChannel:UDP数据包的通信通道,包括读取器、写入器和通道本身。
    • udpConns:管理UDP连接的结构体,用于跟踪每个连接的状态。
    • maxMTU:UDP数据包的最大传输单元大小,可以通过环境变量配置,默认为9012字节。
  4. 打印调试信息,显示UDP最大数据包大小。
  5. 进入一个无限循环,用于不断接收和处理UDP数据包。
  6. 在循环中,创建一个空的udpPacket结构体,用于表示UDP数据包。
  7. 调用h.handleWrite(&p)来处理接收到的UDP数据包,其中&p表示要处理的数据包的引用。
  8. 如果处理UDP数据包过程中出现错误,函数将返回错误信息。

SOCKS

1
2
3
4
5
6
7
8
9
func (t *Tunnel) handleSocks(src io.ReadWriteCloser) error {
return t.socksServer.ServeConn(cnet.NewRWCConn(src))
}
func NewRWCConn(rwc io.ReadWriteCloser) net.Conn {
c := rwcConn{
ReadWriteCloser: rwc,
}
return &c
}

总的来说,这段代码的主要作用是处理 SOCKS 代理的连接请求,以及提供了一个用于创建网络连接的函数。它允许将io.ReadWriteCloser类型的对象转换为net.Conn类型的对象,以便在网络通信中使用。在 SOCKS 代理情境下,handleSocks函数将 SSH 通道的数据流转发给 SOCKS 代理服务器进行处理,而NewRWCConn函数用于创建符合net.Conn接口的对象。

  1. func (t *Tunnel) handleSocks(src io.ReadWriteCloser) error { ... }
    • 这个函数的主要作用是处理 SOCKS 代理的连接。
    • 它接受一个实现了io.ReadWriteCloser接口的参数src,通常来自SSH通道的数据流。
    • 函数通过调用t.socksServer.ServeConnsrc转换为net.Conn类型并传递给 SOCKS 代理服务器来处理。
    • 这个函数的实际作用是将 SOCKS 代理的连接请求转发给 SOCKS 代理服务器进行处理。
  2. func NewRWCConn(rwc io.ReadWriteCloser) net.Conn { ... }
    • 这个函数用于创建一个网络连接,返回一个net.Conn接口类型的对象。
    • 它接受一个实现了io.ReadWriteCloser接口的参数rwc,通常是一个包含读写方法的对象,例如SSH通道的数据流。
    • 在函数内部,它创建了一个rwcConn结构体,并将参数rwc赋值给ReadWriteCloser字段,然后返回这个rwcConn对象。
    • rwcConn是一个自定义的类型,实现了net.Conn接口,用于包装io.ReadWriteCloser对象,以便它可以被当作net.Conn类型使用。

wireshark抓包数据分析

服务器

1)

1
wsConn, err := upgrader.Upgrade(w, req, nil)
  • wsConn, err := upgrader.Upgrade(w, req, nil):这一行代码使用 upgrader 将 HTTP 连接升级为 WebSocket 连接,并返回一个 WebSocket 连接对象 wsConn。如果升级失败,将会返回一个错误。

    image-20240116180058857

2)

1
sshConn, chans, reqs, err := ssh.NewServerConn(conn, s.sshConfig)

server_hander.go里存在这段代码

  • sshConn, chans, reqs, err := ssh.NewServerConn(conn, s.sshConfig):这一行代码使用 SSH 包中的 NewServerConn 函数,将 WebSocket 连接升级为 SSH 连接,并返回 SSH 连接对象 sshConn、通道(channel)和请求(request)。
1
s.clientVersion, err = exchangeVersions(s.sshConn.conn, s.serverVersion)

ssh/server.go里面有这段代码,会将serverVersion传递给客户端

1
2
3
4
5
server.sshConfig = &ssh.ServerConfig{
ServerVersion: "SSH-" + chshare.ProtocolVersion + "-server",
PasswordCallback: server.authUser,
}
var ProtocolVersion = "chisel-v3"

main.go里面将version 定义为了SSH-chisel-v3-server,所以在传递的时候会和传递这个数据

image-20240116181038473

3)

一段乱糟糟的数据,执行完

sshConn, chans, reqs, err := ssh.NewServerConn(conn, s.sshConfig)

这段函数后发送的,但是这个函数是ssh库的函数产生的,不具备特殊性

image-20240116181810929

这段数据可以看出来是ssh的密钥交换算法的一部分,ssh key-exchange-algorithms,也就是ssh支持的算法什么的

客户端

1)

image-20240117170453929

客户端发送一个websocket的升级请求,chisel的数据特征和普通的特征的区别是websocket的子协议存在chisel字段

2)

image-20240117170859973

客户端发送的数据都是带有掩码异或的,但是服务端发送的数据不带有,所以从服务端的数据更能看出数据特征,这段数据便是经过掩码加密的,下图可以看到

image-20240117171059435

3)

image-20240117171155296

这段数据也是掩码加密的,实际数据如下

image-20240117171253956

4)

image-20240117171340886

这段数据经过掩码解密后的数据是乱码,很明显是经过加密的数据,不具备参考意义

5)

image-20240117171441167后面的数据是在ssh登录的时候传递的数据,很明显无论是客户端还是服务端都不具备参考意义

suricata规则

所以可以选择的特征如下

image-20240117175311322