a32ffd5608
When sniffing is enabled, the sniffer extracts the real domain from HTTP Host header or TLS SNI and checks bypass rules against it. However, most handlers were not passing WithBypass(...) to the sniffer, so the sniffer internal bypass check was always skipped. Additionally, the TLS-to-HTTP fallback paths in internal util sniffer and forwarder also omitted bypass when constructing options for the recursive HandleHTTP call after decrypting TLS. Add WithBypass to all sniffer call sites that were missing it across handlers (http, relay, socks4, socks5, ss, sshd, unix) and internal TLS fallback paths. Also fix import ordering in handler/http/connect.go. Fixes go-gost/gost#874
413 lines
10 KiB
Go
413 lines
10 KiB
Go
package ssh
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/go-gost/core/bypass"
|
|
"github.com/go-gost/core/handler"
|
|
"github.com/go-gost/core/logger"
|
|
md "github.com/go-gost/core/metadata"
|
|
"github.com/go-gost/core/observer/stats"
|
|
"github.com/go-gost/core/recorder"
|
|
xbypass "github.com/go-gost/x/bypass"
|
|
xctx "github.com/go-gost/x/ctx"
|
|
ictx "github.com/go-gost/x/internal/ctx"
|
|
xnet "github.com/go-gost/x/internal/net"
|
|
"github.com/go-gost/x/internal/util/sniffing"
|
|
sshd_util "github.com/go-gost/x/internal/util/sshd"
|
|
tls_util "github.com/go-gost/x/internal/util/tls"
|
|
rate_limiter "github.com/go-gost/x/limiter/rate"
|
|
xstats "github.com/go-gost/x/observer/stats"
|
|
stats_wrapper "github.com/go-gost/x/observer/stats/wrapper"
|
|
xrecorder "github.com/go-gost/x/recorder"
|
|
"github.com/go-gost/x/registry"
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
// Applicable SSH Request types for Port Forwarding - RFC 4254 7.X
|
|
const (
|
|
ForwardedTCPReturnRequest = "forwarded-tcpip" // RFC 4254 7.2
|
|
)
|
|
|
|
func init() {
|
|
registry.HandlerRegistry().Register("sshd", NewHandler)
|
|
}
|
|
|
|
type forwardHandler struct {
|
|
md metadata
|
|
options handler.Options
|
|
recorder recorder.RecorderObject
|
|
certPool tls_util.CertPool
|
|
}
|
|
|
|
func NewHandler(opts ...handler.Option) handler.Handler {
|
|
options := handler.Options{}
|
|
for _, opt := range opts {
|
|
opt(&options)
|
|
}
|
|
|
|
return &forwardHandler{
|
|
options: options,
|
|
}
|
|
}
|
|
|
|
func (h *forwardHandler) Init(md md.Metadata) (err error) {
|
|
if err = h.parseMetadata(md); err != nil {
|
|
return
|
|
}
|
|
|
|
for _, ro := range h.options.Recorders {
|
|
if ro.Record == xrecorder.RecorderServiceHandler {
|
|
h.recorder = ro
|
|
break
|
|
}
|
|
}
|
|
|
|
if h.md.certificate != nil && h.md.privateKey != nil {
|
|
h.certPool = tls_util.NewMemoryCertPool()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (h *forwardHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) (err error) {
|
|
defer conn.Close()
|
|
|
|
start := time.Now()
|
|
|
|
ro := &xrecorder.HandlerRecorderObject{
|
|
Network: "tcp",
|
|
Service: h.options.Service,
|
|
RemoteAddr: conn.RemoteAddr().String(),
|
|
LocalAddr: conn.LocalAddr().String(),
|
|
Time: start,
|
|
SID: xctx.SidFromContext(ctx).String(),
|
|
}
|
|
|
|
if srcAddr := xctx.SrcAddrFromContext(ctx); srcAddr != nil {
|
|
ro.ClientAddr = srcAddr.String()
|
|
}
|
|
|
|
log := h.options.Logger.WithFields(map[string]any{
|
|
"network": ro.Network,
|
|
"remote": conn.RemoteAddr().String(),
|
|
"local": conn.LocalAddr().String(),
|
|
"client": ro.ClientAddr,
|
|
"sid": ro.SID,
|
|
})
|
|
log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr())
|
|
|
|
pStats := xstats.Stats{}
|
|
|
|
defer func() {
|
|
if err != nil {
|
|
ro.Err = err.Error()
|
|
}
|
|
ro.InputBytes = pStats.Get(stats.KindInputBytes)
|
|
ro.OutputBytes = pStats.Get(stats.KindOutputBytes)
|
|
ro.Duration = time.Since(start)
|
|
if err := ro.Record(ctx, h.recorder.Recorder); err != nil {
|
|
log.Errorf("record: %v", err)
|
|
}
|
|
|
|
log.WithFields(map[string]any{
|
|
"duration": time.Since(start),
|
|
"inputBytes": ro.InputBytes,
|
|
"outputBytes": ro.OutputBytes,
|
|
}).Infof("%s >< %s", conn.RemoteAddr(), conn.LocalAddr())
|
|
}()
|
|
|
|
if !h.checkRateLimit(conn.RemoteAddr()) {
|
|
return rate_limiter.ErrRateLimit
|
|
}
|
|
|
|
switch cc := unwrapConn(conn).(type) {
|
|
case *sshd_util.DirectForwardConn:
|
|
return h.handleDirectForward(ctx, cc.DstAddr(), conn, ro, log, &pStats)
|
|
case *sshd_util.RemoteForwardConn:
|
|
return h.handleRemoteForward(ctx, cc, ro, log, &pStats)
|
|
default:
|
|
err := errors.New("sshd: wrong connection type")
|
|
log.Error(err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
func (h *forwardHandler) handleDirectForward(ctx context.Context, targetAddr string, origConn net.Conn, ro *xrecorder.HandlerRecorderObject, log logger.Logger, stats *xstats.Stats) error {
|
|
conn := stats_wrapper.WrapConn(origConn, stats)
|
|
|
|
ro.Host = targetAddr
|
|
log = log.WithFields(map[string]any{
|
|
"dst": fmt.Sprintf("%s/%s", targetAddr, "tcp"),
|
|
"cmd": "connect",
|
|
"host": targetAddr,
|
|
})
|
|
|
|
log.Debugf("%s >> %s", conn.RemoteAddr(), targetAddr)
|
|
|
|
if h.options.Bypass != nil && h.options.Bypass.Contains(ctx, "tcp", targetAddr, bypass.WithService(h.options.Service)) {
|
|
log.Debugf("bypass %s", targetAddr)
|
|
return xbypass.ErrBypass
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
cc, err := h.options.Router.Dial(ictx.ContextWithBuffer(ctx, &buf), "tcp", targetAddr)
|
|
ro.Route = buf.String()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cc.Close()
|
|
|
|
log = log.WithFields(map[string]any{"src": cc.LocalAddr().String(), "dst": cc.RemoteAddr().String()})
|
|
ro.SrcAddr = cc.LocalAddr().String()
|
|
ro.DstAddr = cc.RemoteAddr().String()
|
|
|
|
if h.md.sniffing {
|
|
if h.md.sniffingTimeout > 0 {
|
|
conn.SetReadDeadline(time.Now().Add(h.md.sniffingTimeout))
|
|
}
|
|
|
|
br := bufio.NewReader(conn)
|
|
proto, _ := sniffing.Sniff(ctx, br)
|
|
ro.Proto = proto
|
|
|
|
if h.md.sniffingTimeout > 0 {
|
|
conn.SetReadDeadline(time.Time{})
|
|
}
|
|
|
|
dial := func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
return cc, nil
|
|
}
|
|
dialTLS := func(ctx context.Context, network, address string, cfg *tls.Config) (net.Conn, error) {
|
|
return cc, nil
|
|
}
|
|
sniffer := &sniffing.Sniffer{
|
|
Websocket: h.md.sniffingWebsocket,
|
|
WebsocketSampleRate: h.md.sniffingWebsocketSampleRate,
|
|
Recorder: h.recorder.Recorder,
|
|
RecorderOptions: h.recorder.Options,
|
|
Certificate: h.md.certificate,
|
|
PrivateKey: h.md.privateKey,
|
|
NegotiatedProtocol: h.md.alpn,
|
|
CertPool: h.certPool,
|
|
MitmBypass: h.md.mitmBypass,
|
|
ReadTimeout: h.md.readTimeout,
|
|
}
|
|
|
|
switch proto {
|
|
case sniffing.ProtoHTTP:
|
|
return sniffer.HandleHTTP(ctx, "tcp", xnet.NewReadWriteConn(br, conn, conn),
|
|
sniffing.WithService(h.options.Service),
|
|
sniffing.WithDial(dial),
|
|
sniffing.WithDialTLS(dialTLS),
|
|
sniffing.WithBypass(h.options.Bypass),
|
|
sniffing.WithRecorderObject(ro),
|
|
sniffing.WithLog(log),
|
|
)
|
|
case sniffing.ProtoTLS:
|
|
return sniffer.HandleTLS(ctx, "tcp", xnet.NewReadWriteConn(br, conn, conn),
|
|
sniffing.WithService(h.options.Service),
|
|
sniffing.WithDial(dial),
|
|
sniffing.WithDialTLS(dialTLS),
|
|
sniffing.WithBypass(h.options.Bypass),
|
|
sniffing.WithRecorderObject(ro),
|
|
sniffing.WithLog(log),
|
|
)
|
|
}
|
|
}
|
|
|
|
t := time.Now()
|
|
log.Infof("%s <-> %s", cc.LocalAddr(), targetAddr)
|
|
// xnet.Transport(conn, cc)
|
|
xnet.Pipe(ctx, conn, cc)
|
|
log.WithFields(map[string]any{
|
|
"duration": time.Since(t),
|
|
}).Infof("%s >-< %s", cc.LocalAddr(), targetAddr)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (h *forwardHandler) handleRemoteForward(ctx context.Context, c *sshd_util.RemoteForwardConn, ro *xrecorder.HandlerRecorderObject, log logger.Logger, stats *xstats.Stats) error {
|
|
req := c.Request()
|
|
conn := stats_wrapper.WrapConn(c, stats)
|
|
|
|
t := tcpipForward{}
|
|
if err := ssh.Unmarshal(req.Payload, &t); err != nil {
|
|
log.Error(err)
|
|
return err
|
|
}
|
|
|
|
network := "tcp"
|
|
addr := net.JoinHostPort(t.Host, strconv.Itoa(int(t.Port)))
|
|
ro.Host = addr
|
|
|
|
log = log.WithFields(map[string]any{
|
|
"dst": addr,
|
|
"cmd": "bind",
|
|
})
|
|
|
|
log.Debugf("%s >> %s", conn.RemoteAddr(), addr)
|
|
|
|
// tie to the client connection
|
|
ln, err := net.Listen(network, addr)
|
|
if err != nil {
|
|
log.Error(err)
|
|
req.Reply(false, nil)
|
|
return err
|
|
}
|
|
defer ln.Close()
|
|
|
|
log = log.WithFields(map[string]any{
|
|
"src": ln.Addr().String(),
|
|
"bind": ln.Addr().String(),
|
|
})
|
|
ro.SrcAddr = ln.Addr().String()
|
|
|
|
log.Debugf("bind on %s OK", ln.Addr())
|
|
|
|
err = func() error {
|
|
if t.Port == 0 && req.WantReply { // Client sent port 0. let them know which port is actually being used
|
|
_, port, err := getHostPortFromAddr(ln.Addr())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var b [4]byte
|
|
binary.BigEndian.PutUint32(b[:], uint32(port))
|
|
t.Port = uint32(port)
|
|
return req.Reply(true, b[:])
|
|
}
|
|
return req.Reply(true, nil)
|
|
}()
|
|
if err != nil {
|
|
log.Error(err)
|
|
return err
|
|
}
|
|
|
|
sshConn := c.Conn()
|
|
|
|
go func() {
|
|
for {
|
|
cc, err := ln.Accept()
|
|
if err != nil { // Unable to accept new connection - listener is likely closed
|
|
return
|
|
}
|
|
|
|
go func(conn net.Conn) {
|
|
defer conn.Close()
|
|
|
|
log := log.WithFields(map[string]any{
|
|
"local": conn.LocalAddr().String(),
|
|
"remote": conn.RemoteAddr().String(),
|
|
})
|
|
|
|
p := directForward{}
|
|
var err error
|
|
|
|
var portnum int
|
|
p.Host1 = t.Host
|
|
p.Port1 = t.Port
|
|
p.Host2, portnum, err = getHostPortFromAddr(conn.RemoteAddr())
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
p.Port2 = uint32(portnum)
|
|
ch, reqs, err := sshConn.OpenChannel(ForwardedTCPReturnRequest, ssh.Marshal(p))
|
|
if err != nil {
|
|
log.Error("open forwarded channel: ", err)
|
|
return
|
|
}
|
|
defer ch.Close()
|
|
go ssh.DiscardRequests(reqs)
|
|
|
|
t := time.Now()
|
|
log.Debugf("%s <-> %s", conn.LocalAddr(), conn.RemoteAddr())
|
|
// xnet.Transport(ch, conn)
|
|
xnet.Pipe(ctx, ch, conn)
|
|
log.WithFields(map[string]any{
|
|
"duration": time.Since(t),
|
|
}).Debugf("%s >-< %s", conn.LocalAddr(), conn.RemoteAddr())
|
|
}(cc)
|
|
}
|
|
}()
|
|
|
|
tm := time.Now()
|
|
log.Infof("%s <-> %s", conn.RemoteAddr(), addr)
|
|
<-c.Done()
|
|
log.WithFields(map[string]any{
|
|
"duration": time.Since(tm),
|
|
}).Infof("%s >-< %s", conn.RemoteAddr(), addr)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (h *forwardHandler) checkRateLimit(addr net.Addr) bool {
|
|
if h.options.RateLimiter == nil {
|
|
return true
|
|
}
|
|
host, _, _ := net.SplitHostPort(addr.String())
|
|
if limiter := h.options.RateLimiter.Limiter(host); limiter != nil {
|
|
return limiter.Allow(1)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// connUnwrapper is an interface for unwrapping net.Conn wrappers to access
|
|
// the underlying connection type.
|
|
type connUnwrapper interface {
|
|
UnwrapConn() net.Conn
|
|
}
|
|
|
|
// unwrapConn peels through net.Conn wrapper layers (e.g. traffic limiters)
|
|
// to find the underlying concrete connection type.
|
|
func unwrapConn(c net.Conn) net.Conn {
|
|
for {
|
|
uw, ok := c.(connUnwrapper)
|
|
if !ok {
|
|
return c
|
|
}
|
|
if inner := uw.UnwrapConn(); inner != nil {
|
|
c = inner
|
|
} else {
|
|
return c
|
|
}
|
|
}
|
|
}
|
|
|
|
func getHostPortFromAddr(addr net.Addr) (host string, port int, err error) {
|
|
host, portString, err := net.SplitHostPort(addr.String())
|
|
if err != nil {
|
|
return
|
|
}
|
|
port, err = strconv.Atoi(portString)
|
|
return
|
|
}
|
|
|
|
// directForward is structure for RFC 4254 7.2 - can be used for "forwarded-tcpip" and "direct-tcpip"
|
|
type directForward struct {
|
|
Host1 string
|
|
Port1 uint32
|
|
Host2 string
|
|
Port2 uint32
|
|
}
|
|
|
|
func (p directForward) String() string {
|
|
return fmt.Sprintf("%s:%d -> %s:%d", p.Host2, p.Port2, p.Host1, p.Port1)
|
|
}
|
|
|
|
// tcpipForward is structure for RFC 4254 7.1 "tcpip-forward" request
|
|
type tcpipForward struct {
|
|
Host string
|
|
Port uint32
|
|
}
|