Files
lpra/internal/tunnel/frame.go
T
2026-10-13 19:00:50 +03:00

101 lines
2.7 KiB
Go

// Package tunnel defines the framed multiplexing protocol used for DIRECT port
// forwarding over the dedicated host port (DefaultPort). Unlike the HTTP
// long-poll forward, this is a raw TCP stream the agent dials outbound; it
// carries many concurrent forwarded connections (streams) for one agent and
// has far lower latency than long-polling.
//
// Frame layout (big-endian):
//
// type(1) | stream(4) | length(4) | payload(length)
package tunnel
import (
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"io"
)
// DefaultPort is the host port for the direct tunnel (TCP and UDP).
const DefaultPort = 8446
// Frame types.
const (
TypeOpen byte = 1 // host->agent: open a TCP stream to 127.0.0.1:port (payload = port uint16)
TypeData byte = 2 // both: stream bytes
TypeClose byte = 3 // both: close the stream
TypePing byte = 4 // keepalive
)
// MaxPayload caps a single frame payload (1 MiB).
const MaxPayload = 1 << 20
// Frame is one multiplexed message.
type Frame struct {
Type byte
Stream uint32
Payload []byte
}
// WriteFrame writes a frame. The caller must serialize writes to one conn.
func WriteFrame(w io.Writer, f Frame) error {
var hdr [9]byte
hdr[0] = f.Type
binary.BigEndian.PutUint32(hdr[1:5], f.Stream)
binary.BigEndian.PutUint32(hdr[5:9], uint32(len(f.Payload)))
if _, err := w.Write(hdr[:]); err != nil {
return err
}
if len(f.Payload) > 0 {
_, err := w.Write(f.Payload)
return err
}
return nil
}
// ReadFrame reads one frame.
func ReadFrame(r io.Reader) (Frame, error) {
var hdr [9]byte
if _, err := io.ReadFull(r, hdr[:]); err != nil {
return Frame{}, err
}
n := binary.BigEndian.Uint32(hdr[5:9])
if n > MaxPayload {
return Frame{}, io.ErrUnexpectedEOF
}
f := Frame{Type: hdr[0], Stream: binary.BigEndian.Uint32(hdr[1:5])}
if n > 0 {
f.Payload = make([]byte, n)
if _, err := io.ReadFull(r, f.Payload); err != nil {
return Frame{}, err
}
}
return f, nil
}
// AuthKey derives the shared HMAC key from an agent token: sha256(token).
// The server holds the same value as the stored token hash, so neither side
// ever puts the plaintext token on the wire.
func AuthKey(token string) []byte {
sum := sha256.Sum256([]byte(token))
return sum[:]
}
// KeyFromHash decodes a stored hex token hash into the HMAC key bytes.
func KeyFromHash(tokenHashHex string) ([]byte, error) {
return hex.DecodeString(tokenHashHex)
}
// Sign returns hex(HMAC-SHA256(key, nonce)).
func Sign(key, nonce []byte) string {
m := hmac.New(sha256.New, key)
m.Write(nonce)
return hex.EncodeToString(m.Sum(nil))
}
// EqualSig compares two hex signatures in constant time.
func EqualSig(a, b string) bool {
return hmac.Equal([]byte(a), []byte(b))
}