mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-06-08 04:04:55 +03:00
166 lines
4.7 KiB
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
|
|
}
|