Files
go2rtc/pkg/wyze/tutk/conn.go
T
2026-01-15 01:11:06 +01:00

1018 lines
25 KiB
Go

package tutk
import (
"context"
"crypto/hmac"
"crypto/rand"
"crypto/sha1"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"io"
"net"
"sync"
"time"
"github.com/AlexxIT/go2rtc/pkg/wyze/crypto"
"github.com/pion/dtls/v3"
)
const (
MaxPacketSize = 2048
ReadBufferSize = 2 * 1024 * 1024
DiscoTimeout = 5000 * time.Millisecond
DiscoInterval = 100 * time.Millisecond
SessionTimeout = 5000 * time.Millisecond
ReadWaitInterval = 50 * time.Millisecond
)
type Conn struct {
conn *net.UDPConn
addr *net.UDPAddr
frames *FrameHandler
err error
verbose bool
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
mu sync.RWMutex
// DTLS
clientConn *dtls.Conn
serverConn *dtls.Conn
clientBuf chan []byte
serverBuf chan []byte
rawCmd chan []byte
// Identity
uid string
authKey string
enr string
mac string
psk []byte
rid []byte
// Session
sid []byte
ticket uint16
avResp *AVLoginResponse
// Protocol
newProto bool
seq uint16
seqCmd uint16
avSeq uint32
kaSeq uint32
audioSeq uint32
audioFrameNo uint32
// Ack
ackFlags uint16
rxSeqStart uint16
rxSeqEnd uint16
rxSeqInit bool
cmdAck func()
}
func Dial(host string, port int, uid, authKey, enr, mac string, verbose bool) (*Conn, error) {
udp, err := net.ListenUDP("udp", nil)
if err != nil {
return nil, err
}
_ = udp.SetReadBuffer(ReadBufferSize)
ctx, cancel := context.WithCancel(context.Background())
psk := derivePSK(enr)
if port == 0 {
port = DefaultPort
}
c := &Conn{
conn: udp,
addr: &net.UDPAddr{IP: net.ParseIP(host), Port: port},
rid: genRandomID(),
uid: uid,
authKey: authKey,
enr: enr,
mac: mac,
psk: psk,
verbose: verbose,
ctx: ctx,
cancel: cancel,
rxSeqStart: 0xffff, // Initialize RX seq for ACK
rxSeqEnd: 0xffff,
}
if err = c.discovery(); err != nil {
_ = c.Close()
return nil, err
}
c.clientBuf = make(chan []byte, 64)
c.serverBuf = make(chan []byte, 64)
c.rawCmd = make(chan []byte, 16)
c.frames = NewFrameHandler(c.verbose)
c.wg.Add(1)
go c.reader()
if err = c.connect(); err != nil {
_ = c.Close()
return nil, err
}
c.wg.Add(1)
go c.worker()
return c, nil
}
func (c *Conn) AVClientStart(timeout time.Duration) error {
randomID := genRandomID()
pkt1 := c.buildAVLoginPacket(MagicAVLogin1, 570, 0x0001, randomID)
pkt2 := c.buildAVLoginPacket(MagicAVLogin2, 572, 0x0000, randomID)
pkt2[20]++ // pkt2 has randomID incremented by 1
if _, err := c.clientConn.Write(pkt1); err != nil {
return fmt.Errorf("av login 1 failed: %w", err)
}
time.Sleep(50 * time.Millisecond)
if _, err := c.clientConn.Write(pkt2); err != nil {
return fmt.Errorf("av login 2 failed: %w", err)
}
// Wait for response
timer := time.NewTimer(timeout)
defer timer.Stop()
for {
select {
case data, ok := <-c.rawCmd:
if !ok {
return io.EOF
}
if len(data) >= 32 && binary.LittleEndian.Uint16(data) == MagicAVLoginResp {
c.avResp = &AVLoginResponse{
ServerType: binary.LittleEndian.Uint32(data[4:]),
Resend: int32(data[29]),
TwoWayStreaming: int32(data[31]),
}
ack := c.buildACK()
c.clientConn.Write(ack)
c.wg.Add(1)
go func() {
defer c.wg.Done()
ackTicker := time.NewTicker(100 * time.Millisecond)
defer ackTicker.Stop()
for {
select {
case <-c.ctx.Done():
return
case <-ackTicker.C:
if c.clientConn != nil {
ack := c.buildACK()
c.clientConn.Write(ack)
}
}
}
}()
return nil
}
case <-timer.C:
return context.DeadlineExceeded
}
}
}
func (c *Conn) AVServStart() error {
if c.verbose {
fmt.Printf("[DTLS] Waiting for client handshake on channel %d\n", IOTCChannelBack)
fmt.Printf("[DTLS] PSK Identity: %s\n", PSKIdentity)
fmt.Printf("[DTLS] PSK Key: %s\n", hex.EncodeToString(c.psk))
}
conn, err := NewDtlsServer(c, IOTCChannelBack, c.psk)
if err != nil {
return fmt.Errorf("dtls: server handshake failed: %w", err)
}
c.mu.Lock()
c.serverConn = conn
c.mu.Unlock()
if c.verbose {
fmt.Printf("[DTLS] Server handshake complete on channel %d\n", IOTCChannelBack)
}
// Wait for and respond to AV Login request from camera
if err := c.handleSpeakerAVLogin(); err != nil {
return fmt.Errorf("speaker av login failed: %w", err)
}
return nil
}
func (c *Conn) AVServStop() error {
c.mu.Lock()
defer c.mu.Unlock()
// Reset audio TX state
c.audioSeq = 0
c.audioFrameNo = 0
if c.serverConn != nil {
err := c.serverConn.Close()
c.serverConn = nil
return err
}
return nil
}
func (c *Conn) AVRecvFrameData() (*Packet, error) {
select {
case pkt, ok := <-c.frames.Recv():
if !ok {
return nil, c.Error()
}
return pkt, nil
case <-c.ctx.Done():
return nil, c.Error()
}
}
func (c *Conn) AVSendAudioData(codec uint16, payload []byte, timestampUS uint32, sampleRate uint32, channels uint8) error {
c.mu.Lock()
conn := c.serverConn
if conn == nil {
c.mu.Unlock()
return fmt.Errorf("speaker channel not connected")
}
frame := c.buildAudioFrame(payload, timestampUS, codec, sampleRate, channels)
c.mu.Unlock()
n, err := conn.Write(frame)
if c.verbose {
if err != nil {
fmt.Printf("[SPEAKER TX] DTLS Write ERROR: %v\n", err)
} else {
fmt.Printf("[SPEAKER TX] len=%d, data:\n%s", n, hexDump(frame))
}
}
return err
}
func (c *Conn) Write(data []byte) error {
// if c.verbose {
// fmt.Printf("[UDP TX] to=%s, len=%d, data:\n%s", c.addr.String(), len(data), hexDump(data))
// }
if c.newProto {
_, err := c.conn.WriteToUDP(data, c.addr)
return err
}
_, err := c.conn.WriteToUDP(crypto.TransCodeBlob(data), c.addr)
return err
}
func (c *Conn) WriteDTLS(payload []byte, channel byte) error {
var frame []byte
if c.newProto {
frame = c.buildNewTxData(payload, channel)
} else {
frame = c.buildTxData(payload, channel)
}
// if c.verbose {
// fmt.Printf("[DTLS TX] to=%s, len=%d, channel=%d, data:\n%s", c.addr.String(), len(frame), channel, hexDump(frame))
// }
return c.Write(frame)
}
func (c *Conn) WriteAndWait(req []byte, timeout time.Duration, ok func(res []byte) bool) ([]byte, error) {
var t *time.Timer
t = time.AfterFunc(1, func() {
if err := c.Write(req); err == nil && t != nil {
t.Reset(time.Second)
}
})
defer t.Stop()
_ = c.conn.SetDeadline(time.Now().Add(timeout))
defer c.conn.SetDeadline(time.Time{})
buf := make([]byte, MaxPacketSize)
for {
n, addr, err := c.conn.ReadFromUDP(buf)
if err != nil {
return nil, err
}
if string(addr.IP) != string(c.addr.IP) || n < 16 {
continue
}
var res []byte
if c.newProto {
res = buf[:n]
} else {
res = crypto.ReverseTransCodeBlob(buf[:n])
}
if ok(res) {
c.addr.Port = addr.Port
return res, nil
}
}
}
func (c *Conn) WriteAndWaitIOCtrl(cmd uint16, payload []byte, expectCmd uint16, timeout time.Duration) ([]byte, error) {
frame := c.buildIOCtrlFrame(payload)
var t *time.Timer
t = time.AfterFunc(1, func() {
if _, err := c.clientConn.Write(frame); err == nil && t != nil {
t.Reset(time.Second)
}
})
defer t.Stop()
timer := time.NewTimer(timeout)
defer timer.Stop()
for {
select {
case data, ok := <-c.rawCmd:
if !ok {
return nil, io.EOF
}
ack := c.buildACK()
c.clientConn.Write(ack)
if len(data) >= 6 {
if binary.LittleEndian.Uint16(data[4:]) == expectCmd {
return data, nil
}
}
case <-timer.C:
return nil, fmt.Errorf("timeout waiting for K%d", expectCmd)
}
}
}
func (c *Conn) GetAVLoginResponse() *AVLoginResponse {
return c.avResp
}
func (c *Conn) IsBackchannelReady() bool {
c.mu.RLock()
defer c.mu.RUnlock()
return c.serverConn != nil
}
func (c *Conn) RemoteAddr() *net.UDPAddr {
return c.addr
}
func (c *Conn) LocalAddr() *net.UDPAddr {
return c.conn.LocalAddr().(*net.UDPAddr)
}
func (c *Conn) SetDeadline(t time.Time) error {
return c.conn.SetDeadline(t)
}
func (c *Conn) Close() error {
c.cancel()
c.mu.Lock()
if c.clientConn != nil {
c.clientConn.Close()
c.clientConn = nil
}
if c.serverConn != nil {
c.serverConn.Close()
c.serverConn = nil
}
if c.frames != nil {
c.frames.Close()
}
c.mu.Unlock()
c.wg.Wait()
return c.conn.Close()
}
func (c *Conn) Error() error {
if c.err != nil {
return c.err
}
return io.EOF
}
func (c *Conn) discovery() error {
c.sid = make([]byte, 8)
rand.Read(c.sid)
oldPkt := crypto.TransCodeBlob(c.buildDisco(1))
newPkt := c.buildNewDisco(0, 0, false)
buf := make([]byte, MaxPacketSize)
deadline := time.Now().Add(DiscoTimeout)
for time.Now().Before(deadline) {
c.conn.WriteToUDP(oldPkt, c.addr)
c.conn.WriteToUDP(newPkt, c.addr)
c.conn.SetReadDeadline(time.Now().Add(DiscoInterval))
n, addr, err := c.conn.ReadFromUDP(buf)
if err != nil {
continue
}
if !addr.IP.Equal(c.addr.IP) {
continue
}
// NEW protocol
if n >= NewPacketSize && binary.LittleEndian.Uint16(buf[:2]) == MagicNewProto {
if binary.LittleEndian.Uint16(buf[4:]) == CmdNewDisco {
c.addr, c.newProto, c.ticket = addr, true, binary.LittleEndian.Uint16(buf[14:])
if n >= 24 {
copy(c.sid, buf[16:24])
}
return c.newDiscoDone()
}
continue
}
// OLD protocol
data := crypto.ReverseTransCodeBlob(buf[:n])
if len(data) >= 16 && binary.LittleEndian.Uint16(data[8:]) == CmdDiscoRes {
c.addr, c.newProto = addr, false
return c.oldDiscoDone()
}
}
return fmt.Errorf("discovery timeout")
}
func (c *Conn) oldDiscoDone() error {
c.Write(c.buildDisco(2))
time.Sleep(100 * time.Millisecond)
_, err := c.WriteAndWait(c.buildSession(), SessionTimeout, func(res []byte) bool {
return len(res) >= 16 && binary.LittleEndian.Uint16(res[8:]) == CmdSessionRes
})
return err
}
func (c *Conn) newDiscoDone() error {
_, err := c.WriteAndWait(c.buildNewDisco(2, c.ticket, false), SessionTimeout, func(res []byte) bool {
if len(res) < NewPacketSize || binary.LittleEndian.Uint16(res[:2]) != MagicNewProto {
return false
}
cmd := binary.LittleEndian.Uint16(res[4:])
dir := binary.LittleEndian.Uint16(res[8:])
seq := binary.LittleEndian.Uint16(res[12:])
return cmd == CmdNewDisco && dir == 0xFFFF && seq == 3
})
return err
}
func (c *Conn) connect() error {
if c.verbose {
fmt.Printf("[DTLS] Starting client handshake on channel %d\n", IOTCChannelMain)
fmt.Printf("[DTLS] PSK Identity: %s\n", PSKIdentity)
fmt.Printf("[DTLS] PSK Key: %s\n", hex.EncodeToString(c.psk))
}
conn, err := NewDtlsClient(c, IOTCChannelMain, c.psk)
if err != nil {
return fmt.Errorf("dtls: client create failed: %w", err)
}
c.mu.Lock()
c.clientConn = conn
c.mu.Unlock()
if c.verbose {
fmt.Printf("[DTLS] Client created for channel %d\n", IOTCChannelMain)
}
return nil
}
func (c *Conn) worker() {
defer c.wg.Done()
buf := make([]byte, 2048)
for {
select {
case <-c.ctx.Done():
return
default:
}
n, err := c.clientConn.Read(buf)
if err != nil {
c.err = err
return
}
if n < 2 {
continue
}
data := buf[:n]
magic := binary.LittleEndian.Uint16(data)
// if c.verbose {
// fmt.Printf("[DTLS RX] from=%s, len=%d, data:\n%s", c.addr.String(), n, hexDump(data))
// }
switch magic {
case MagicAVLoginResp:
c.queue(c.rawCmd, data)
case MagicIOCtrl:
if len(data) >= 32 {
for i := 32; i+2 < len(data); i++ {
if data[i] == 'H' && data[i+1] == 'L' {
c.queue(c.rawCmd, data[i:])
break
}
}
}
case MagicChannelMsg:
if len(data) >= 36 && data[16] == 0x00 {
for i := 36; i+2 < len(data); i++ {
if data[i] == 'H' && data[i+1] == 'L' {
c.queue(c.rawCmd, data[i:])
break
}
}
}
case ProtoVersion:
if len(data) >= 8 {
// Extract seq number at byte 4-5 (uint16 of uint32 AVSeq)
seq := binary.LittleEndian.Uint16(data[4:])
if !c.rxSeqInit {
c.rxSeqInit = true
}
// Track highest received sequence
if seq > c.rxSeqEnd || c.rxSeqEnd == 0xffff {
c.rxSeqEnd = seq
}
// Check for HL command response
if len(data) >= 36 {
for i := 32; i+2 < len(data); i++ {
if data[i] == 'H' && data[i+1] == 'L' {
c.queue(c.rawCmd, data[i:])
break
}
}
}
}
case MagicACK:
c.mu.RLock()
ack := c.cmdAck
c.mu.RUnlock()
if ack != nil {
ack()
}
default:
channel := data[0]
if channel == ChannelAudio || channel == ChannelIVideo || channel == ChannelPVideo {
c.frames.Handle(data)
}
}
}
}
func (c *Conn) reader() {
defer c.wg.Done()
buf := make([]byte, MaxPacketSize)
for {
select {
case <-c.ctx.Done():
return
default:
}
c.conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
n, addr, err := c.conn.ReadFromUDP(buf)
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
continue
}
return
}
// if c.verbose {
// fmt.Printf("[UDP RX] from=%s, len=%d, data:\n%s", addr.String(), n, hexDump(buf[:n]))
// }
if !addr.IP.Equal(c.addr.IP) {
if c.verbose {
fmt.Printf("Ignored packet from unknown IP: %s\n", addr.IP.String())
}
continue
}
if addr.Port != c.addr.Port {
c.addr.Port = addr.Port
}
// NEW protocol (0xCC51)
if c.newProto && n >= 12 && binary.LittleEndian.Uint16(buf[:2]) == MagicNewProto {
cmd := binary.LittleEndian.Uint16(buf[4:])
switch cmd {
case CmdNewKeepalive:
if n >= NewKeepaliveSize {
_ = c.Write(c.buildNewKeepalive())
}
case CmdNewDTLS:
if n >= NewHeaderSize+NewAuthSize {
ch := byte(binary.LittleEndian.Uint16(buf[12:]) >> 8)
dtls := buf[NewHeaderSize : n-NewAuthSize]
switch ch {
case IOTCChannelMain:
c.queue(c.clientBuf, dtls)
case IOTCChannelBack:
c.queue(c.serverBuf, dtls)
}
}
}
continue
}
// OLD protocol (TransCode)
data := crypto.ReverseTransCodeBlob(buf[:n])
if len(data) < 16 {
continue
}
switch binary.LittleEndian.Uint16(data[8:]) {
case CmdKeepaliveRes:
if len(data) > 24 {
_ = c.Write(c.buildKeepAlive(data[16:]))
}
case CmdDataRX:
if len(data) > 28 {
ch := data[14]
switch ch {
case IOTCChannelMain:
c.queue(c.clientBuf, data[28:])
case IOTCChannelBack:
c.queue(c.serverBuf, data[28:])
}
}
}
}
}
func (c *Conn) queue(ch chan []byte, data []byte) {
b := make([]byte, len(data))
copy(b, data)
select {
case ch <- b:
default:
select {
case <-ch:
default:
}
ch <- b
}
}
func (c *Conn) handleSpeakerAVLogin() error {
if c.verbose {
fmt.Printf("[SPEAK] Waiting for AV Login request from camera...\n")
}
buf := make([]byte, 1024)
c.serverConn.SetReadDeadline(time.Now().Add(5 * time.Second))
n, err := c.serverConn.Read(buf)
if err != nil {
return fmt.Errorf("read av login: %w", err)
}
if c.verbose {
fmt.Printf("[SPEAK] AV Login request len=%d data:\n%s", n, hexDump(buf[:n]))
}
if n < 24 {
return fmt.Errorf("av login too short: %d bytes", n)
}
checksum := binary.LittleEndian.Uint32(buf[20:])
resp := c.buildAVLoginResponse(checksum)
if c.verbose {
fmt.Printf("[SPEAK] Sending AV Login response: %d bytes\n", len(resp))
}
if _, err = c.serverConn.Write(resp); err != nil {
return fmt.Errorf("write AV login response: %w", err)
}
// Camera may resend, respond again
c.serverConn.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
if n, _ = c.serverConn.Read(buf); n > 0 {
if c.verbose {
fmt.Printf("[SPEAK] Received AV Login resend: %d bytes\n", n)
}
c.serverConn.Write(resp)
}
c.serverConn.SetReadDeadline(time.Time{})
if c.verbose {
fmt.Printf("[SPEAK] AV Login complete, ready for audio\n")
}
return nil
}
func (c *Conn) buildDisco(stage byte) []byte {
b := make([]byte, OldDiscoSize)
copy(b, "\x04\x02\x1a\x02") // marker + mode
binary.LittleEndian.PutUint16(b[4:], OldDiscoBodySize) // body size
binary.LittleEndian.PutUint16(b[8:], CmdDiscoReq) // 0x0601
binary.LittleEndian.PutUint16(b[10:], 0x0021) // flags
body := b[OldHeaderSize:]
copy(body[:UIDSize], c.uid)
copy(body[36:], "\x01\x01\x02\x04") // unknown
copy(body[40:], c.rid)
body[48] = stage
if stage == 1 && len(c.authKey) > 0 {
copy(body[58:], c.authKey)
}
return b
}
func (c *Conn) buildNewDisco(seq, ticket uint16, isResponse bool) []byte {
b := make([]byte, NewPacketSize)
binary.LittleEndian.PutUint16(b[0:], MagicNewProto) // 0xCC51
binary.LittleEndian.PutUint16(b[4:], CmdNewDisco) // 0x1002
binary.LittleEndian.PutUint16(b[6:], NewPayloadSize) // 40 bytes
if isResponse {
binary.LittleEndian.PutUint16(b[8:], 0xFFFF) // response
}
binary.LittleEndian.PutUint16(b[12:], seq)
binary.LittleEndian.PutUint16(b[14:], ticket)
copy(b[16:24], c.sid)
copy(b[24:32], "\x00\x08\x03\x04\x1d\x00\x00\x00") // SDK 4.3.8.0
authKey := crypto.CalculateAuthKey(c.enr, c.mac)
h := hmac.New(sha1.New, append([]byte(c.uid), authKey...))
h.Write(b[:32])
copy(b[32:52], h.Sum(nil))
return b
}
func (c *Conn) buildNewKeepalive() []byte {
c.kaSeq += 2
b := make([]byte, NewKeepaliveSize)
binary.LittleEndian.PutUint16(b[0:], MagicNewProto) // 0xCC51
binary.LittleEndian.PutUint16(b[4:], CmdNewKeepalive) // 0x1202
binary.LittleEndian.PutUint16(b[6:], 0x0024) // 36 bytes payload
binary.LittleEndian.PutUint32(b[16:], c.kaSeq) // counter
copy(b[20:28], c.sid) // session ID
authKey := crypto.CalculateAuthKey(c.enr, c.mac)
h := hmac.New(sha1.New, append([]byte(c.uid), authKey...))
h.Write(b[:28])
copy(b[28:48], h.Sum(nil))
return b
}
func (c *Conn) buildSession() []byte {
b := make([]byte, OldSessionSize)
copy(b, "\x04\x02\x1a\x02") // marker + mode
binary.LittleEndian.PutUint16(b[4:], OldSessionBody) // body size
binary.LittleEndian.PutUint16(b[8:], CmdSessionReq) // 0x0402
binary.LittleEndian.PutUint16(b[10:], 0x0033) // flags
body := b[OldHeaderSize:]
copy(body[:UIDSize], c.uid)
copy(body[UIDSize:], c.rid)
binary.LittleEndian.PutUint32(body[32:], uint32(time.Now().Unix()))
return b
}
func (c *Conn) buildAVLoginPacket(magic uint16, size int, flags uint16, randomID []byte) []byte {
b := make([]byte, size)
binary.LittleEndian.PutUint16(b, magic)
binary.LittleEndian.PutUint16(b[2:], ProtoVersion)
binary.LittleEndian.PutUint16(b[16:], uint16(size-24)) // payload size
binary.LittleEndian.PutUint16(b[18:], flags)
copy(b[20:], randomID[:4])
copy(b[24:], DefaultUser) // username
copy(b[280:], c.enr) // password/ENR
// binary.LittleEndian.PutUint32(b[536:], 1) // resend enabled
binary.LittleEndian.PutUint32(b[540:], 4) // security_mode ?
binary.LittleEndian.PutUint32(b[552:], DefaultCaps) // capabilities
return b
}
func (c *Conn) buildAVLoginResponse(checksum uint32) []byte {
b := make([]byte, 60)
binary.LittleEndian.PutUint16(b, 0x2100) // magic
binary.LittleEndian.PutUint16(b[2:], 0x000c) // version
b[4] = 0x10 // success
binary.LittleEndian.PutUint32(b[16:], 0x24) // payload size
binary.LittleEndian.PutUint32(b[20:], checksum) // echo checksum
b[29] = 0x01 // enable flag
b[31] = 0x01 // two-way streaming
binary.LittleEndian.PutUint32(b[36:], 0x04) // buffer config
binary.LittleEndian.PutUint32(b[40:], DefaultCaps)
binary.LittleEndian.PutUint16(b[54:], 0x0003) // channel info
binary.LittleEndian.PutUint16(b[56:], 0x0002)
return b
}
func (c *Conn) buildAudioFrame(payload []byte, timestampUS uint32, codec uint16, sampleRate uint32, channels uint8) []byte {
c.audioSeq++
c.audioFrameNo++
prevFrame := uint32(0)
if c.audioFrameNo > 1 {
prevFrame = c.audioFrameNo - 1
}
totalPayload := len(payload) + 16 // payload + frameinfo
b := make([]byte, 36+totalPayload)
// Outer header (36 bytes)
b[0] = ChannelAudio // 0x03
b[1] = FrameTypeStartAlt // 0x09
binary.LittleEndian.PutUint16(b[2:], ProtoVersion)
binary.LittleEndian.PutUint32(b[4:], c.audioSeq)
binary.LittleEndian.PutUint32(b[8:], timestampUS)
if c.audioFrameNo == 1 {
binary.LittleEndian.PutUint32(b[12:], 0x00000001)
} else {
binary.LittleEndian.PutUint32(b[12:], 0x00100001)
}
// Inner header
b[16] = ChannelAudio
b[17] = FrameTypeEndSingle
binary.LittleEndian.PutUint16(b[18:], uint16(prevFrame))
binary.LittleEndian.PutUint16(b[20:], 0x0001) // pkt_total
binary.LittleEndian.PutUint16(b[22:], 0x0010) // flags
binary.LittleEndian.PutUint32(b[24:], uint32(totalPayload))
binary.LittleEndian.PutUint32(b[28:], prevFrame)
binary.LittleEndian.PutUint32(b[32:], c.audioFrameNo)
copy(b[36:], payload) // Payload + FrameInfo
fi := b[36+len(payload):]
binary.LittleEndian.PutUint16(fi, codec)
fi[2] = BuildAudioFlags(sampleRate, true, channels == 2)
fi[4] = 1 // online
binary.LittleEndian.PutUint32(fi[12:], (c.audioFrameNo-1)*GetSamplesPerFrame(codec)*1000/sampleRate)
return b
}
func (c *Conn) buildTxData(payload []byte, channel byte) []byte {
bodySize := 12 + len(payload)
b := make([]byte, 16+bodySize)
copy(b, "\x04\x02\x1a\x0b") // marker + mode=data
binary.LittleEndian.PutUint16(b[4:], uint16(bodySize)) // body size
binary.LittleEndian.PutUint16(b[6:], c.seq) // sequence
c.seq++
binary.LittleEndian.PutUint16(b[8:], CmdDataTX) // 0x0407
binary.LittleEndian.PutUint16(b[10:], 0x0021) // flags
copy(b[12:], c.rid[:2]) // rid[0:2]
b[14] = channel // channel
b[15] = 0x01 // marker
binary.LittleEndian.PutUint32(b[16:], 0x0000000c) // const
copy(b[20:], c.rid[:8]) // rid
copy(b[28:], payload)
return b
}
func (c *Conn) buildNewTxData(payload []byte, channel byte) []byte {
payloadSize := uint16(16 + len(payload) + NewAuthSize)
b := make([]byte, NewHeaderSize+len(payload)+NewAuthSize)
binary.LittleEndian.PutUint16(b[0:], MagicNewProto) // 0xCC51
binary.LittleEndian.PutUint16(b[4:], CmdNewDTLS) // 0x1502
binary.LittleEndian.PutUint16(b[6:], payloadSize)
binary.LittleEndian.PutUint16(b[12:], uint16(0x0010)|(uint16(channel)<<8)) // channel in high byte
binary.LittleEndian.PutUint16(b[14:], c.ticket)
copy(b[16:24], c.sid)
binary.LittleEndian.PutUint32(b[24:], 1) // const
copy(b[NewHeaderSize:], payload)
authKey := crypto.CalculateAuthKey(c.enr, c.mac)
h := hmac.New(sha1.New, append([]byte(c.uid), authKey...))
h.Write(b[:NewHeaderSize])
copy(b[NewHeaderSize+len(payload):], h.Sum(nil))
return b
}
func (c *Conn) buildACK() []byte {
c.ackFlags++
b := make([]byte, 24)
binary.LittleEndian.PutUint16(b[0:], MagicACK) // 0x0009
binary.LittleEndian.PutUint16(b[2:], ProtoVersion) // 0x000c
binary.LittleEndian.PutUint32(b[4:], c.avSeq) // TX seq
c.avSeq++
binary.LittleEndian.PutUint16(b[8:], c.rxSeqStart) // RX start (last acked)
binary.LittleEndian.PutUint16(b[10:], c.rxSeqEnd) // RX end (highest received)
if c.rxSeqInit {
c.rxSeqStart = c.rxSeqEnd
}
binary.LittleEndian.PutUint16(b[12:], c.ackFlags) // AckFlags
binary.LittleEndian.PutUint32(b[16:], uint32(c.ackFlags)<<16) // AckCounter
ts := uint32(time.Now().UnixMilli() & 0xFFFF)
binary.LittleEndian.PutUint16(b[20:], uint16(ts)) // Timestamp
return b
}
func (c *Conn) buildKeepAlive(incoming []byte) []byte {
b := make([]byte, 24)
copy(b, "\x04\x02\x1a\x0a") // marker + mode
binary.LittleEndian.PutUint16(b[4:], 8) // body size
binary.LittleEndian.PutUint16(b[8:], CmdKeepaliveReq) // 0x0427
binary.LittleEndian.PutUint16(b[10:], 0x0021) // flags
if len(incoming) >= 8 {
copy(b[16:], incoming[:8]) // echo payload
}
return b
}
func (c *Conn) buildIOCtrlFrame(payload []byte) []byte {
b := make([]byte, 40+len(payload))
binary.LittleEndian.PutUint16(b, ProtoVersion) // magic
binary.LittleEndian.PutUint16(b[2:], ProtoVersion) // version
binary.LittleEndian.PutUint32(b[4:], c.avSeq) // av seq
c.avSeq++
binary.LittleEndian.PutUint16(b[16:], MagicIOCtrl) // 0x7000
binary.LittleEndian.PutUint16(b[18:], c.seqCmd) // sub channel
binary.LittleEndian.PutUint32(b[20:], 1) // ioctl seq
binary.LittleEndian.PutUint32(b[24:], uint32(len(payload)+4)) // payload size
binary.LittleEndian.PutUint32(b[28:], uint32(c.seqCmd)) // flag
b[37] = 0x01
copy(b[40:], payload)
c.seqCmd++
return b
}
func derivePSK(enr string) []byte {
// TUTK SDK treats the PSK as a NULL-terminated C string, so if SHA256(ENR)
// contains a 0x00 byte, the PSK is truncated at that position.
// bytes after the first 0x00 are padded with zeros to make a 32-byte key.
hash := sha256.Sum256([]byte(enr))
pskLen := 32
for i := range 32 {
if hash[i] == 0x00 {
pskLen = i
break
}
}
psk := make([]byte, 32)
copy(psk[:pskLen], hash[:pskLen])
return psk
}
func genRandomID() []byte {
b := make([]byte, 8)
_, _ = rand.Read(b)
return b
}
func hexDump(data []byte) string {
const maxBytes = 650
totalLen := len(data)
truncated := totalLen > maxBytes
if truncated {
data = data[:maxBytes]
}
var result string
for i := 0; i < len(data); i += 16 {
end := min(i+16, len(data))
line := fmt.Sprintf(" %04x:", i)
for j := i; j < end; j++ {
line += fmt.Sprintf(" %02x", data[j])
}
result += line + "\n"
}
if truncated {
result += fmt.Sprintf(" ... (truncated, showing %d of %d bytes)\n", maxBytes, totalLen)
}
return result
}