From 8bd54a04f275f22fff945483b416b1132d7455cb Mon Sep 17 00:00:00 2001 From: adman Date: Fri, 31 Jul 2026 14:30:25 +0200 Subject: [PATCH] client: add ConnectWithConn, WithConn, WithDialer, LoginRawAddr - ConnectWithConn wraps an existing net.Conn into a versioned proto.Conn - DialFunc is a context-aware dial function type - WithConn uses a pre-established net.Conn (version required, no DNS) - WithDialer replaces net.Dialer with a custom dial function - LoginRawAddr accepts an explicit handshake host address - Login() wired to support all three connection modes --- mc/client/client.go | 20 +++++++++++ mc/client/login.go | 85 ++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 97 insertions(+), 8 deletions(-) diff --git a/mc/client/client.go b/mc/client/client.go index af68166..5ec713a 100644 --- a/mc/client/client.go +++ b/mc/client/client.go @@ -60,3 +60,23 @@ func ConnectVersionLess(ctx context.Context, addr string) (ret *proto.Conn, err ret.W = ret.Conn return } + +// ConnectWithConn wraps an existing net.Conn into a proto.Conn with the given +// Minecraft version. The version must be non-empty. +func ConnectWithConn(ctx context.Context, conn net.Conn, version string) (ret *proto.Conn, err error) { + if ctx == nil { + ctx = context.Background() + } + ret = proto.NewConnCtx(ctx) + ret.Actor = proto_base.Client + ret.Conn = conn + ret.R = conn + ret.W = conn + v, err := data.LookUpVersionByName(version) + if err != nil { + return + } + ret.ProtocolVersion = v.Version + ret.Version = version + return +} diff --git a/mc/client/login.go b/mc/client/login.go index 32f7cf6..7b5a1c7 100644 --- a/mc/client/login.go +++ b/mc/client/login.go @@ -7,6 +7,7 @@ import ( "crypto/x509" "encoding/json" "errors" + "net" "github.com/admin-else/strom/mc/api" "github.com/admin-else/strom/mc/crypto" @@ -135,7 +136,14 @@ func (s *LoginClient) OnStart() (err error) { } // LoginRaw performs the login handshake on an already-established connection without DNS resolution or configuration handling. +// The handshake address is derived from the connection's RemoteAddr. func LoginRaw(c *proto.Conn, account *api.Account) (err error) { + return LoginRawAddr(c, account, "") +} + +// LoginRawAddr performs the login handshake on an already-established connection using the given address for the handshake packet. +// If hostAddr is empty, the handshake address is derived from the connection's RemoteAddr. +func LoginRawAddr(c *proto.Conn, account *api.Account, hostAddr string) (err error) { lc := &LoginClient{ Conn: c, Account: account, @@ -148,7 +156,12 @@ func LoginRaw(c *proto.Conn, account *api.Account) (err error) { lc.RegisterCriticalUntil("26.1", lc.OnSuccess) lc.RegisterCriticalUntilLatest(lc.OnSuccess26_2) - p, err := MakeHandshakePacket(lc.Conn, proto_base.Login) + var p proto_base.EncodeDecodeAble + if hostAddr != "" { + p, err = MakeHandshakePacketAddr(lc.Conn, proto_base.Login, hostAddr) + } else { + p, err = MakeHandshakePacket(lc.Conn, proto_base.Login) + } if err != nil { return } @@ -194,11 +207,37 @@ func WithIgnoreConfig() func(ls *loginSettings) { } } +// DialFunc is a context-aware dial function used by Login to establish a TCP connection. +type DialFunc func(ctx context.Context, network, addr string) (net.Conn, error) + +// WithConn returns a login setting that uses an already-established net.Conn instead of dialling. +// WithVersion must also be set; auto-detection is not possible with a pre-established connection. +func WithConn(conn net.Conn) func(ls *loginSettings) { + return func(ls *loginSettings) { + ls.Conn = conn + } +} + +// WithDialer returns a login setting that uses the given dial function instead of net.Dialer. +// Auto version detection via status ping is still supported when no explicit version is set. +func WithDialer(d DialFunc) func(ls *loginSettings) { + return func(ls *loginSettings) { + ls.Dialer = d + } +} + +var ( + ErrWithConnRequiresVersion = errors.New("WithVersion is required when using WithConn") + ErrWithConnNoStatus = errors.New("auto version detection is not supported with WithConn") +) + type loginSettings struct { NoDns bool Ctx context.Context Version string IgnoreConfig bool + Conn net.Conn + Dialer DialFunc } // Login resolves the address with SRV DNS, connects, performs the login handshake, and handles the configuration phase, returning an authenticated connection ready for play. @@ -209,18 +248,48 @@ func Login(connectTo string, account *api.Account, settings ...func(ls *loginSet setting(ls) } - if !ls.NoDns { - connectTo, _, _, err = DoDNSChecked(ls.Ctx, connectTo, nil) + if ls.Conn != nil { + if ls.Version == "" { + return nil, ErrWithConnRequiresVersion + } + c, err = ConnectWithConn(ls.Ctx, ls.Conn, ls.Version) + if err != nil { + return + } + } else if ls.Dialer != nil { + if ls.Version == "" { + return nil, ErrWithConnRequiresVersion + } + if !ls.NoDns { + connectTo, _, _, err = DoDNSChecked(ls.Ctx, connectTo, nil) + if err != nil { + return + } + } + var conn net.Conn + conn, err = ls.Dialer(ls.Ctx, "tcp", connectTo) + if err != nil { + return + } + c, err = ConnectWithConn(ls.Ctx, conn, ls.Version) + if err != nil { + conn.Close() + return + } + } else { + if !ls.NoDns { + connectTo, _, _, err = DoDNSChecked(ls.Ctx, connectTo, nil) + if err != nil { + return + } + } + c, err = Connect(ls.Ctx, connectTo, ls.Version) if err != nil { return } } - c, err = Connect(ls.Ctx, connectTo, ls.Version) - if err != nil { - return - } - err = LoginRaw(c, account) + err = LoginRawAddr(c, account, connectTo) if err != nil { return }