M1: single-node OpenTunnel-compatible server
HTTP API per the recorded contract, bridge WebSocket, SNI relay with pass-through tunnel TLS, local CA issuance, JSON store. Verified end-to-end with the official opentunnel CLI.
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// clientHelloLimit bounds the bytes read while looking for a ClientHello
|
||||
// (the reference uses 64 KiB and 10 seconds).
|
||||
const (
|
||||
clientHelloLimit = 64 * 1024
|
||||
clientHelloTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// readClientHello reads TLS records from conn until the ClientHello is
|
||||
// complete, without consuming anything beyond it. It returns the exact bytes
|
||||
// read (for replay or forwarding), the SNI server name, and the offered ALPN
|
||||
// protocols. Reads are exact (no read-ahead), so conn is handed on intact.
|
||||
func readClientHello(conn net.Conn) (raw []byte, sni string, alpn []string, err error) {
|
||||
conn.SetReadDeadline(time.Now().Add(clientHelloTimeout))
|
||||
defer conn.SetReadDeadline(time.Time{})
|
||||
|
||||
var hello []byte // handshake message fragments, starting with the 4-byte header
|
||||
need := -1 // total ClientHello bytes once the header is known
|
||||
|
||||
for len(raw) < clientHelloLimit {
|
||||
header := make([]byte, 5)
|
||||
if _, err = io.ReadFull(conn, header); err != nil {
|
||||
return nil, "", nil, fmt.Errorf("reading record header: %w", err)
|
||||
}
|
||||
if header[0] != 22 { // handshake
|
||||
return nil, "", nil, fmt.Errorf("expected a handshake record, got %d", header[0])
|
||||
}
|
||||
bodyLen := int(binary.BigEndian.Uint16(header[3:]))
|
||||
if len(raw)+5+bodyLen > clientHelloLimit {
|
||||
return nil, "", nil, errors.New("ClientHello exceeds 64 KiB")
|
||||
}
|
||||
body := make([]byte, bodyLen)
|
||||
if _, err = io.ReadFull(conn, body); err != nil {
|
||||
return nil, "", nil, fmt.Errorf("reading record body: %w", err)
|
||||
}
|
||||
raw = append(raw, header...)
|
||||
raw = append(raw, body...)
|
||||
hello = append(hello, body...)
|
||||
|
||||
if need < 0 {
|
||||
if len(hello) < 4 {
|
||||
continue
|
||||
}
|
||||
if hello[0] != 1 { // client_hello
|
||||
return nil, "", nil, fmt.Errorf("expected ClientHello, got handshake type %d", hello[0])
|
||||
}
|
||||
need = 4 + int(hello[1])<<16 | int(hello[2])<<8 | int(hello[3])
|
||||
}
|
||||
if len(hello) >= need {
|
||||
sni, alpn = parseClientHello(hello[4:need])
|
||||
return raw, sni, alpn, nil
|
||||
}
|
||||
}
|
||||
return nil, "", nil, errors.New("ClientHello exceeds 64 KiB")
|
||||
}
|
||||
|
||||
// parseClientHello extracts SNI and ALPN from a ClientHello body (everything
|
||||
// after the 4-byte handshake header). Truncation is an error; a missing
|
||||
// extension simply yields an empty result, as the relay closes such
|
||||
// connections anyway.
|
||||
func parseClientHello(body []byte) (sni string, alpn []string) {
|
||||
p := parser{data: body}
|
||||
p.skip(2) // version
|
||||
p.skip(32) // random
|
||||
p.skip(int(p.u8())) // session id
|
||||
p.skip(int(p.u16())) // cipher suites
|
||||
p.skip(int(p.u8())) // compression methods
|
||||
extLen := int(p.u16()) // extensions block
|
||||
if p.err {
|
||||
return "", nil
|
||||
}
|
||||
exts := parser{data: p.take(extLen)}
|
||||
for !exts.empty() {
|
||||
typ := exts.u16()
|
||||
data := exts.take(int(exts.u16()))
|
||||
if exts.err {
|
||||
break
|
||||
}
|
||||
switch typ {
|
||||
case 0: // server_name
|
||||
d := parser{data: data}
|
||||
list := parser{data: d.take(int(d.u16()))}
|
||||
for !list.empty() {
|
||||
nameType := list.u8()
|
||||
name := list.take(int(list.u16()))
|
||||
if list.err {
|
||||
break
|
||||
}
|
||||
if nameType == 0 {
|
||||
sni = string(name)
|
||||
}
|
||||
}
|
||||
case 16: // application_layer_protocol_negotiation
|
||||
d := parser{data: data}
|
||||
list := parser{data: d.take(int(d.u16()))}
|
||||
for !list.empty() {
|
||||
proto := list.take(int(list.u8()))
|
||||
if list.err {
|
||||
break
|
||||
}
|
||||
alpn = append(alpn, string(proto))
|
||||
}
|
||||
}
|
||||
}
|
||||
return sni, alpn
|
||||
}
|
||||
|
||||
// parser is a cursor over a byte slice; any out-of-bounds read sets err and
|
||||
// yields zero values from then on.
|
||||
type parser struct {
|
||||
data []byte
|
||||
err bool
|
||||
}
|
||||
|
||||
func (p *parser) empty() bool { return p.err || len(p.data) == 0 }
|
||||
|
||||
func (p *parser) take(n int) []byte {
|
||||
if n < 0 || len(p.data) < n {
|
||||
p.err = true
|
||||
return nil
|
||||
}
|
||||
out := p.data[:n]
|
||||
p.data = p.data[n:]
|
||||
return out
|
||||
}
|
||||
|
||||
func (p *parser) skip(n int) { p.take(n) }
|
||||
|
||||
func (p *parser) u8() byte {
|
||||
b := p.take(1)
|
||||
if b == nil {
|
||||
return 0
|
||||
}
|
||||
return b[0]
|
||||
}
|
||||
|
||||
func (p *parser) u16() uint16 {
|
||||
b := p.take(2)
|
||||
if b == nil {
|
||||
return 0
|
||||
}
|
||||
return binary.BigEndian.Uint16(b)
|
||||
}
|
||||
Reference in New Issue
Block a user