Files
sing-box-extended/protocol/ssh/service.go

166 lines
4.7 KiB
Go

package ssh
import (
"bytes"
"context"
"net"
"os"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/listener"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/bufio/deadline"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"golang.org/x/crypto/ssh"
)
type Service interface {
PasswordCallback(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error)
PublicKeyCallback(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error)
UpdateUsers(users []option.SSHUser)
Handle(ctx context.Context, serverConn *ssh.ServerConn, channels <-chan ssh.NewChannel, requests <-chan *ssh.Request, metadata adapter.InboundContext, user string)
Close() error
}
var _ Service = (*service)(nil)
type service struct {
router adapter.ConnectionRouterEx
logger logger.ContextLogger
listener *listener.Listener
users []option.SSHUser
mtx sync.RWMutex
}
func newService(router adapter.ConnectionRouterEx, logger logger.ContextLogger, users []option.SSHUser) *service {
return &service{
router: router,
logger: logger,
users: users,
}
}
func (h *service) PasswordCallback(conn ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
h.mtx.RLock()
users := h.users
h.mtx.RUnlock()
for _, user := range users {
if user.Name != "" && user.Name != conn.User() {
continue
}
if user.Password != "" && user.Password == string(password) {
return &ssh.Permissions{Extensions: map[string]string{"user": user.Name}}, nil
}
}
return nil, E.New("password authentication failed for user ", conn.User())
}
func (h *service) PublicKeyCallback(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
h.mtx.RLock()
users := h.users
h.mtx.RUnlock()
for _, user := range users {
if user.Name != "" && user.Name != conn.User() {
continue
}
for _, authorizedKey := range user.AuthorizedKeys {
parsed, _, _, _, err := ssh.ParseAuthorizedKey([]byte(authorizedKey))
if err != nil {
continue
}
if bytes.Equal(parsed.Marshal(), key.Marshal()) {
return &ssh.Permissions{Extensions: map[string]string{"user": user.Name}}, nil
}
}
}
return nil, E.New("public key authentication failed for user ", conn.User())
}
func (h *service) UpdateUsers(users []option.SSHUser) {
h.mtx.Lock()
h.users = users
h.mtx.Unlock()
}
func (h *service) Handle(ctx context.Context, serverConn *ssh.ServerConn, channels <-chan ssh.NewChannel, requests <-chan *ssh.Request, metadata adapter.InboundContext, user string) {
h.logger.InfoContext(ctx, "[", user, "] authenticated SSH connection from ", metadata.Source)
go ssh.DiscardRequests(requests)
for newChannel := range channels {
switch newChannel.ChannelType() {
case "direct-tcpip":
go h.handleDirectChannel(ctx, metadata, newChannel)
default:
newChannel.Reject(ssh.UnknownChannelType, "only direct-tcpip is supported")
}
}
}
func (h *service) Close() error {
return nil
}
func (h *service) handleDirectChannel(ctx context.Context, metadata adapter.InboundContext, newChannel ssh.NewChannel) {
var payload directTCPIPData
if err := ssh.Unmarshal(newChannel.ExtraData(), &payload); err != nil {
newChannel.Reject(ssh.ConnectionFailed, "invalid direct-tcpip payload")
h.logger.ErrorContext(ctx, E.Cause(err, "parse direct-tcpip payload"))
return
}
channel, requests, err := newChannel.Accept()
if err != nil {
h.logger.ErrorContext(ctx, E.Cause(err, "accept direct-tcpip channel"))
return
}
go ssh.DiscardRequests(requests)
connMetadata := metadata
connMetadata.Destination = M.ParseSocksaddrHostPort(payload.HostToConnect, uint16(payload.PortToConnect))
conn := deadline.NewConn(&channelConn{
Channel: channel,
localAddr: metadata.OriginDestination.TCPAddr(),
remoteAddr: metadata.Source.TCPAddr(),
})
h.logger.InfoContext(ctx, "[", metadata.User, "] inbound connection to ", connMetadata.Destination)
h.router.RouteConnectionEx(ctx, conn, connMetadata, N.OnceClose(func(it error) {
channel.Close()
}))
}
type directTCPIPData struct {
HostToConnect string
PortToConnect uint32
OriginatorAddress string
OriginatorPort uint32
}
type channelConn struct {
ssh.Channel
localAddr net.Addr
remoteAddr net.Addr
}
func (c *channelConn) LocalAddr() net.Addr {
return c.localAddr
}
func (c *channelConn) RemoteAddr() net.Addr {
return c.remoteAddr
}
func (c *channelConn) SetDeadline(t time.Time) error {
return os.ErrInvalid
}
func (c *channelConn) SetReadDeadline(t time.Time) error {
return os.ErrInvalid
}
func (c *channelConn) SetWriteDeadline(t time.Time) error {
return os.ErrInvalid
}