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
This commit is contained in:
ginuerzh
2026-06-21 15:39:52 +08:00
parent 61eb3e2a3b
commit 9ccf402ed1
3 changed files with 140 additions and 0 deletions
+11
View File
@@ -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{
+116
View File
@@ -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 }
+13
View File
@@ -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
}