Files

234 lines
6.5 KiB
Go

// Package sqlserverproxy forwards TDS 7.x SQL-password sessions.
package sqlserverproxy
import (
"context"
"crypto/sha256"
"crypto/subtle"
"crypto/tls"
"database/sql"
"encoding/binary"
"errors"
"github.com/microsoft/go-mssqldb"
"github.com/microsoft/go-mssqldb/msdsn"
"linkport/internal/cluster"
"linkport/internal/model"
"linkport/internal/outbound"
"linkport/internal/protocol/transport"
"net"
"strconv"
"time"
)
type Handler struct{}
func New() *Handler { return &Handler{} }
type fixedDialer struct {
dial model.DialFunc
address string
}
func (d fixedDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
return d.dial(ctx, "tcp", d.address)
}
func (d fixedDialer) HostName() string { host, _, _ := net.SplitHostPort(d.address); return host }
func (h *Handler) Test(ctx context.Context, p model.Proxy, dial model.DialFunc) (model.TestResult, error) {
if err := p.CheckTargetCredential(); err != nil {
return model.TestResult{}, err
}
tlsConfig, err := transport.TLSConfig(p)
if err != nil {
return model.TestResult{}, err
}
encryption := msdsn.Encryption(msdsn.EncryptionDisabled)
if p.TargetTLS {
encryption = msdsn.EncryptionRequired
}
c := mssql.NewConnectorConfig(msdsn.Config{Host: p.TargetHost, Port: uint64(p.TargetPort), User: p.TargetUser, Password: p.TargetPassword, Database: p.AuthSource, Encryption: encryption, TLSConfig: tlsConfig, Protocols: []string{"tcp"}, DisableRetry: true})
c.Dialer = fixedDialer{dial, net.JoinHostPort(p.TargetHost, strconv.Itoa(p.TargetPort))}
db := sql.OpenDB(c)
defer db.Close()
done := outbound.Begin(ctx, "protocol_session")
err = db.PingContext(ctx)
done(err)
if err != nil {
return model.TestResult{}, errors.New("SQL Server authentication or connection failed")
}
return model.TestResult{Version: "SQL Server TDS", TLS: p.TargetTLS}, nil
}
func (h *Handler) Serve(ctx context.Context, raw net.Conn, p model.Proxy, dial model.DialFunc, cfg *tls.Config, event model.EventFunc) error {
defer raw.Close()
stop := context.AfterFunc(ctx, func() { raw.Close() })
defer stop()
raw.SetDeadline(time.Now().Add(15 * time.Second))
kind, hello, err := readMessage(raw)
if err != nil {
return err
}
if kind != 18 {
return errors.New("TDS prelogin required")
}
enc, err := encryptionOption(hello)
if err != nil {
return err
}
var down net.Conn = raw
if cfg != nil {
if enc == 2 {
return errors.New("SQL Server gateway requires TLS")
}
if err = writeMessage(raw, 4, prelogin(3)); err != nil {
return err
}
framed := &handshakeConn{Conn: raw, framed: true}
tlsCfg := cfg.Clone()
tlsCfg.MaxVersion = tls.VersionTLS12
tlsCfg.MinVersion = tls.VersionTLS12
secure := tls.Server(framed, tlsCfg)
if err = secure.HandshakeContext(ctx); err != nil {
return err
}
framed.framed = false
down = secure
} else {
if enc == 1 || enc == 3 {
return errors.New("gateway TLS certificate required")
}
if err = writeMessage(raw, 4, prelogin(2)); err != nil {
return err
}
}
kind, login, err := readMessage(down)
if err != nil {
return err
}
if kind != 16 || len(login) < 94 || int(binary.LittleEndian.Uint32(login)) != len(login) || login[25]&0x80 != 0 || login[27]&1 != 0 {
return errors.New("only SQL password LOGIN7 is supported")
}
user, err := loginField(login, 40, false)
if err != nil {
return err
}
password, err := loginField(login, 44, true)
if err != nil {
return err
}
account, found := transport.Account(p, user, raw.RemoteAddr())
a, b := sha256.Sum256([]byte(password)), sha256.Sum256([]byte(account.Password))
if !found || account.Password == "" || subtle.ConstantTimeCompare(a[:], b[:]) != 1 {
transport.AuthenticationEvent(event, user, false)
return errors.New("SQL Server gateway authentication failed")
}
p = account.Apply(p)
dial = p.DialForTarget(dial)
transport.AuthenticationEvent(event, user, true)
p, _, err = cluster.Resolve(ctx, p, dial)
if err != nil {
return err
}
up, err := dial(ctx, "tcp", net.JoinHostPort(p.TargetHost, strconv.Itoa(p.TargetPort)))
if err != nil {
return err
}
defer up.Close()
stopUp := context.AfterFunc(ctx, func() { up.Close() })
defer stopUp()
up.SetDeadline(time.Now().Add(15 * time.Second))
requestEncryption := byte(2)
if p.TargetTLS {
requestEncryption = 1
}
if err = writeMessage(up, 18, prelogin(requestEncryption)); err != nil {
return err
}
kind, response, err := readMessage(up)
if err != nil {
return err
}
if kind != 4 {
return errors.New("invalid upstream prelogin")
}
enc, err = encryptionOption(response)
if err != nil {
return err
}
var target net.Conn = up
if p.TargetTLS || enc == 1 || enc == 3 {
if enc != 1 && enc != 3 {
return errors.New("upstream TLS required")
}
p.TargetTLS = true
tlsCfg, e := transport.TLSConfig(p)
if e != nil {
return e
}
tlsCfg.MaxVersion = tls.VersionTLS12
framed := &handshakeConn{Conn: up, framed: true}
secure := tls.Client(framed, tlsCfg)
if err = secure.HandshakeContext(ctx); err != nil {
return err
}
framed.framed = false
target = secure
} else if enc != 2 {
return errors.New("unsupported upstream encryption negotiation")
}
login, err = upstreamLogin(login, p.TargetUser, p.TargetPassword, p.TargetHost)
if err != nil {
return err
}
if err = writeMessage(target, 16, login); err != nil {
return err
}
raw.SetDeadline(time.Time{})
up.SetDeadline(time.Time{})
done := make(chan error, 1)
var timing model.ExecutionQueue
go func() { e := relayMeasured(ctx, down, target, &timing); raw.Close(); done <- e }()
for {
var flags byte
kind, payload, e := readWithFlags(down, &flags)
if e != nil {
err = e
break
}
switch kind {
case 1:
if len(payload) < 4 {
err = errors.New("invalid SQL batch")
break
}
offset := int(binary.LittleEndian.Uint32(payload))
if offset < 4 || offset > len(payload) {
err = errors.New("invalid SQL batch headers")
break
}
err = model.AuditCommand(ctx, "sql_batch", decodeText(payload[offset:]), "allowed")
case 3:
err = model.AuditCommand(ctx, "rpc", "SQL Server RPC request (parameters omitted)", "allowed")
case 6, 7, 14:
err = model.AuditCommand(ctx, "tds", "SQL Server protocol operation", "allowed")
default:
err = errors.New("unsupported TDS operation")
}
if err != nil {
break
}
if kind == 1 {
timing.Start("sql_batch")
} else if kind == 3 {
timing.Start("rpc")
} else {
timing.Disable()
}
if err = writeWithFlags(target, kind, payload, flags); err != nil {
break
}
}
up.Close()
raw.Close()
<-done
return err
}