diff --git a/go.mod b/go.mod index 491b544..372508c 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,10 @@ module github.com/paroxity/portal go 1.18 require ( + github.com/df-mc/dragonfly v0.9.2 github.com/go-gl/mathgl v1.0.0 github.com/google/uuid v1.3.0 + github.com/klauspost/compress v1.15.15 github.com/mattn/go-colorable v0.1.11 github.com/sandertv/gophertunnel v1.27.2 github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e @@ -13,21 +15,22 @@ require ( ) require ( + github.com/brentp/intintmap v0.0.0-20190211203843-30dc0ade9af9 // indirect + github.com/cespare/xxhash v1.1.0 // indirect github.com/df-mc/atomic v1.10.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/klauspost/compress v1.15.13 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/muhammadmuzzammil1998/jsonc v1.0.0 // indirect github.com/sandertv/go-raknet v1.12.0 // indirect golang.org/x/crypto v0.5.0 // indirect + golang.org/x/exp v0.0.0-20230206171751-46f607a40771 // indirect golang.org/x/image v0.3.0 // indirect golang.org/x/net v0.5.0 // indirect golang.org/x/oauth2 v0.4.0 // indirect - golang.org/x/sys v0.4.0 // indirect + golang.org/x/sys v0.5.0 // indirect golang.org/x/text v0.6.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.sum b/go.sum index e92e505..6bf071e 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,16 @@ +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/brentp/intintmap v0.0.0-20190211203843-30dc0ade9af9 h1:/G0ghZwrhou0Wq21qc1vXXMm/t/aKWkALWwITptKbE0= +github.com/brentp/intintmap v0.0.0-20190211203843-30dc0ade9af9/go.mod h1:TOk10ahXejq9wkEaym3KPRNeuR/h5Jx+s8QRWIa2oTM= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/df-mc/atomic v1.10.0 h1:0ZuxBKwR/hxcFGorKiHIp+hY7hgY+XBTzhCYD2NqSEg= github.com/df-mc/atomic v1.10.0/go.mod h1:Gw9rf+rPIbydMjA329Jn4yjd/O2c/qusw3iNp4tFGSc= +github.com/df-mc/dragonfly v0.9.2 h1:VoffCnFiiOd03P5X+ZsbuZkSuNELYh1Zc51v+0A/3HA= +github.com/df-mc/dragonfly v0.9.2/go.mod h1:hGGjGbLxpcn7nVTZOrk8kPfeGGntaMOqqcbuugZauyI= github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA= github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= github.com/go-gl/mathgl v1.0.0 h1:t9DznWJlXxxjeeKLIdovCOVJQk/GzDEL7h/h+Ro2B68= @@ -17,8 +25,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/klauspost/compress v1.15.13 h1:NFn1Wr8cfnenSJSA46lLq4wHCcBzKTSjnBIexDMMOV0= -github.com/klauspost/compress v1.15.13/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= +github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= @@ -35,6 +43,8 @@ github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e h1:7q6NSFZDeGfvv github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e/go.mod h1:DkpGd78rljTxKAnTDPFqXSGxvETQnJyuSOQwsHycqfs= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -45,6 +55,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/exp v0.0.0-20230206171751-46f607a40771 h1:xP7rWLUr1e1n2xkK5YB4LI0hPEy3LJC6Wk+D4pGlOJg= +golang.org/x/exp v0.0.0-20230206171751-46f607a40771/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.3.0 h1:HTDXbdK9bjfSWkPzDJIw89W8CAtfFGduujWs33NLLsg= golang.org/x/image v0.3.0/go.mod h1:fXd9211C/0VTlYuAcOhW8dY/RtEJqODXOWBDpmYBf+A= @@ -67,8 +79,8 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -93,4 +105,3 @@ gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/server/server.go b/server/server.go index cf69834..3335e40 100644 --- a/server/server.go +++ b/server/server.go @@ -6,17 +6,20 @@ import ( // Server represents a server connected to the proxy which players can join and play on. type Server struct { - name string - address string + name string + address string + useRakNet bool playerCount atomic.Int64 } -// New creates a new Server with the provided name, group and address. -func New(name, address string) *Server { +// New creates a new Server with the provided name, group and address as well as if the connection should use the RakNet +// protocol or not. +func New(name, address string, useRakNet bool) *Server { s := &Server{ - name: name, - address: address, + name: name, + address: address, + useRakNet: useRakNet, } return s @@ -33,6 +36,11 @@ func (s *Server) Address() string { return s.address } +// UseRakNet returns if a connection should use the RakNet protocol when connecting to the server. +func (s *Server) UseRakNet() bool { + return s.useRakNet +} + // IncrementPlayerCount increments the player count of the server. func (s *Server) IncrementPlayerCount() { s.playerCount.Add(1) diff --git a/session/server_conn.go b/session/server_conn.go new file mode 100644 index 0000000..699fed7 --- /dev/null +++ b/session/server_conn.go @@ -0,0 +1,28 @@ +package session + +import ( + "github.com/sandertv/gophertunnel/minecraft" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" + "io" + "time" +) + +// ServerConn represents a connection that can be used between the proxy and a server to communicate on behalf of a client. +type ServerConn interface { + io.Closer + // GameData returns specific game data set to the connection for the player to be initialised with. This data is + // obtained from the server during the login process. + GameData() minecraft.GameData + // DoSpawnTimeout starts the game for the client in the server with a timeout after which an error is returned if the + // client has not yet spawned by that time. DoSpawnTimeout will start the spawning sequence using the game data found + // in conn.GameData(), which was sent earlier by the server. + DoSpawnTimeout(timeout time.Duration) error + // ReadPacket reads a packet from the Conn, depending on the packet ID that is found in front of the packet data. If + // a read deadline is set, an error is returned if the deadline is reached before any packet is received. ReadPacket + // must not be called on multiple goroutines simultaneously. If the packet read was not implemented, a *packet.Unknown + // is returned, containing the raw payload of the packet read. + ReadPacket() (packet.Packet, error) + // WritePacket encodes the packet passed and writes it to the Conn. The encoded data is buffered until the next 20th + // of a second, after which the data is flushed and sent over the connection. + WritePacket(packet.Packet) error +} diff --git a/session/session.go b/session/session.go index 3513f41..b392419 100644 --- a/session/session.go +++ b/session/session.go @@ -7,6 +7,7 @@ import ( "github.com/paroxity/portal/event" "github.com/paroxity/portal/internal" "github.com/paroxity/portal/server" + "github.com/paroxity/portal/session/tcpprotocol" "github.com/sandertv/gophertunnel/minecraft" "github.com/sandertv/gophertunnel/minecraft/protocol" "github.com/sandertv/gophertunnel/minecraft/protocol/packet" @@ -34,8 +35,8 @@ type Session struct { loginMu sync.RWMutex serverMu sync.RWMutex server *server.Server - serverConn *minecraft.Conn - tempServerConn *minecraft.Conn + serverConn ServerConn + tempServerConn ServerConn entities *i64set.Set playerList *b16set.Set @@ -106,13 +107,20 @@ func New(conn *minecraft.Conn, store *Store, loadBalancer LoadBalancer, log inte // dial dials a new connection to the provided server. It then returns the connection between the proxy and // that server, along with any error that may have occurred. -func (s *Session) dial(srv *server.Server) (*minecraft.Conn, error) { +func (s *Session) dial(srv *server.Server) (ServerConn, error) { i := s.conn.IdentityData() i.XUID = "" - return minecraft.Dialer{ - ClientData: s.conn.ClientData(), - IdentityData: i, - }.Dial("raknet", srv.Address()) + if srv.UseRakNet() { + return minecraft.Dialer{ + ClientData: s.conn.ClientData(), + IdentityData: i, + }.Dial("raknet", srv.Address()) + } + return tcpprotocol.Dialer{ + ClientData: s.conn.ClientData(), + IdentityData: i, + EnableClientCache: s.conn.ClientCacheEnabled(), + }.Dial("tcp", srv.Address(), s.conn.RemoteAddr().String()) } // login performs the initial login sequence for the session. @@ -153,7 +161,7 @@ func (s *Session) Server() *server.Server { } // ServerConn returns the connection for the session's current server. -func (s *Session) ServerConn() *minecraft.Conn { +func (s *Session) ServerConn() ServerConn { s.waitForLogin() s.serverMu.RLock() defer s.serverMu.RUnlock() diff --git a/session/tcpprotocol/conn.go b/session/tcpprotocol/conn.go new file mode 100644 index 0000000..cca8d6b --- /dev/null +++ b/session/tcpprotocol/conn.go @@ -0,0 +1,331 @@ +package tcpprotocol + +import ( + "bytes" + "context" + "encoding/binary" + "fmt" + "github.com/google/uuid" + "github.com/klauspost/compress/snappy" + packet2 "github.com/paroxity/portal/session/tcpprotocol/packet" + "github.com/sandertv/gophertunnel/minecraft" + "github.com/sandertv/gophertunnel/minecraft/protocol" + "github.com/sandertv/gophertunnel/minecraft/protocol/login" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" + "go.uber.org/atomic" + "net" + "sync" + "time" +) + +// Conn represents a player's connection to a server that is using the TCP protocol instead of RakNet. +type Conn struct { + conn net.Conn + pool packet.Pool + + identityData login.IdentityData + clientData login.ClientData + gameData minecraft.GameData + + enableClientCache bool + + sendMu sync.Mutex + hdr *packet.Header + buf *bytes.Buffer + shieldID atomic.Int32 + + spawn chan struct{} +} + +// newConn attempts to create a new TCP-based connection to the provided server using the provided client data. If +// successful the connection will be returned, otherwise an error will be returned instead. +func newConn(netConn net.Conn) *Conn { + conn := &Conn{ + conn: netConn, + pool: packet.NewPool(), + buf: bytes.NewBuffer(make([]byte, 0, 4096)), + hdr: &packet.Header{}, + spawn: make(chan struct{}, 1), + } + return conn +} + +// login attempts to connect to a server by identifying the player and waiting to receive the StartGame packet. An error +// is returned if it failed to connect. +func (conn *Conn) login(playerAddress string) error { + err := conn.WritePacket(&packet2.ConnectionRequest{ + ProtocolVersion: ProtocolVersion, + }) + if err != nil { + return err + } + + connectionResponse, err := expect(conn, packet2.IDConnectionResponse) + if err != nil { + return err + } + switch connectionResponse.(*packet2.ConnectionResponse).Response { + case packet2.ConnectionResponseUnsupportedProtocol: + return fmt.Errorf("unsupported protocol version %d", ProtocolVersion) + } + + err = conn.WritePacket(&packet2.PlayerIdentity{ + IdentityData: conn.identityData, + ClientData: conn.clientData, + EnableClientCache: conn.enableClientCache, + Address: playerAddress, + }) + if err != nil { + return err + } + + startGame, err := expect(conn, packet.IDStartGame) + if err != nil { + return err + } + startGamePacket := startGame.(*packet.StartGame) + conn.gameData = minecraft.GameData{ + Difficulty: startGamePacket.Difficulty, + WorldName: startGamePacket.WorldName, + EntityUniqueID: startGamePacket.EntityUniqueID, + EntityRuntimeID: startGamePacket.EntityRuntimeID, + PlayerGameMode: startGamePacket.PlayerGameMode, + BaseGameVersion: startGamePacket.BaseGameVersion, + PlayerPosition: startGamePacket.PlayerPosition, + Pitch: startGamePacket.Pitch, + Yaw: startGamePacket.Yaw, + Dimension: startGamePacket.Dimension, + WorldSpawn: startGamePacket.WorldSpawn, + EditorWorld: startGamePacket.EditorWorld, + GameRules: startGamePacket.GameRules, + Time: startGamePacket.Time, + ServerBlockStateChecksum: startGamePacket.ServerBlockStateChecksum, + CustomBlocks: startGamePacket.Blocks, + Items: startGamePacket.Items, + PlayerMovementSettings: startGamePacket.PlayerMovementSettings, + WorldGameMode: startGamePacket.WorldGameMode, + ServerAuthoritativeInventory: startGamePacket.ServerAuthoritativeInventory, + Experiments: startGamePacket.Experiments, + } + return nil +} + +// identify attempts to identify the player attempting to connect to the server through the PlayerIdentity packet. An +// error is returned if it failed to identify. +func (conn *Conn) identify() error { + connectionRequest, err := expect(conn, packet2.IDConnectionRequest) + if err != nil { + return err + } + + response := packet2.ConnectionResponseSuccess + if connectionRequest.(*packet2.ConnectionRequest).ProtocolVersion != ProtocolVersion { + response = packet2.ConnectionResponseUnsupportedProtocol + } + err = conn.WritePacket(&packet2.ConnectionResponse{ + Response: response, + }) + if err != nil { + return err + } + + playerIdentity, err := expect(conn, packet2.IDPlayerIdentity) + if err != nil { + return err + } + playerIdentityPacket := playerIdentity.(*packet2.PlayerIdentity) + conn.identityData = playerIdentityPacket.IdentityData + conn.clientData = playerIdentityPacket.ClientData + conn.enableClientCache = playerIdentityPacket.EnableClientCache + return nil +} + +// Close ... +func (conn *Conn) Close() error { + return conn.conn.Close() +} + +// IdentityData ... +func (conn *Conn) IdentityData() login.IdentityData { + return conn.identityData +} + +// ClientData ... +func (conn *Conn) ClientData() login.ClientData { + return conn.clientData +} + +// ClientCacheEnabled ... +func (conn *Conn) ClientCacheEnabled() bool { + return conn.enableClientCache +} + +// ChunkRadius ... +func (conn *Conn) ChunkRadius() int { + //TODO implement me + return 8 +} + +// Latency ... +func (conn *Conn) Latency() time.Duration { + //TODO implement me + return time.Millisecond * 20 +} + +// Flush ... +func (conn *Conn) Flush() error { + return nil +} + +// RemoteAddr ... +func (conn *Conn) RemoteAddr() net.Addr { + return conn.conn.RemoteAddr() +} + +// GameData ... +func (conn *Conn) GameData() minecraft.GameData { + return conn.gameData +} + +// DoSpawnTimeout ... +func (conn *Conn) DoSpawnTimeout(timeout time.Duration) error { + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + select { + case <-ctx.Done(): + return fmt.Errorf("spawn timeout") + case <-conn.spawn: + return nil + } +} + +// StartGameContext ... +func (conn *Conn) StartGameContext(_ context.Context, data minecraft.GameData) error { + for _, item := range data.Items { + if item.Name == "minecraft:shield" { + conn.shieldID.Store(int32(item.RuntimeID)) + break + } + } + return conn.WritePacket(&packet.StartGame{ + Difficulty: data.Difficulty, + EntityUniqueID: data.EntityUniqueID, + EntityRuntimeID: data.EntityRuntimeID, + PlayerGameMode: data.PlayerGameMode, + PlayerPosition: data.PlayerPosition, + Pitch: data.Pitch, + Yaw: data.Yaw, + Dimension: data.Dimension, + WorldSpawn: data.WorldSpawn, + EditorWorld: data.EditorWorld, + GameRules: data.GameRules, + Time: data.Time, + Blocks: data.CustomBlocks, + Items: data.Items, + AchievementsDisabled: true, + Generator: 1, + EducationFeaturesEnabled: true, + MultiPlayerGame: true, + MultiPlayerCorrelationID: uuid.Must(uuid.NewRandom()).String(), + CommandsEnabled: true, + WorldName: data.WorldName, + LANBroadcastEnabled: true, + PlayerMovementSettings: data.PlayerMovementSettings, + WorldGameMode: data.WorldGameMode, + ServerAuthoritativeInventory: data.ServerAuthoritativeInventory, + Experiments: data.Experiments, + BaseGameVersion: data.BaseGameVersion, + GameVersion: protocol.CurrentVersion, + }) +} + +// ReadPacket ... +func (conn *Conn) ReadPacket() (pk packet.Packet, err error) { + var l uint32 + if err := binary.Read(conn.conn, binary.LittleEndian, &l); err != nil { + return nil, err + } + + data := make([]byte, l) + read, err := conn.conn.Read(data) + if err != nil { + return nil, err + } + if read != int(l) { + return nil, fmt.Errorf("expected %v bytes, got %v", l, read) + } + + decoded, err := snappy.Decode(nil, data) + if err != nil { + return nil, err + } + + buf := bytes.NewBuffer(decoded) + header := &packet.Header{} + if err := header.Read(buf); err != nil { + return nil, err + } + + pkFunc, ok := conn.pool[header.PacketID] + if !ok { + return nil, fmt.Errorf("unknown packet %v", header.PacketID) + } + + defer func() { + if recoveredErr := recover(); recoveredErr != nil { + err = fmt.Errorf("%T: %w", pk, recoveredErr.(error)) + } + }() + pk = pkFunc() + pk.Unmarshal(protocol.NewReader(buf, 0)) + if buf.Len() > 0 { + return nil, fmt.Errorf("still have %v bytes unread", buf.Len()) + } + + if _, ok := pk.(*packet.StartGame); ok { + close(conn.spawn) + } + + return pk, nil +} + +// WritePacket ... +func (conn *Conn) WritePacket(pk packet.Packet) error { + conn.sendMu.Lock() + conn.hdr.PacketID = pk.ID() + _ = conn.hdr.Write(conn.buf) + + pk.Marshal(protocol.NewWriter(conn.buf, conn.shieldID.Load())) + + data := conn.buf.Bytes() + conn.buf.Reset() + conn.sendMu.Unlock() + + encoded := snappy.Encode(nil, data) + + buf := bytes.NewBuffer(make([]byte, 0, 4+len(encoded))) + + if err := binary.Write(buf, binary.LittleEndian, int32(len(encoded))); err != nil { + return err + } + if _, err := buf.Write(encoded); err != nil { + return err + } + + if _, err := conn.conn.Write(buf.Bytes()); err != nil { + return err + } + + return nil +} + +func expect(conn *Conn, expectedID uint32) (packet.Packet, error) { + pk, err := conn.ReadPacket() + if err != nil { + return pk, err + } + if pk.ID() != expectedID { + return nil, fmt.Errorf("received unexpected packet %T", pk) + } + return pk, nil +} diff --git a/session/tcpprotocol/dialer.go b/session/tcpprotocol/dialer.go new file mode 100644 index 0000000..39a4f81 --- /dev/null +++ b/session/tcpprotocol/dialer.go @@ -0,0 +1,50 @@ +package tcpprotocol + +import ( + "github.com/sandertv/gophertunnel/minecraft/protocol/login" + "net" +) + +// Dialer allows specifying specific settings for connection to a Minecraft server. The zero value of Dialer is used for +// the package level Dial function. +type Dialer struct { + // IdentityData is the identity data used to login to the server with. It includes the username, UUID and XUID of the + // player. The IdentityData object is obtained using Minecraft auth if Email and Password are set. If not, the object + // provided here is used, or a default one if left empty. + IdentityData login.IdentityData + // ClientData is the client data used to login to the server with. It includes fields such as the skin, locale and + // UUIDs unique to the client. If empty, a default is sent produced using defaultClientData(). + ClientData login.ClientData + // EnableClientCache, if set to true, enables the client blob cache for the client. This means that the server will + // send chunks as blobs, which may be saved by the client so that chunks don't have to be transmitted every time, + // resulting in less network transmission. + EnableClientCache bool +} + +// Dial dials a Minecraft connection to the address passed over the network passed. The network is typically "tcp". A +// Conn is returned which may be used to receive packets from and send packets to. A zero value of a Dialer struct is +// used to initiate the connection. A custom Dialer may be used to specify additional behaviour. +func Dial(network, address, playerAddress string) (*Conn, error) { + var d Dialer + return d.Dial(network, address, playerAddress) +} + +// Dial dials a Minecraft connection to the address passed over the network passed. The network is typically +// "raknet". A Conn is returned which may be used to receive packets from and send packets to. +func (d Dialer) Dial(network, address, playerAddress string) (*Conn, error) { + netConn, err := net.Dial(network, address) + if err != nil { + return nil, err + } + + conn := newConn(netConn) + conn.identityData = d.IdentityData + conn.clientData = d.ClientData + conn.enableClientCache = d.EnableClientCache + + err = conn.login(playerAddress) + if err != nil { + return nil, err + } + return conn, nil +} diff --git a/session/tcpprotocol/listener.go b/session/tcpprotocol/listener.go new file mode 100644 index 0000000..877a38d --- /dev/null +++ b/session/tcpprotocol/listener.go @@ -0,0 +1,56 @@ +package tcpprotocol + +import ( + "github.com/df-mc/dragonfly/server/session" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" + "net" +) + +// Listener implements a Minecraft listener on top of an unspecific net.Listener. It abstracts away the login sequence +// of connecting clients and provides the implements the net.Listener interface to provide a consistent API. +type Listener struct { + listener net.Listener +} + +// Listen announces on the local network address. The network must be "tcp", "tcp4", "tcp6", "unix", "unixpacket" +// A Listener is returned which may be used to accept connections. If the host in the address parameter is empty or a +// literal unspecified IP address, Listen listens on all available unicast and anycast IP addresses of the local system. +func Listen(network, address string) (*Listener, error) { + l, err := net.Listen(network, address) + if err != nil { + return nil, err + } + listener := &Listener{ + listener: l, + } + return listener, nil +} + +// Accept ... +func (l *Listener) Accept() (session.Conn, error) { + netConn, err := l.listener.Accept() + if err != nil { + return nil, err + } + + conn := newConn(netConn) + err = conn.identify() + if err != nil { + return nil, err + } + return conn, nil +} + +// Disconnect ... +func (l *Listener) Disconnect(conn session.Conn, message string) error { + _ = conn.WritePacket(&packet.Disconnect{ + HideDisconnectionScreen: message == "", + Message: message, + }) + return conn.Close() +} + +// Close ... +func (l *Listener) Close() error { + return l.listener.Close() +} diff --git a/session/tcpprotocol/packet/connection_request.go b/session/tcpprotocol/packet/connection_request.go new file mode 100644 index 0000000..2c0f421 --- /dev/null +++ b/session/tcpprotocol/packet/connection_request.go @@ -0,0 +1,26 @@ +package packet + +import ( + "github.com/sandertv/gophertunnel/minecraft/protocol" +) + +// ConnectionRequest is sent by the proxy to request a connection for a player who is attempting to join the server. +type ConnectionRequest struct { + // ProtocolVersion is the protocol version of the TCP protocol used by the proxy. + ProtocolVersion uint32 +} + +// ID ... +func (pk *ConnectionRequest) ID() uint32 { + return IDConnectionRequest +} + +// Marshal ... +func (pk *ConnectionRequest) Marshal(w *protocol.Writer) { + w.Uint32(&pk.ProtocolVersion) +} + +// Unmarshal ... +func (pk *ConnectionRequest) Unmarshal(r *protocol.Reader) { + r.Uint32(&pk.ProtocolVersion) +} diff --git a/session/tcpprotocol/packet/connection_response.go b/session/tcpprotocol/packet/connection_response.go new file mode 100644 index 0000000..18f5826 --- /dev/null +++ b/session/tcpprotocol/packet/connection_response.go @@ -0,0 +1,32 @@ +package packet + +import ( + "github.com/sandertv/gophertunnel/minecraft/protocol" +) + +const ( + ConnectionResponseSuccess byte = iota + ConnectionResponseUnsupportedProtocol +) + +// ConnectionResponse is sent by the server in response to a ConnectionRequest packet. It contains the response and if +// the player was able to connect to the server successfully. +type ConnectionResponse struct { + // Response is the response from the server. This can be one of the constants above. + Response byte +} + +// ID ... +func (pk *ConnectionResponse) ID() uint32 { + return IDConnectionResponse +} + +// Marshal ... +func (pk *ConnectionResponse) Marshal(w *protocol.Writer) { + w.Uint8(&pk.Response) +} + +// Unmarshal ... +func (pk *ConnectionResponse) Unmarshal(r *protocol.Reader) { + r.Uint8(&pk.Response) +} diff --git a/session/tcpprotocol/packet/id.go b/session/tcpprotocol/packet/id.go new file mode 100644 index 0000000..44b116f --- /dev/null +++ b/session/tcpprotocol/packet/id.go @@ -0,0 +1,7 @@ +package packet + +const ( + IDConnectionRequest uint32 = 0x3FF - iota + IDConnectionResponse + IDPlayerIdentity +) diff --git a/session/tcpprotocol/packet/player_identity.go b/session/tcpprotocol/packet/player_identity.go new file mode 100644 index 0000000..ec052c0 --- /dev/null +++ b/session/tcpprotocol/packet/player_identity.go @@ -0,0 +1,49 @@ +package packet + +import ( + "encoding/json" + "github.com/sandertv/gophertunnel/minecraft/protocol" + "github.com/sandertv/gophertunnel/minecraft/protocol/login" +) + +// PlayerIdentity is sent by the proxy to give the server information about the client that would usually be sent within +// the login sequence. +type PlayerIdentity struct { + // IdentityData contains identity data of the player logged in. + IdentityData login.IdentityData + // ClientData is a container of client specific data of a Login packet. It holds data such as the skin of a player, + // but also its language code and device information. + ClientData login.ClientData + // EnableClientCache, if set to true, enables the client blob cache for the client. This means that the server will + // send chunks as blobs, which may be saved by the client so that chunks don't have to be transmitted every time, + // resulting in less network transmission. + EnableClientCache bool + // Address is the address of the player that has joined the server. + Address string +} + +// ID ... +func (pk *PlayerIdentity) ID() uint32 { + return IDPlayerIdentity +} + +// Marshal ... +func (pk *PlayerIdentity) Marshal(w *protocol.Writer) { + identityData, _ := json.Marshal(pk.IdentityData) + clientData, _ := json.Marshal(pk.ClientData) + w.ByteSlice(&identityData) + w.ByteSlice(&clientData) + w.Bool(&pk.EnableClientCache) + w.String(&pk.Address) +} + +// Unmarshal ... +func (pk *PlayerIdentity) Unmarshal(r *protocol.Reader) { + var identityData, clientData []byte + r.ByteSlice(&identityData) + r.ByteSlice(&clientData) + r.Bool(&pk.EnableClientCache) + r.String(&pk.Address) + _ = json.Unmarshal(identityData, &pk.IdentityData) + _ = json.Unmarshal(clientData, &pk.ClientData) +} diff --git a/session/tcpprotocol/protocol.go b/session/tcpprotocol/protocol.go new file mode 100644 index 0000000..86d64ab --- /dev/null +++ b/session/tcpprotocol/protocol.go @@ -0,0 +1,16 @@ +package tcpprotocol + +import ( + packet2 "github.com/paroxity/portal/session/tcpprotocol/packet" + "github.com/sandertv/gophertunnel/minecraft/protocol/packet" +) + +// ProtocolVersion is the current supported version of the protocol. If a server is using an outdated version of the +// protocol, players will be unable to connect. This constant gets updated every time the protocol is changed. +const ProtocolVersion = 1 + +func init() { + packet.Register(packet2.IDPlayerIdentity, func() packet.Packet { return &packet2.PlayerIdentity{} }) + packet.Register(packet2.IDConnectionRequest, func() packet.Packet { return &packet2.ConnectionRequest{} }) + packet.Register(packet2.IDConnectionResponse, func() packet.Packet { return &packet2.ConnectionResponse{} }) +} diff --git a/socket/handler_register_server.go b/socket/handler_register_server.go index 9d360e5..ce23f0f 100644 --- a/socket/handler_register_server.go +++ b/socket/handler_register_server.go @@ -11,7 +11,11 @@ type RegisterServerHandler struct{ requireAuth } // Handle ... func (*RegisterServerHandler) Handle(p packet.Packet, srv Server, c *Client) error { pk := p.(*packet.RegisterServer) - srv.ServerRegistry().AddServer(server.New(c.Name(), pk.Address)) - srv.Logger().Debugf("socket connection \"%s\" has registered itself as a server with the address \"%s\"", c.Name(), pk.Address) + srv.ServerRegistry().AddServer(server.New(c.Name(), pk.Address, pk.UseRakNet)) + mode := "TCP" + if pk.UseRakNet { + mode = "RakNet" + } + srv.Logger().Debugf("socket connection \"%s\" has registered itself as a server with the address \"%s\" (%s)", c.Name(), pk.Address, mode) return nil } diff --git a/socket/packet/id.go b/socket/packet/id.go index 58cbff6..4b08947 100644 --- a/socket/packet/id.go +++ b/socket/packet/id.go @@ -2,7 +2,7 @@ package packet // ProtocolVersion is the protocol version supported by the proxy. It will only accept clients that match this version, // and it should be incremented every time the protocol changes. -const ProtocolVersion = 1 +const ProtocolVersion = 2 const ( IDAuthRequest uint16 = iota diff --git a/socket/packet/register_server.go b/socket/packet/register_server.go index b560b72..ef71517 100644 --- a/socket/packet/register_server.go +++ b/socket/packet/register_server.go @@ -6,6 +6,8 @@ import "github.com/sandertv/gophertunnel/minecraft/protocol" type RegisterServer struct { // Address is the address of the server in the format ip:port. Address string + // UseRakNet is if a connection should use the RakNet protocol when connecting to the server. + UseRakNet bool } // ID ... @@ -16,9 +18,11 @@ func (pk *RegisterServer) ID() uint16 { // Marshal ... func (pk *RegisterServer) Marshal(w *protocol.Writer) { w.String(&pk.Address) + w.Bool(&pk.UseRakNet) } // Unmarshal ... func (pk *RegisterServer) Unmarshal(r *protocol.Reader) { r.String(&pk.Address) + r.Bool(&pk.UseRakNet) }