From 9ccf402ed1f5e2308b427d389051dce203997664 Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Sun, 21 Jun 2026 15:39:52 +0800 Subject: [PATCH] feat(listener): add stdio listener for SSH ProxyCommand support The stdio listener wraps os.Stdin/os.Stdout as a net.Listener that accepts exactly one connection. It enables GOST to be used as an SSH ProxyCommand, where the parent process (SSH) pipes the transport byte stream through the child process's standard I/O. Usage: gost -L stdio://example.com:22 -F http://proxy:8080 The URL host:port is treated as the forwarding target and converted to a forward node in buildServiceConfig. The standard forward handler then routes traffic through the configured chain to the destination. Fixes go-gost/gost#433 --- config/cmd/cmd.go | 11 ++++ listener/stdio/listener.go | 116 +++++++++++++++++++++++++++++++++++++ listener/stdio/metadata.go | 13 +++++ 3 files changed, 140 insertions(+) create mode 100644 listener/stdio/listener.go create mode 100644 listener/stdio/metadata.go diff --git a/config/cmd/cmd.go b/config/cmd/cmd.go index 87c24b4c..34e15296 100644 --- a/config/cmd/cmd.go +++ b/config/cmd/cmd.go @@ -466,6 +466,17 @@ func buildServiceConfig(url *url.URL) ([]*config.ServiceConfig, error) { } } + // stdio listener uses url.Host as the forwarding target. + if listener == "stdio" && len(nodes) == 0 && url.Host != "" { + nodes = append(nodes, &config.ForwardNodeConfig{ + Name: fmt.Sprintf("%starget-0", namePrefix), + Addr: url.Host, + }) + if handler == "auto" { + handler = "tcp" + } + } + if len(nodes) > 0 { if len(services) == 1 { services[0].Forwarder = &config.ForwarderConfig{ diff --git a/listener/stdio/listener.go b/listener/stdio/listener.go new file mode 100644 index 00000000..b06e1a32 --- /dev/null +++ b/listener/stdio/listener.go @@ -0,0 +1,116 @@ +// Package stdio implements a listener that wraps os.Stdin/os.Stdout as a +// net.Listener. It accepts exactly one connection, making it suitable for +// SSH ProxyCommand where the parent process (SSH) pipes the transport byte +// stream through the child process's standard I/O. +// +// Usage: gost -L stdio://example.com:22 -F http://proxy:8080 +package stdio + +import ( + "net" + "os" + "sync" + "time" + + "github.com/go-gost/core/listener" + "github.com/go-gost/core/logger" + md "github.com/go-gost/core/metadata" + "github.com/go-gost/x/registry" +) + +func init() { + registry.ListenerRegistry().Register("stdio", NewListener) +} + +type ( + stdioListener struct { + conn net.Conn + done chan struct{} + accepted bool + mu sync.Mutex + laddr net.Addr + logger logger.Logger + md metadata + options listener.Options + } +) + +// NewListener creates a stdio listener. The returned listener wraps +// os.Stdin and os.Stdout as a single-use net.Conn. +func NewListener(opts ...listener.Option) listener.Listener { + options := listener.Options{} + for _, opt := range opts { + opt(&options) + } + return &stdioListener{ + logger: options.Logger, + options: options, + } +} + +func (l *stdioListener) Init(md md.Metadata) (err error) { + if err = l.parseMetadata(md); err != nil { + return + } + + l.laddr = &stdioAddr{addr: "stdio"} + l.conn = &stdioConn{} + l.done = make(chan struct{}) + + return +} + +// Accept returns the stdio connection on the first call. +// Subsequent calls block until the listener is closed. +func (l *stdioListener) Accept() (net.Conn, error) { + l.mu.Lock() + if !l.accepted { + l.accepted = true + l.mu.Unlock() + return l.conn, nil + } + l.mu.Unlock() + + <-l.done + return nil, net.ErrClosed +} + +func (l *stdioListener) Addr() net.Addr { + return l.laddr +} + +func (l *stdioListener) Close() error { + select { + case <-l.done: + default: + close(l.done) + } + return nil +} + +// stdioConn implements net.Conn over os.Stdin and os.Stdout. +type stdioConn struct{} + +func (c *stdioConn) Read(b []byte) (int, error) { return os.Stdin.Read(b) } +func (c *stdioConn) Write(b []byte) (int, error) { return os.Stdout.Write(b) } + +// Close is a no-op: stdin/stdout are owned by the process and will be +// closed by the parent when the session ends. +func (c *stdioConn) Close() error { return nil } + +func (c *stdioConn) LocalAddr() net.Addr { return &stdioAddr{addr: "stdio"} } +func (c *stdioConn) RemoteAddr() net.Addr { return &stdioAddr{addr: "pipe"} } + +// Deadline methods are no-ops: os.Stdin/os.Stdout do not support +// deadline-based I/O. +func (c *stdioConn) SetDeadline(t time.Time) error { return nil } +func (c *stdioConn) SetReadDeadline(t time.Time) error { return nil } +func (c *stdioConn) SetWriteDeadline(t time.Time) error { return nil } + +// stdioAddr is a trivial net.Addr for stdio connections. +type stdioAddr struct { + addr string +} + +func (a *stdioAddr) Network() string { return "tcp" } +func (a *stdioAddr) String() string { return a.addr } diff --git a/listener/stdio/metadata.go b/listener/stdio/metadata.go new file mode 100644 index 00000000..dbea6067 --- /dev/null +++ b/listener/stdio/metadata.go @@ -0,0 +1,13 @@ +package stdio + +import ( + md "github.com/go-gost/core/metadata" +) + +type metadata struct { + // reserved for future options (e.g., timeouts) +} + +func (l *stdioListener) parseMetadata(md md.Metadata) (err error) { + return +}