package e2e import ( "bufio" "bytes" "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" "io" "net" "testing" "time" "github.com/iceBear67/redapricot/client" "github.com/iceBear67/redapricot/client/wire" ) // ---- mock velocity-forwarding destination ---- // veloEvent is what the mock backend saw in the (verified) forwarding payload. type veloEvent struct { version int ip string uuid []byte name string err error } type veloDest struct { addr string secret string success []byte // the Login Success packet the backend sends after the exchange events chan veloEvent } // newVeloDest starts a mock backend that requires Velocity modern forwarding: // it reads the handshake and Login Start, sends the velocity:player_info // query (with a negative message id, as Paper's random ids often are), verifies // the HMAC-signed response, and finally sends a recognizable Login Success. func newVeloDest(t *testing.T, secret string) *veloDest { t.Helper() ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("velo dest listen: %v", err) } d := &veloDest{ addr: ln.Addr().String(), secret: secret, success: mcPacket(wire.NewWriter(). VarInt(0x02). // Login Success Bytes(bytes.Repeat([]byte{0x42}, 16)). String("e2ePlayer"). VarInt(0). Out()), events: make(chan veloEvent, 16), } t.Cleanup(func() { _ = ln.Close() }) go func() { for { conn, err := ln.Accept() if err != nil { return } go d.handle(conn) } }() return d } const veloMsgID = -777 func (d *veloDest) handle(conn net.Conn) { defer conn.Close() ev := d.exchange(conn) d.events <- ev if ev.err == nil { _, _ = conn.Write(d.success) } _, _ = io.Copy(io.Discard, conn) // hold the connection until the peer closes } func (d *veloDest) exchange(conn net.Conn) veloEvent { br := bufio.NewReader(conn) if _, err := readMCPacket(br); err != nil { // handshake return veloEvent{err: fmt.Errorf("read handshake: %w", err)} } if _, err := readMCPacket(br); err != nil { // login start return veloEvent{err: fmt.Errorf("read login start: %w", err)} } query := mcPacket(wire.NewWriter(). VarInt(0x04). // Login Plugin Request VarInt(veloMsgID). String("velocity:player_info"). U8(0x04). // max supported forwarding version Out()) if _, err := conn.Write(query); err != nil { return veloEvent{err: err} } resp, err := readMCPacket(br) if err != nil { return veloEvent{err: fmt.Errorf("read plugin response: %w", err)} } r := wire.NewReader(resp) id, _ := r.VarInt() if id != 0x02 { return veloEvent{err: fmt.Errorf("expected Login Plugin Response, got packet %#x", id)} } msgID, _ := r.VarInt() if !bytes.Equal(wire.AppendVarInt(nil, msgID), wire.AppendVarInt(nil, veloMsgID)) { return veloEvent{err: fmt.Errorf("message id not echoed: got %d", msgID)} } okFlag, _ := r.U8() if okFlag != 1 { return veloEvent{err: fmt.Errorf("response marked unsuccessful")} } sig, err := r.Bytes(32) if err != nil { return veloEvent{err: fmt.Errorf("missing signature: %w", err)} } payload := r.Remaining() mac := hmac.New(sha256.New, []byte(d.secret)) mac.Write(payload) if !hmac.Equal(sig, mac.Sum(nil)) { return veloEvent{err: fmt.Errorf("forwarding signature does not verify")} } pr := wire.NewReader(payload) var ev veloEvent ev.version, _ = pr.VarInt() ev.ip, _ = pr.String() ev.uuid, _ = pr.Bytes(16) ev.name, err = pr.String() if err != nil { return veloEvent{err: fmt.Errorf("truncated payload: %w", err)} } if props, err := pr.VarInt(); err != nil || props != 0 || len(pr.Remaining()) != 0 { return veloEvent{err: fmt.Errorf("unexpected properties/trailer in payload")} } return ev } func mcPacket(body []byte) []byte { return append(wire.AppendVarInt(nil, len(body)), body...) } func readMCPacket(br *bufio.Reader) ([]byte, error) { n, err := wire.ReadVarInt(br) if err != nil { return nil, err } if n <= 0 || n > 1<<20 { return nil, fmt.Errorf("bad packet length %d", n) } pkt := make([]byte, n) if _, err := io.ReadFull(br, pkt); err != nil { return nil, err } return pkt, nil } // ---- test ---- // TestVelocityForwarding drives a full player login through the hub and a // velocity-enabled mapping: the backend's velocity:player_info query must be // answered by the client (never reaching the player), carrying the player's // real IP, username and UUID, and the player's first bytes must be the // backend's Login Success. func TestVelocityForwarding(t *testing.T) { const psk = "e2e-velocity" const secret = "velo-forwarding-secret" port := freePort(t) hubAddr := fmt.Sprintf("127.0.0.1:%d", port) startHub(t, port, psk) dest := newVeloDest(t, secret) startClient(t, hubAddr, psk, 2, []client.Mapping{ {Pattern: `velo\.local`, Destination: dest.addr, VelocitySecret: secret}, }) pc := dialPlayer(t, hubAddr, "velo.local") // protocol 767, login intent defer pc.Close() uuid, _ := hex.DecodeString("00112233445566778899aabbccddeeff") loginStart := mcPacket(wire.NewWriter().VarInt(0x00).String("e2ePlayer").Bytes(uuid).Out()) if _, err := pc.Write(loginStart); err != nil { t.Fatalf("player login start: %v", err) } var ev veloEvent select { case ev = <-dest.events: case <-time.After(10 * time.Second): t.Fatalf("backend never completed the forwarding exchange") } if ev.err != nil { t.Fatalf("backend rejected the forwarding exchange: %v", ev.err) } if ev.version != 4 { t.Fatalf("forwarding version = %d, want 4 (lazy session)", ev.version) } if ev.ip != "127.0.0.1" { t.Fatalf("forwarded IP = %q, want the player's real 127.0.0.1", ev.ip) } if ev.name != "e2ePlayer" || !bytes.Equal(ev.uuid, uuid) { t.Fatalf("forwarded profile = %s/%x, want e2ePlayer/%x", ev.name, ev.uuid, uuid) } // The player must see the Login Success as its very first bytes — the // velocity query must have been swallowed by the client. got := make([]byte, len(dest.success)) _ = pc.SetReadDeadline(time.Now().Add(10 * time.Second)) if _, err := io.ReadFull(pc, got); err != nil { t.Fatalf("player read login success: %v", err) } if !bytes.Equal(got, dest.success) { t.Fatalf("player's first bytes are not the Login Success:\n got %x\nwant %x", got, dest.success) } }