Add OpenVPN, TrustTunnel, Sudoku, inbound managers. Fixes

This commit is contained in:
Shtorm
2026-06-04 01:47:50 +03:00
parent 9b3da79c32
commit 195a33379d
164 changed files with 16665 additions and 1332 deletions

View File

@@ -0,0 +1,161 @@
package httpmask
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/binary"
"strings"
"time"
"net/http"
)
const (
tunnelAuthHeaderKey = "Authorization"
tunnelAuthHeaderPrefix = "Bearer "
tunnelAuthQueryKey = "auth"
)
type tunnelAuth struct {
key [32]byte // derived HMAC key
skew time.Duration
}
func newTunnelAuth(key string, skew time.Duration) *tunnelAuth {
key = strings.TrimSpace(key)
if key == "" {
return nil
}
if skew <= 0 {
skew = 60 * time.Second
}
// Domain separation: keep this HMAC key independent from other uses of cfg.Key.
h := sha256.New()
_, _ = h.Write([]byte("sudoku-httpmask-auth-v1:"))
_, _ = h.Write([]byte(key))
var sum [32]byte
h.Sum(sum[:0])
return &tunnelAuth{key: sum, skew: skew}
}
func (a *tunnelAuth) token(mode TunnelMode, method, path string, now time.Time) string {
if a == nil {
return ""
}
ts := now.Unix()
sig := a.sign(mode, method, path, ts)
var buf [8 + 16]byte
binary.BigEndian.PutUint64(buf[:8], uint64(ts))
copy(buf[8:], sig[:])
return base64.RawURLEncoding.EncodeToString(buf[:])
}
func (a *tunnelAuth) verify(headers map[string]string, mode TunnelMode, method, path string, now time.Time) bool {
if a == nil {
return true
}
if headers == nil {
return false
}
return a.verifyValue(headers["authorization"], mode, method, path, now)
}
func (a *tunnelAuth) verifyValue(val string, mode TunnelMode, method, path string, now time.Time) bool {
if a == nil {
return true
}
val = strings.TrimSpace(val)
if val == "" {
return false
}
// Accept both "Bearer <token>" and raw token forms (for forward proxies / CDNs that may normalize headers).
if len(val) > len(tunnelAuthHeaderPrefix) && strings.EqualFold(val[:len(tunnelAuthHeaderPrefix)], tunnelAuthHeaderPrefix) {
val = strings.TrimSpace(val[len(tunnelAuthHeaderPrefix):])
}
if val == "" {
return false
}
raw, err := base64.RawURLEncoding.DecodeString(val)
if err != nil || len(raw) != 8+16 {
return false
}
ts := int64(binary.BigEndian.Uint64(raw[:8]))
nowTS := now.Unix()
delta := nowTS - ts
if delta < 0 {
delta = -delta
}
if delta > int64(a.skew.Seconds()) {
return false
}
want := a.sign(mode, method, path, ts)
return subtle.ConstantTimeCompare(raw[8:], want[:]) == 1
}
func (a *tunnelAuth) sign(mode TunnelMode, method, path string, ts int64) [16]byte {
method = strings.ToUpper(strings.TrimSpace(method))
if method == "" {
method = "GET"
}
path = strings.TrimSpace(path)
var tsBuf [8]byte
binary.BigEndian.PutUint64(tsBuf[:], uint64(ts))
mac := hmac.New(sha256.New, a.key[:])
_, _ = mac.Write([]byte(mode))
_, _ = mac.Write([]byte{0})
_, _ = mac.Write([]byte(method))
_, _ = mac.Write([]byte{0})
_, _ = mac.Write([]byte(path))
_, _ = mac.Write([]byte{0})
_, _ = mac.Write(tsBuf[:])
var full [32]byte
mac.Sum(full[:0])
var out [16]byte
copy(out[:], full[:16])
return out
}
type httpHeaderSetter = http.Header
func applyTunnelAuthHeader(h httpHeaderSetter, auth *tunnelAuth, mode TunnelMode, method, path string) {
if auth == nil || h == nil {
return
}
token := auth.token(mode, method, path, time.Now())
if token == "" {
return
}
h.Set(tunnelAuthHeaderKey, tunnelAuthHeaderPrefix+token)
}
func applyTunnelAuth(req *http.Request, auth *tunnelAuth, mode TunnelMode, method, path string) {
if auth == nil || req == nil {
return
}
token := auth.token(mode, method, path, time.Now())
if token == "" {
return
}
req.Header.Set(tunnelAuthHeaderKey, tunnelAuthHeaderPrefix+token)
if req.URL != nil {
q := req.URL.Query()
q.Set(tunnelAuthQueryKey, token)
req.URL.RawQuery = q.Encode()
}
}

View File

@@ -0,0 +1,174 @@
package httpmask
import (
"encoding/base64"
"errors"
"fmt"
"net"
"net/url"
"strings"
)
const (
tunnelEarlyDataQueryKey = "ed"
tunnelEarlyDataHeader = "X-Sudoku-Early"
)
type ClientEarlyHandshake struct {
RequestPayload []byte
HandleResponse func(payload []byte) error
Ready func() bool
WrapConn func(raw net.Conn) (net.Conn, error)
}
type TunnelServerEarlyHandshake struct {
Prepare func(payload []byte) (*PreparedServerEarlyHandshake, error)
}
type PreparedServerEarlyHandshake struct {
ResponsePayload []byte
WrapConn func(raw net.Conn) (net.Conn, error)
UserHash string
}
type earlyHandshakeMeta interface {
HTTPMaskEarlyHandshakeUserHash() string
}
type earlyHandshakeConn struct {
net.Conn
userHash string
}
func (c *earlyHandshakeConn) HTTPMaskEarlyHandshakeUserHash() string {
if c == nil {
return ""
}
return c.userHash
}
func wrapEarlyHandshakeConn(conn net.Conn, userHash string) net.Conn {
if conn == nil {
return nil
}
return &earlyHandshakeConn{Conn: conn, userHash: userHash}
}
func EarlyHandshakeUserHash(conn net.Conn) (string, bool) {
if conn == nil {
return "", false
}
v, ok := conn.(earlyHandshakeMeta)
if !ok {
return "", false
}
return v.HTTPMaskEarlyHandshakeUserHash(), true
}
type authorizeResponse struct {
token string
earlyPayload []byte
}
func isTunnelTokenByte(c byte) bool {
return (c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '-' ||
c == '_'
}
func parseAuthorizeResponse(body []byte) (*authorizeResponse, error) {
s := strings.TrimSpace(string(body))
idx := strings.Index(s, "token=")
if idx < 0 {
return nil, errors.New("missing token")
}
s = s[idx+len("token="):]
if s == "" {
return nil, errors.New("empty token")
}
var b strings.Builder
for i := 0; i < len(s); i++ {
c := s[i]
if isTunnelTokenByte(c) {
b.WriteByte(c)
continue
}
break
}
token := b.String()
if token == "" {
return nil, errors.New("empty token")
}
out := &authorizeResponse{token: token}
if earlyLine := findAuthorizeField(body, "ed="); earlyLine != "" {
decoded, err := base64.RawURLEncoding.DecodeString(earlyLine)
if err != nil {
return nil, fmt.Errorf("decode early authorize payload failed: %w", err)
}
out.earlyPayload = decoded
}
return out, nil
}
func findAuthorizeField(body []byte, prefix string) string {
for _, line := range strings.Split(strings.TrimSpace(string(body)), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, prefix) {
return strings.TrimSpace(strings.TrimPrefix(line, prefix))
}
}
return ""
}
func setEarlyDataQuery(rawURL string, payload []byte) (string, error) {
if len(payload) == 0 {
return rawURL, nil
}
u, err := url.Parse(rawURL)
if err != nil {
return "", err
}
q := u.Query()
q.Set(tunnelEarlyDataQueryKey, base64.RawURLEncoding.EncodeToString(payload))
u.RawQuery = q.Encode()
return u.String(), nil
}
func parseEarlyDataQuery(u *url.URL) ([]byte, error) {
if u == nil {
return nil, nil
}
val := strings.TrimSpace(u.Query().Get(tunnelEarlyDataQueryKey))
if val == "" {
return nil, nil
}
return base64.RawURLEncoding.DecodeString(val)
}
func applyEarlyHandshakeOrUpgrade(raw net.Conn, opts TunnelDialOptions) (net.Conn, error) {
out := raw
if opts.EarlyHandshake != nil && opts.EarlyHandshake.WrapConn != nil && (opts.EarlyHandshake.Ready == nil || opts.EarlyHandshake.Ready()) {
wrapped, err := opts.EarlyHandshake.WrapConn(raw)
if err != nil {
return nil, err
}
if wrapped != nil {
out = wrapped
}
return out, nil
}
if opts.Upgrade != nil {
wrapped, err := opts.Upgrade(raw)
if err != nil {
return nil, err
}
if wrapped != nil {
out = wrapped
}
}
return out, nil
}

View File

@@ -0,0 +1,229 @@
package httpmask
import (
"io"
"net"
"os"
"sync"
"time"
)
type pipeDeadline struct {
mu sync.Mutex
timer *time.Timer
cancel chan struct{}
}
func makePipeDeadline() pipeDeadline {
return pipeDeadline{cancel: make(chan struct{})}
}
func (d *pipeDeadline) set(t time.Time) {
d.mu.Lock()
defer d.mu.Unlock()
if d.timer != nil && !d.timer.Stop() {
<-d.cancel
}
d.timer = nil
closed := isClosedPipeChan(d.cancel)
if t.IsZero() {
if closed {
d.cancel = make(chan struct{})
}
return
}
if dur := time.Until(t); dur > 0 {
if closed {
d.cancel = make(chan struct{})
}
d.timer = time.AfterFunc(dur, func() {
close(d.cancel)
})
return
}
if !closed {
close(d.cancel)
}
}
func (d *pipeDeadline) wait() <-chan struct{} {
d.mu.Lock()
ch := d.cancel
d.mu.Unlock()
return ch
}
func isClosedPipeChan(ch <-chan struct{}) bool {
select {
case <-ch:
return true
default:
return false
}
}
type halfPipeAddr struct{}
func (halfPipeAddr) Network() string { return "pipe" }
func (halfPipeAddr) String() string { return "pipe" }
type halfPipeConn struct {
wrMu sync.Mutex
rdRx <-chan []byte
rdTx chan<- int
wrTx chan<- []byte
wrRx <-chan int
readOnce sync.Once
writeOnce sync.Once
localReadDone chan struct{}
localWriteDone chan struct{}
remoteReadDone <-chan struct{}
remoteWriteDone <-chan struct{}
readDeadline pipeDeadline
writeDeadline pipeDeadline
}
func newHalfPipe() (net.Conn, net.Conn) {
cb1 := make(chan []byte)
cb2 := make(chan []byte)
cn1 := make(chan int)
cn2 := make(chan int)
r1 := make(chan struct{})
w1 := make(chan struct{})
r2 := make(chan struct{})
w2 := make(chan struct{})
c1 := &halfPipeConn{
rdRx: cb1,
rdTx: cn1,
wrTx: cb2,
wrRx: cn2,
localReadDone: r1,
localWriteDone: w1,
remoteReadDone: r2,
remoteWriteDone: w2,
readDeadline: makePipeDeadline(),
writeDeadline: makePipeDeadline(),
}
c2 := &halfPipeConn{
rdRx: cb2,
rdTx: cn2,
wrTx: cb1,
wrRx: cn1,
localReadDone: r2,
localWriteDone: w2,
remoteReadDone: r1,
remoteWriteDone: w1,
readDeadline: makePipeDeadline(),
writeDeadline: makePipeDeadline(),
}
return c1, c2
}
func (*halfPipeConn) LocalAddr() net.Addr { return halfPipeAddr{} }
func (*halfPipeConn) RemoteAddr() net.Addr { return halfPipeAddr{} }
func (c *halfPipeConn) Read(p []byte) (int, error) {
switch {
case isClosedPipeChan(c.localReadDone):
return 0, io.ErrClosedPipe
case isClosedPipeChan(c.remoteWriteDone):
return 0, io.EOF
case isClosedPipeChan(c.readDeadline.wait()):
return 0, os.ErrDeadlineExceeded
}
select {
case b := <-c.rdRx:
n := copy(p, b)
c.rdTx <- n
return n, nil
case <-c.localReadDone:
return 0, io.ErrClosedPipe
case <-c.remoteWriteDone:
return 0, io.EOF
case <-c.readDeadline.wait():
return 0, os.ErrDeadlineExceeded
}
}
func (c *halfPipeConn) Write(p []byte) (int, error) {
switch {
case isClosedPipeChan(c.localWriteDone):
return 0, io.ErrClosedPipe
case isClosedPipeChan(c.remoteReadDone):
return 0, io.ErrClosedPipe
case isClosedPipeChan(c.writeDeadline.wait()):
return 0, os.ErrDeadlineExceeded
}
c.wrMu.Lock()
defer c.wrMu.Unlock()
var (
total int
rest = p
)
for once := true; once || len(rest) > 0; once = false {
select {
case c.wrTx <- rest:
n := <-c.wrRx
rest = rest[n:]
total += n
case <-c.localWriteDone:
return total, io.ErrClosedPipe
case <-c.remoteReadDone:
return total, io.ErrClosedPipe
case <-c.writeDeadline.wait():
return total, os.ErrDeadlineExceeded
}
}
return total, nil
}
func (c *halfPipeConn) CloseWrite() error {
c.writeOnce.Do(func() { close(c.localWriteDone) })
return nil
}
func (c *halfPipeConn) CloseRead() error {
c.readOnce.Do(func() { close(c.localReadDone) })
return nil
}
func (c *halfPipeConn) Close() error {
_ = c.CloseRead()
_ = c.CloseWrite()
return nil
}
func (c *halfPipeConn) SetDeadline(t time.Time) error {
c.readDeadline.set(t)
c.writeDeadline.set(t)
return nil
}
func (c *halfPipeConn) SetReadDeadline(t time.Time) error {
c.readDeadline.set(t)
return nil
}
func (c *halfPipeConn) SetWriteDeadline(t time.Time) error {
c.writeDeadline.set(t)
return nil
}

View File

@@ -0,0 +1,252 @@
package httpmask
import (
"bufio"
"bytes"
"encoding/base64"
"fmt"
"io"
"math/rand"
"net"
"strconv"
"strings"
"sync"
"time"
)
var (
userAgents = []string{
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1",
"Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Mobile Safari/537.36",
}
accepts = []string{
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"application/json, text/plain, */*",
"application/octet-stream",
"*/*",
}
acceptLanguages = []string{
"en-US,en;q=0.9",
"en-GB,en;q=0.9",
"zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
"ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7",
"de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7",
}
acceptEncodings = []string{
"gzip, deflate, br",
"gzip, deflate",
"br, gzip, deflate",
}
paths = []string{
"/api/v1/upload",
"/data/sync",
"/uploads/raw",
"/api/report",
"/feed/update",
"/v2/events",
"/v1/telemetry",
"/session",
"/stream",
"/ws",
}
contentTypes = []string{
"application/octet-stream",
"application/x-protobuf",
"application/json",
}
)
var (
rngPool = sync.Pool{
New: func() interface{} {
return rand.New(rand.NewSource(time.Now().UnixNano()))
},
}
headerBufPool = sync.Pool{
New: func() interface{} {
b := make([]byte, 0, 1024)
return &b
},
}
)
// LooksLikeHTTPRequestStart reports whether peek4 looks like a supported HTTP/1.x request method prefix.
func LooksLikeHTTPRequestStart(peek4 []byte) bool {
if len(peek4) < 4 {
return false
}
// Common methods: "GET ", "POST", "HEAD", "PUT ", "OPTI" (OPTIONS), "PATC" (PATCH), "DELE" (DELETE)
return bytes.Equal(peek4, []byte("GET ")) ||
bytes.Equal(peek4, []byte("POST")) ||
bytes.Equal(peek4, []byte("HEAD")) ||
bytes.Equal(peek4, []byte("PUT ")) ||
bytes.Equal(peek4, []byte("OPTI")) ||
bytes.Equal(peek4, []byte("PATC")) ||
bytes.Equal(peek4, []byte("DELE"))
}
func trimPortForHost(host string) string {
if host == "" {
return host
}
// Accept "example.com:443" / "1.2.3.4:443" / "[::1]:443"
h, _, err := net.SplitHostPort(host)
if err == nil && h != "" {
return h
}
// If it's not in host:port form, keep as-is.
return host
}
func appendCommonHeaders(buf []byte, host string, r *rand.Rand) []byte {
ua := userAgents[r.Intn(len(userAgents))]
accept := accepts[r.Intn(len(accepts))]
lang := acceptLanguages[r.Intn(len(acceptLanguages))]
enc := acceptEncodings[r.Intn(len(acceptEncodings))]
buf = append(buf, "Host: "...)
buf = append(buf, host...)
buf = append(buf, "\r\nUser-Agent: "...)
buf = append(buf, ua...)
buf = append(buf, "\r\nAccept: "...)
buf = append(buf, accept...)
buf = append(buf, "\r\nAccept-Language: "...)
buf = append(buf, lang...)
buf = append(buf, "\r\nAccept-Encoding: "...)
buf = append(buf, enc...)
buf = append(buf, "\r\nConnection: keep-alive\r\n"...)
// A couple of common cache headers; keep them static for simplicity.
buf = append(buf, "Cache-Control: no-cache\r\nPragma: no-cache\r\n"...)
return buf
}
// WriteRandomRequestHeader writes a plausible HTTP/1.1 request header as a mask.
func WriteRandomRequestHeader(w io.Writer, host string) error {
return WriteRandomRequestHeaderWithPathRoot(w, host, "")
}
// WriteRandomRequestHeaderWithPathRoot is like WriteRandomRequestHeader but prefixes all paths with pathRoot.
// pathRoot must be a single segment (e.g. "aabbcc"); invalid inputs are treated as empty (disabled).
func WriteRandomRequestHeaderWithPathRoot(w io.Writer, host string, pathRoot string) error {
// Get RNG from pool
r := rngPool.Get().(*rand.Rand)
defer rngPool.Put(r)
path := joinPathRoot(pathRoot, paths[r.Intn(len(paths))])
ctype := contentTypes[r.Intn(len(contentTypes))]
// Use buffer pool
bufPtr := headerBufPool.Get().(*[]byte)
buf := *bufPtr
buf = buf[:0]
defer func() {
if cap(buf) <= 4096 {
*bufPtr = buf
headerBufPool.Put(bufPtr)
}
}()
// Weighted template selection. Keep a conservative default (POST w/ Content-Length),
// but occasionally rotate to other realistic templates (e.g. WebSocket upgrade).
switch r.Intn(10) {
case 0, 1: // ~20% WebSocket-like upgrade
hostNoPort := trimPortForHost(host)
var keyBytes [16]byte
for i := 0; i < len(keyBytes); i++ {
keyBytes[i] = byte(r.Intn(256))
}
wsKey := base64.StdEncoding.EncodeToString(keyBytes[:])
buf = append(buf, "GET "...)
buf = append(buf, path...)
buf = append(buf, " HTTP/1.1\r\n"...)
buf = appendCommonHeaders(buf, host, r)
buf = append(buf, "Upgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Version: 13\r\nSec-WebSocket-Key: "...)
buf = append(buf, wsKey...)
buf = append(buf, "\r\nOrigin: https://"...)
buf = append(buf, hostNoPort...)
buf = append(buf, "\r\n\r\n"...)
default: // ~80% POST upload
// Random Content-Length: 4KB10MB. Small enough to look plausible, large enough
// to justify long-lived writes on keep-alive connections.
const minCL = int64(4 * 1024)
const maxCL = int64(10 * 1024 * 1024)
contentLength := minCL + r.Int63n(maxCL-minCL+1)
buf = append(buf, "POST "...)
buf = append(buf, path...)
buf = append(buf, " HTTP/1.1\r\n"...)
buf = appendCommonHeaders(buf, host, r)
buf = append(buf, "Content-Type: "...)
buf = append(buf, ctype...)
buf = append(buf, "\r\nContent-Length: "...)
buf = strconv.AppendInt(buf, contentLength, 10)
// A couple of extra headers seen in real clients.
if r.Intn(2) == 0 {
buf = append(buf, "\r\nX-Requested-With: XMLHttpRequest"...)
}
if r.Intn(3) == 0 {
buf = append(buf, "\r\nReferer: https://"...)
buf = append(buf, trimPortForHost(host)...)
buf = append(buf, "/"...)
}
buf = append(buf, "\r\n\r\n"...)
}
_, err := w.Write(buf)
return err
}
// ConsumeHeader 读取并消耗 HTTP 头部,返回消耗的数据和剩余的 reader 数据
// 如果不是 POST 请求或格式严重错误,返回 error
func ConsumeHeader(r *bufio.Reader) ([]byte, error) {
var consumed bytes.Buffer
// 1. 读取请求行
// Use ReadSlice to avoid allocation if line fits in buffer
line, err := r.ReadSlice('\n')
if err != nil {
return nil, err
}
consumed.Write(line)
// Basic method validation: accept common HTTP/1.x methods used by our masker.
// Keep it strict enough to reject obvious garbage.
switch {
case bytes.HasPrefix(line, []byte("POST ")),
bytes.HasPrefix(line, []byte("GET ")),
bytes.HasPrefix(line, []byte("HEAD ")),
bytes.HasPrefix(line, []byte("PUT ")),
bytes.HasPrefix(line, []byte("DELETE ")),
bytes.HasPrefix(line, []byte("OPTIONS ")),
bytes.HasPrefix(line, []byte("PATCH ")):
default:
return consumed.Bytes(), fmt.Errorf("invalid method or garbage: %s", strings.TrimSpace(string(line)))
}
// 2. 循环读取头部,直到遇到空行
for {
line, err = r.ReadSlice('\n')
if err != nil {
return consumed.Bytes(), err
}
consumed.Write(line)
// Check for empty line (\r\n or \n)
// ReadSlice includes the delimiter
n := len(line)
if n == 2 && line[0] == '\r' && line[1] == '\n' {
return consumed.Bytes(), nil
}
if n == 1 && line[0] == '\n' {
return consumed.Bytes(), nil
}
}
}

View File

@@ -0,0 +1,52 @@
package httpmask
import "strings"
// normalizePathRoot normalizes the configured path root into "/<segment>" form.
//
// It is intentionally strict: only a single path segment is allowed, consisting of
// [A-Za-z0-9_-]. Invalid inputs are treated as empty (disabled).
func normalizePathRoot(root string) string {
root = strings.TrimSpace(root)
root = strings.Trim(root, "/")
if root == "" {
return ""
}
for i := 0; i < len(root); i++ {
c := root[i]
switch {
case c >= 'a' && c <= 'z':
case c >= 'A' && c <= 'Z':
case c >= '0' && c <= '9':
case c == '_' || c == '-':
default:
return ""
}
}
return "/" + root
}
func joinPathRoot(root, path string) string {
root = normalizePathRoot(root)
if root == "" {
return path
}
if path == "" {
return root
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
return root + path
}
func stripPathRoot(root, fullPath string) (string, bool) {
root = normalizePathRoot(root)
if root == "" {
return fullPath, true
}
if !strings.HasPrefix(fullPath, root+"/") {
return "", false
}
return strings.TrimPrefix(fullPath, root), true
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,190 @@
package httpmask
import (
"context"
"encoding/base64"
"fmt"
"io"
mrand "math/rand"
"net"
stdhttp "net/http"
"net/url"
"strings"
"time"
"github.com/gobwas/ws"
)
func normalizeWSSchemeFromAddress(serverAddress string, tlsEnabled bool) (string, string) {
addr := strings.TrimSpace(serverAddress)
if strings.Contains(addr, "://") {
if u, err := url.Parse(addr); err == nil && u != nil {
switch strings.ToLower(strings.TrimSpace(u.Scheme)) {
case "ws":
return "ws", u.Host
case "wss":
return "wss", u.Host
}
}
}
if tlsEnabled {
return "wss", addr
}
return "ws", addr
}
func normalizeWSDialTarget(serverAddress string, tlsEnabled bool, hostOverride string) (scheme, urlHost, dialAddr, serverName string, err error) {
scheme, addr := normalizeWSSchemeFromAddress(serverAddress, tlsEnabled)
host, port, err := net.SplitHostPort(addr)
if err != nil {
// Allow ws(s)://host without port.
if strings.Contains(addr, ":") {
return "", "", "", "", fmt.Errorf("invalid server address %q: %w", serverAddress, err)
}
switch scheme {
case "wss":
port = "443"
default:
port = "80"
}
host = addr
}
if hostOverride != "" {
// Allow "example.com" or "example.com:443"
if h, p, splitErr := net.SplitHostPort(hostOverride); splitErr == nil {
if h != "" {
hostOverride = h
}
if p != "" {
port = p
}
}
serverName = hostOverride
urlHost = net.JoinHostPort(hostOverride, port)
} else {
serverName = host
urlHost = net.JoinHostPort(host, port)
}
dialAddr = net.JoinHostPort(host, port)
return scheme, urlHost, dialAddr, trimPortForHost(serverName), nil
}
func applyWSHeaders(h stdhttp.Header, host string) {
if h == nil {
return
}
r := rngPool.Get().(*mrand.Rand)
ua := userAgents[r.Intn(len(userAgents))]
accept := accepts[r.Intn(len(accepts))]
lang := acceptLanguages[r.Intn(len(acceptLanguages))]
enc := acceptEncodings[r.Intn(len(acceptEncodings))]
rngPool.Put(r)
h.Set("User-Agent", ua)
h.Set("Accept", accept)
h.Set("Accept-Language", lang)
h.Set("Accept-Encoding", enc)
h.Set("Cache-Control", "no-cache")
h.Set("Pragma", "no-cache")
h.Set("X-Sudoku-Tunnel", string(TunnelModeWS))
h.Set("X-Sudoku-Version", "1")
}
func dialWS(ctx context.Context, serverAddress string, opts TunnelDialOptions) (net.Conn, error) {
if opts.DialContext == nil {
panic("httpmask: DialContext is nil")
}
scheme, urlHost, dialAddr, _, err := normalizeWSDialTarget(serverAddress, opts.TLSConfig != nil, opts.HostOverride)
if err != nil {
return nil, err
}
httpScheme := "http"
if scheme == "wss" {
httpScheme = "https"
}
headerHost := canonicalHeaderHost(urlHost, httpScheme)
auth := newTunnelAuth(opts.AuthKey, 0)
u := &url.URL{
Scheme: scheme,
Host: urlHost,
Path: joinPathRoot(opts.PathRoot, "/ws"),
}
if opts.EarlyHandshake != nil && len(opts.EarlyHandshake.RequestPayload) > 0 {
rawURL, err := setEarlyDataQuery(u.String(), opts.EarlyHandshake.RequestPayload)
if err != nil {
return nil, err
}
u, err = url.Parse(rawURL)
if err != nil {
return nil, err
}
}
header := make(stdhttp.Header)
applyWSHeaders(header, headerHost)
if auth != nil {
token := auth.token(TunnelModeWS, stdhttp.MethodGet, "/ws", time.Now())
if token != "" {
header.Set("Authorization", "Bearer "+token)
q := u.Query()
q.Set(tunnelAuthQueryKey, token)
u.RawQuery = q.Encode()
}
}
d := ws.Dialer{
Host: headerHost,
Header: ws.HandshakeHeaderHTTP(header),
OnHeader: func(key, value []byte) error {
if !strings.EqualFold(string(key), tunnelEarlyDataHeader) || opts.EarlyHandshake == nil || opts.EarlyHandshake.HandleResponse == nil {
return nil
}
decoded, err := base64.RawURLEncoding.DecodeString(strings.TrimSpace(string(value)))
if err != nil {
return err
}
return opts.EarlyHandshake.HandleResponse(decoded)
},
NetDial: func(dialCtx context.Context, network, addr string) (net.Conn, error) {
if addr == urlHost {
addr = dialAddr
}
return opts.DialContext(dialCtx, network, addr)
},
}
if scheme == "wss" {
if opts.TLSConfig == nil {
return nil, fmt.Errorf("httpmask: TLSConfig is required for wss")
}
d.TLSClient = func(conn net.Conn, hostname string) net.Conn {
tlsConn, _ := opts.TLSConfig.Client(conn)
return tlsConn
}
}
conn, br, _, err := d.Dial(ctx, u.String())
if err != nil {
return nil, err
}
if br != nil && br.Buffered() > 0 {
pre := make([]byte, br.Buffered())
_, _ = io.ReadFull(br, pre)
conn = newPreBufferedConn(conn, pre)
}
wsConn := newWSStreamConn(conn, ws.StateClientSide)
upgraded, err := applyEarlyHandshakeOrUpgrade(wsConn, opts)
if err != nil {
_ = wsConn.Close()
return nil, err
}
return upgraded, nil
}

View File

@@ -0,0 +1,109 @@
package httpmask
import (
"encoding/base64"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/gobwas/ws"
)
func looksLikeWebSocketUpgrade(headers map[string]string) bool {
if headers == nil {
return false
}
if !strings.EqualFold(strings.TrimSpace(headers["upgrade"]), "websocket") {
return false
}
conn := headers["connection"]
for _, part := range strings.Split(conn, ",") {
if strings.EqualFold(strings.TrimSpace(part), "upgrade") {
return true
}
}
return false
}
func (s *TunnelServer) handleWS(rawConn net.Conn, req *httpRequestHeader, headerBytes []byte, buffered []byte) (HandleResult, net.Conn, error) {
rejectOrReply := func(code int, body string) (HandleResult, net.Conn, error) {
if s.passThroughOnReject {
prefix := make([]byte, 0, len(headerBytes)+len(buffered))
prefix = append(prefix, headerBytes...)
prefix = append(prefix, buffered...)
return HandlePassThrough, newRejectedPreBufferedConn(rawConn, prefix), nil
}
_ = writeSimpleHTTPResponse(rawConn, code, body)
_ = rawConn.Close()
return HandleDone, nil, nil
}
u, err := url.ParseRequestURI(req.target)
if err != nil {
return rejectOrReply(http.StatusBadRequest, "bad request")
}
path, ok := stripPathRoot(s.pathRoot, u.Path)
if !ok || path != "/ws" {
return rejectOrReply(http.StatusNotFound, "not found")
}
if strings.ToUpper(strings.TrimSpace(req.method)) != http.MethodGet {
return rejectOrReply(http.StatusBadRequest, "bad request")
}
if !looksLikeWebSocketUpgrade(req.headers) {
return rejectOrReply(http.StatusBadRequest, "bad request")
}
authVal := req.headers["authorization"]
if authVal == "" {
authVal = u.Query().Get(tunnelAuthQueryKey)
}
if !s.auth.verifyValue(authVal, TunnelModeWS, req.method, path, time.Now()) {
return rejectOrReply(http.StatusNotFound, "not found")
}
earlyPayload, err := parseEarlyDataQuery(u)
if err != nil {
return rejectOrReply(http.StatusBadRequest, "bad request")
}
var prepared *PreparedServerEarlyHandshake
if len(earlyPayload) > 0 && s.earlyHandshake != nil && s.earlyHandshake.Prepare != nil {
prepared, err = s.earlyHandshake.Prepare(earlyPayload)
if err != nil {
return rejectOrReply(http.StatusNotFound, "not found")
}
}
prefix := make([]byte, 0, len(headerBytes)+len(buffered))
prefix = append(prefix, headerBytes...)
prefix = append(prefix, buffered...)
wsConnRaw := newPreBufferedConn(rawConn, prefix)
upgrader := ws.Upgrader{}
if prepared != nil && len(prepared.ResponsePayload) > 0 {
upgrader.OnBeforeUpgrade = func() (ws.HandshakeHeader, error) {
h := http.Header{}
h.Set(tunnelEarlyDataHeader, base64.RawURLEncoding.EncodeToString(prepared.ResponsePayload))
return ws.HandshakeHeaderHTTP(h), nil
}
}
if _, err := upgrader.Upgrade(wsConnRaw); err != nil {
_ = rawConn.Close()
return HandleDone, nil, nil
}
outConn := net.Conn(newWSStreamConn(wsConnRaw, ws.StateServerSide))
if prepared != nil && prepared.WrapConn != nil {
wrapped, err := prepared.WrapConn(outConn)
if err != nil {
_ = outConn.Close()
return HandleDone, nil, nil
}
if wrapped != nil {
outConn = wrapEarlyHandshakeConn(wrapped, prepared.UserHash)
}
}
return HandleStartTunnel, outConn, nil
}

View File

@@ -0,0 +1,78 @@
package httpmask
import (
"errors"
"fmt"
"io"
"net"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
)
type wsStreamConn struct {
net.Conn
state ws.State
reader *wsutil.Reader
controlHandler wsutil.FrameHandlerFunc
}
func newWSStreamConn(conn net.Conn, state ws.State) net.Conn {
controlHandler := wsutil.ControlFrameHandler(conn, state)
return &wsStreamConn{
Conn: conn,
state: state,
reader: &wsutil.Reader{
Source: conn,
State: state,
},
controlHandler: controlHandler,
}
}
func (c *wsStreamConn) Read(b []byte) (n int, err error) {
defer func() {
if v := recover(); v != nil {
err = fmt.Errorf("websocket error: %v", v)
}
}()
for {
n, err = c.reader.Read(b)
if errors.Is(err, io.EOF) {
err = nil
}
if !errors.Is(err, wsutil.ErrNoFrameAdvance) {
return n, err
}
hdr, err2 := c.reader.NextFrame()
if err2 != nil {
return 0, err2
}
if hdr.OpCode.IsControl() {
if err := c.controlHandler(hdr, c.reader); err != nil {
return 0, err
}
continue
}
if hdr.OpCode&(ws.OpBinary|ws.OpText) == 0 {
if err := c.reader.Discard(); err != nil {
return 0, err
}
continue
}
}
}
func (c *wsStreamConn) Write(b []byte) (int, error) {
if err := wsutil.WriteMessage(c.Conn, c.state, ws.OpBinary, b); err != nil {
return 0, err
}
return len(b), nil
}
func (c *wsStreamConn) Close() error {
_ = wsutil.WriteMessage(c.Conn, c.state, ws.OpClose, ws.NewCloseFrameBody(ws.StatusNormalClosure, ""))
return c.Conn.Close()
}

View File

@@ -0,0 +1,93 @@
package sudoku
import (
"fmt"
"strings"
)
const (
asciiModeTokenASCII = "ascii"
asciiModeTokenEntropy = "entropy"
)
// ASCIIMode describes the preferred wire layout for each traffic direction.
// Uplink is client->server, Downlink is server->client.
type ASCIIMode struct {
Uplink string
Downlink string
}
// ParseASCIIMode accepts legacy symmetric values ("ascii"/"entropy"/"prefer_*")
// and directional values like "up_ascii_down_entropy".
func ParseASCIIMode(mode string) (ASCIIMode, error) {
raw := strings.ToLower(strings.TrimSpace(mode))
switch raw {
case "", "entropy", "prefer_entropy":
return ASCIIMode{Uplink: asciiModeTokenEntropy, Downlink: asciiModeTokenEntropy}, nil
case "ascii", "prefer_ascii":
return ASCIIMode{Uplink: asciiModeTokenASCII, Downlink: asciiModeTokenASCII}, nil
}
if !strings.HasPrefix(raw, "up_") {
return ASCIIMode{}, fmt.Errorf("invalid ascii mode: %s", mode)
}
parts := strings.SplitN(strings.TrimPrefix(raw, "up_"), "_down_", 2)
if len(parts) != 2 {
return ASCIIMode{}, fmt.Errorf("invalid ascii mode: %s", mode)
}
up, ok := normalizeASCIIModeToken(parts[0])
if !ok {
return ASCIIMode{}, fmt.Errorf("invalid ascii mode: %s", mode)
}
down, ok := normalizeASCIIModeToken(parts[1])
if !ok {
return ASCIIMode{}, fmt.Errorf("invalid ascii mode: %s", mode)
}
return ASCIIMode{Uplink: up, Downlink: down}, nil
}
// NormalizeASCIIMode returns the canonical config string for a supported mode.
func NormalizeASCIIMode(mode string) (string, error) {
parsed, err := ParseASCIIMode(mode)
if err != nil {
return "", err
}
return parsed.Canonical(), nil
}
func (m ASCIIMode) Canonical() string {
if m.Uplink == asciiModeTokenASCII && m.Downlink == asciiModeTokenASCII {
return "prefer_ascii"
}
if m.Uplink == asciiModeTokenEntropy && m.Downlink == asciiModeTokenEntropy {
return "prefer_entropy"
}
return "up_" + m.Uplink + "_down_" + m.Downlink
}
func (m ASCIIMode) uplinkPreference() string {
return singleDirectionPreference(m.Uplink)
}
func (m ASCIIMode) downlinkPreference() string {
return singleDirectionPreference(m.Downlink)
}
func normalizeASCIIModeToken(token string) (string, bool) {
switch strings.ToLower(strings.TrimSpace(token)) {
case "ascii", "prefer_ascii":
return asciiModeTokenASCII, true
case "entropy", "prefer_entropy", "":
return asciiModeTokenEntropy, true
default:
return "", false
}
}
func singleDirectionPreference(token string) string {
if token == asciiModeTokenASCII {
return "prefer_ascii"
}
return "prefer_entropy"
}

View File

@@ -0,0 +1,193 @@
package sudoku
import (
"bufio"
"bytes"
"net"
"sync"
"sync/atomic"
)
const IOBufferSize = 32 * 1024
var perm4 = [24][4]byte{
{0, 1, 2, 3},
{0, 1, 3, 2},
{0, 2, 1, 3},
{0, 2, 3, 1},
{0, 3, 1, 2},
{0, 3, 2, 1},
{1, 0, 2, 3},
{1, 0, 3, 2},
{1, 2, 0, 3},
{1, 2, 3, 0},
{1, 3, 0, 2},
{1, 3, 2, 0},
{2, 0, 1, 3},
{2, 0, 3, 1},
{2, 1, 0, 3},
{2, 1, 3, 0},
{2, 3, 0, 1},
{2, 3, 1, 0},
{3, 0, 1, 2},
{3, 0, 2, 1},
{3, 1, 0, 2},
{3, 1, 2, 0},
{3, 2, 0, 1},
{3, 2, 1, 0},
}
type Conn struct {
net.Conn
table *Table
reader *bufio.Reader
recorder *bytes.Buffer
recording atomic.Bool
recordLock sync.Mutex
rawBuf []byte
pendingData pendingBuffer
hintBuf [4]byte
hintCount int
writeMu sync.Mutex
writeBuf []byte
rng randomSource
paddingThreshold uint64
}
func (sc *Conn) CloseWrite() error {
if sc == nil || sc.Conn == nil {
return nil
}
if cw, ok := sc.Conn.(interface{ CloseWrite() error }); ok {
return cw.CloseWrite()
}
return nil
}
func (sc *Conn) CloseRead() error {
if sc == nil || sc.Conn == nil {
return nil
}
if cr, ok := sc.Conn.(interface{ CloseRead() error }); ok {
return cr.CloseRead()
}
return nil
}
func NewConn(c net.Conn, table *Table, pMin, pMax int, record bool) *Conn {
localRng := newSeededRand()
sc := &Conn{
Conn: c,
table: table,
reader: bufio.NewReaderSize(c, IOBufferSize),
rawBuf: make([]byte, IOBufferSize),
pendingData: newPendingBuffer(4096),
writeBuf: make([]byte, 0, 4096),
rng: localRng,
paddingThreshold: pickPaddingThreshold(localRng, pMin, pMax),
}
if record {
sc.recorder = new(bytes.Buffer)
sc.recording.Store(true)
}
return sc
}
func (sc *Conn) StopRecording() {
sc.recordLock.Lock()
sc.recording.Store(false)
sc.recorder = nil
sc.recordLock.Unlock()
}
func (sc *Conn) GetBufferedAndRecorded() []byte {
if sc == nil {
return nil
}
sc.recordLock.Lock()
defer sc.recordLock.Unlock()
var recorded []byte
if sc.recorder != nil {
recorded = sc.recorder.Bytes()
}
buffered := sc.reader.Buffered()
if buffered > 0 {
peeked, _ := sc.reader.Peek(buffered)
full := make([]byte, len(recorded)+len(peeked))
copy(full, recorded)
copy(full[len(recorded):], peeked)
return full
}
return recorded
}
func (sc *Conn) Write(p []byte) (n int, err error) {
if len(p) == 0 {
return 0, nil
}
sc.writeMu.Lock()
defer sc.writeMu.Unlock()
sc.writeBuf = encodeSudokuPayload(sc.writeBuf[:0], sc.table, sc.rng, sc.paddingThreshold, p)
return len(p), writeFull(sc.Conn, sc.writeBuf)
}
func (sc *Conn) Read(p []byte) (n int, err error) {
if n, ok := drainPending(p, &sc.pendingData); ok {
return n, nil
}
for {
if sc.pendingData.available() > 0 {
break
}
nr, rErr := sc.reader.Read(sc.rawBuf)
if nr > 0 {
chunk := sc.rawBuf[:nr]
if sc.recording.Load() {
sc.recordLock.Lock()
if sc.recording.Load() && sc.recorder != nil {
sc.recorder.Write(chunk)
}
sc.recordLock.Unlock()
}
layout := sc.table.layout
for _, b := range chunk {
if !layout.hintTable[b] {
continue
}
sc.hintBuf[sc.hintCount] = b
sc.hintCount++
if sc.hintCount == len(sc.hintBuf) {
key := packHintsToKey(sc.hintBuf)
val, ok := sc.table.DecodeMap[key]
if !ok {
return 0, ErrInvalidSudokuMapMiss
}
sc.pendingData.appendByte(val)
sc.hintCount = 0
}
}
}
if rErr != nil {
return 0, rErr
}
if sc.pendingData.available() > 0 {
break
}
}
n, _ = drainPending(p, &sc.pendingData)
return n, nil
}

View File

@@ -0,0 +1,36 @@
package sudoku
func encodeSudokuPayload(dst []byte, table *Table, rng randomSource, paddingThreshold uint64, p []byte) []byte {
if len(p) == 0 {
return dst[:0]
}
outCapacity := len(p)*6 + 1
if cap(dst) < outCapacity {
dst = make([]byte, 0, outCapacity)
}
out := dst[:0]
pads := table.PaddingPool
padLen := len(pads)
for _, b := range p {
if shouldPad(rng, paddingThreshold) {
out = append(out, pads[rng.Intn(padLen)])
}
puzzles := table.EncodeTable[b]
puzzle := puzzles[rng.Intn(len(puzzles))]
perm := perm4[rng.Intn(len(perm4))]
for _, idx := range perm {
if shouldPad(rng, paddingThreshold) {
out = append(out, pads[rng.Intn(padLen)])
}
out = append(out, puzzle[idx])
}
}
if shouldPad(rng, paddingThreshold) {
out = append(out, pads[rng.Intn(padLen)])
}
return out
}

View File

@@ -0,0 +1,46 @@
package sudoku
// Grid represents a 4x4 sudoku grid
type Grid [16]uint8
// GenerateAllGrids generates all valid 4x4 Sudoku grids
func GenerateAllGrids() []Grid {
var grids []Grid
var g Grid
var backtrack func(int)
backtrack = func(idx int) {
if idx == 16 {
grids = append(grids, g)
return
}
row, col := idx/4, idx%4
br, bc := (row/2)*2, (col/2)*2
for num := uint8(1); num <= 4; num++ {
valid := true
for i := 0; i < 4; i++ {
if g[row*4+i] == num || g[i*4+col] == num {
valid = false
break
}
}
if valid {
for r := 0; r < 2; r++ {
for c := 0; c < 2; c++ {
if g[(br+r)*4+(bc+c)] == num {
valid = false
break
}
}
}
}
if valid {
g[idx] = num
backtrack(idx + 1)
g[idx] = 0
}
}
}
backtrack(0)
return grids
}

View File

@@ -0,0 +1,255 @@
package sudoku
import (
"fmt"
"math/bits"
"sort"
"strings"
)
type byteLayout struct {
name string
hintMask byte
hintValue byte
padMarker byte
paddingPool []byte
hintTable [256]bool
encodeHint [4][16]byte
encodeGroup [64]byte
decodeGroup [256]byte
groupValid [256]bool
}
func (l *byteLayout) isHint(b byte) bool {
return l != nil && l.hintTable[b]
}
func (l *byteLayout) hintByte(val, pos byte) byte {
return l.encodeHint[val&0x03][pos&0x0F]
}
func (l *byteLayout) groupByte(group byte) byte {
return l.encodeGroup[group&0x3F]
}
func (l *byteLayout) decodePackedGroup(b byte) (byte, bool) {
if l == nil {
return 0, false
}
return l.decodeGroup[b], l.groupValid[b]
}
// resolveLayout picks the byte layout for a single traffic direction.
// ASCII always wins if requested. Custom patterns are ignored when ASCII is preferred.
func resolveLayout(mode string, customPattern string) (*byteLayout, error) {
switch strings.ToLower(mode) {
case "ascii", "prefer_ascii":
return newASCIILayout(), nil
case "entropy", "prefer_entropy", "":
// fallback to entropy unless a custom pattern is provided
default:
return nil, fmt.Errorf("invalid ascii mode: %s", mode)
}
if strings.TrimSpace(customPattern) != "" {
return newCustomLayout(customPattern)
}
return newEntropyLayout(), nil
}
func newASCIILayout() *byteLayout {
padding := make([]byte, 0, 32)
for i := 0; i < 32; i++ {
padding = append(padding, byte(0x20+i))
}
layout := &byteLayout{
name: "ascii",
hintMask: 0x40,
hintValue: 0x40,
padMarker: 0x3F,
paddingPool: padding,
}
for val := 0; val < 4; val++ {
for pos := 0; pos < 16; pos++ {
b := byte(0x40 | (byte(val) << 4) | byte(pos))
if b == 0x7F {
b = '\n'
}
layout.encodeHint[val][pos] = b
}
}
for group := 0; group < 64; group++ {
b := byte(0x40 | byte(group))
if b == 0x7F {
b = '\n'
}
layout.encodeGroup[group] = b
}
for b := 0; b < 256; b++ {
wire := byte(b)
if (wire & 0x40) == 0x40 {
layout.hintTable[wire] = true
layout.decodeGroup[wire] = wire & 0x3F
layout.groupValid[wire] = true
}
}
layout.hintTable['\n'] = true
layout.decodeGroup['\n'] = 0x3F
layout.groupValid['\n'] = true
return layout
}
func newEntropyLayout() *byteLayout {
padding := make([]byte, 0, 16)
for i := 0; i < 8; i++ {
padding = append(padding, byte(0x80+i))
padding = append(padding, byte(0x10+i))
}
layout := &byteLayout{
name: "entropy",
hintMask: 0x90,
hintValue: 0x00,
padMarker: 0x80,
paddingPool: padding,
}
for val := 0; val < 4; val++ {
for pos := 0; pos < 16; pos++ {
layout.encodeHint[val][pos] = (byte(val) << 5) | byte(pos)
}
}
for group := 0; group < 64; group++ {
v := byte(group)
layout.encodeGroup[group] = ((v & 0x30) << 1) | (v & 0x0F)
}
for b := 0; b < 256; b++ {
wire := byte(b)
if (wire & 0x90) != 0 {
continue
}
layout.hintTable[wire] = true
layout.decodeGroup[wire] = ((wire >> 1) & 0x30) | (wire & 0x0F)
layout.groupValid[wire] = true
}
return layout
}
func newCustomLayout(pattern string) (*byteLayout, error) {
cleaned := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(pattern), " ", ""))
if len(cleaned) != 8 {
return nil, fmt.Errorf("custom table must have 8 symbols, got %d", len(cleaned))
}
var xBits, pBits, vBits []uint8
for i, c := range cleaned {
bit := uint8(7 - i)
switch c {
case 'x':
xBits = append(xBits, bit)
case 'p':
pBits = append(pBits, bit)
case 'v':
vBits = append(vBits, bit)
default:
return nil, fmt.Errorf("invalid char %q in custom table", c)
}
}
if len(xBits) != 2 || len(pBits) != 2 || len(vBits) != 4 {
return nil, fmt.Errorf("custom table must contain exactly 2 x, 2 p, 4 v")
}
xMask := byte(0)
for _, b := range xBits {
xMask |= 1 << b
}
encodeBits := func(val, pos byte, dropX int) byte {
var out byte
out |= xMask
if dropX >= 0 {
out &^= 1 << xBits[dropX]
}
if (val & 0x02) != 0 {
out |= 1 << pBits[0]
}
if (val & 0x01) != 0 {
out |= 1 << pBits[1]
}
for i, bit := range vBits {
if (pos>>(3-uint8(i)))&0x01 == 1 {
out |= 1 << bit
}
}
return out
}
paddingSet := make(map[byte]struct{})
var padding []byte
for drop := range xBits {
for val := 0; val < 4; val++ {
for pos := 0; pos < 16; pos++ {
b := encodeBits(byte(val), byte(pos), drop)
if bits.OnesCount8(b) >= 5 {
if _, ok := paddingSet[b]; !ok {
paddingSet[b] = struct{}{}
padding = append(padding, b)
}
}
}
}
}
sort.Slice(padding, func(i, j int) bool { return padding[i] < padding[j] })
if len(padding) == 0 {
return nil, fmt.Errorf("custom table produced empty padding pool")
}
layout := &byteLayout{
name: fmt.Sprintf("custom(%s)", cleaned),
hintMask: xMask,
hintValue: xMask,
padMarker: padding[0],
paddingPool: padding,
}
for val := 0; val < 4; val++ {
for pos := 0; pos < 16; pos++ {
layout.encodeHint[val][pos] = encodeBits(byte(val), byte(pos), -1)
}
}
for group := 0; group < 64; group++ {
val := byte(group>>4) & 0x03
pos := byte(group) & 0x0F
layout.encodeGroup[group] = encodeBits(val, pos, -1)
}
for b := 0; b < 256; b++ {
wire := byte(b)
if (wire & xMask) != xMask {
continue
}
layout.hintTable[wire] = true
var val, pos byte
if wire&(1<<pBits[0]) != 0 {
val |= 0x02
}
if wire&(1<<pBits[1]) != 0 {
val |= 0x01
}
for i, bit := range vBits {
if wire&(1<<bit) != 0 {
pos |= 1 << (3 - uint8(i))
}
}
layout.decodeGroup[wire] = (val << 4) | pos
layout.groupValid[wire] = true
}
return layout, nil
}

View File

@@ -0,0 +1,359 @@
package sudoku
import (
"bufio"
"io"
"net"
"sync"
)
const (
RngBatchSize = 128
packedProtectedPrefixBytes = 14
)
// PackedConn encodes traffic with the packed Sudoku layout while preserving
// the same padding model as the regular connection.
type PackedConn struct {
net.Conn
table *Table
reader *bufio.Reader
// Read-side buffers.
rawBuf []byte
pendingData pendingBuffer
// Write-side state.
writeMu sync.Mutex
writeBuf []byte
bitBuf uint64
bitCount int
// Read-side bit accumulator.
readBitBuf uint64
readBits int
// Padding selection matches Conn's threshold-based model.
rng randomSource
paddingThreshold uint64
padMarker byte
padPool []byte
}
func (pc *PackedConn) CloseWrite() error {
if pc == nil || pc.Conn == nil {
return nil
}
if cw, ok := pc.Conn.(interface{ CloseWrite() error }); ok {
return cw.CloseWrite()
}
return nil
}
func (pc *PackedConn) CloseRead() error {
if pc == nil || pc.Conn == nil {
return nil
}
if cr, ok := pc.Conn.(interface{ CloseRead() error }); ok {
return cr.CloseRead()
}
return nil
}
func NewPackedConn(c net.Conn, table *Table, pMin, pMax int) *PackedConn {
localRng := newSeededRand()
pc := &PackedConn{
Conn: c,
table: table,
reader: bufio.NewReaderSize(c, IOBufferSize),
rawBuf: make([]byte, IOBufferSize),
pendingData: newPendingBuffer(4096),
writeBuf: make([]byte, 0, 4096),
rng: localRng,
paddingThreshold: pickPaddingThreshold(localRng, pMin, pMax),
}
pc.padMarker = table.layout.padMarker
for _, b := range table.PaddingPool {
if b != pc.padMarker {
pc.padPool = append(pc.padPool, b)
}
}
if len(pc.padPool) == 0 {
pc.padPool = append(pc.padPool, pc.padMarker)
}
return pc
}
func (pc *PackedConn) maybeAddPadding(out []byte) []byte {
if shouldPad(pc.rng, pc.paddingThreshold) {
out = append(out, pc.getPaddingByte())
}
return out
}
func (pc *PackedConn) appendGroup(out []byte, group byte) []byte {
out = pc.maybeAddPadding(out)
return append(out, pc.table.layout.groupByte(group))
}
func (pc *PackedConn) appendForcedPadding(out []byte) []byte {
return append(out, pc.getPaddingByte())
}
func (pc *PackedConn) nextProtectedPrefixGap() int {
return 1 + pc.rng.Intn(2)
}
func (pc *PackedConn) writeProtectedPrefix(out []byte, p []byte) ([]byte, int) {
if len(p) == 0 {
return out, 0
}
limit := len(p)
if limit > packedProtectedPrefixBytes {
limit = packedProtectedPrefixBytes
}
for padCount := 0; padCount < 1+pc.rng.Intn(2); padCount++ {
out = pc.appendForcedPadding(out)
}
gap := pc.nextProtectedPrefixGap()
effective := 0
for i := 0; i < limit; i++ {
pc.bitBuf = (pc.bitBuf << 8) | uint64(p[i])
pc.bitCount += 8
for pc.bitCount >= 6 {
pc.bitCount -= 6
group := byte(pc.bitBuf >> pc.bitCount)
if pc.bitCount == 0 {
pc.bitBuf = 0
} else {
pc.bitBuf &= (1 << pc.bitCount) - 1
}
out = pc.appendGroup(out, group&0x3F)
}
effective++
if effective >= gap {
out = pc.appendForcedPadding(out)
effective = 0
gap = pc.nextProtectedPrefixGap()
}
}
return out, limit
}
func (pc *PackedConn) Write(p []byte) (int, error) {
if len(p) == 0 {
return 0, nil
}
pc.writeMu.Lock()
defer pc.writeMu.Unlock()
needed := len(p)*3/2 + 32
if cap(pc.writeBuf) < needed {
pc.writeBuf = make([]byte, 0, needed)
}
out := pc.writeBuf[:0]
var prefixN int
out, prefixN = pc.writeProtectedPrefix(out, p)
i := prefixN
n := len(p)
for pc.bitCount > 0 && i < n {
b := p[i]
i++
pc.bitBuf = (pc.bitBuf << 8) | uint64(b)
pc.bitCount += 8
for pc.bitCount >= 6 {
pc.bitCount -= 6
group := byte(pc.bitBuf >> pc.bitCount)
if pc.bitCount == 0 {
pc.bitBuf = 0
} else {
pc.bitBuf &= (1 << pc.bitCount) - 1
}
out = pc.appendGroup(out, group&0x3F)
}
}
for i+11 < n {
for batch := 0; batch < 4; batch++ {
b1, b2, b3 := p[i], p[i+1], p[i+2]
i += 3
g1 := (b1 >> 2) & 0x3F
g2 := ((b1 & 0x03) << 4) | ((b2 >> 4) & 0x0F)
g3 := ((b2 & 0x0F) << 2) | ((b3 >> 6) & 0x03)
g4 := b3 & 0x3F
out = pc.appendGroup(out, g1)
out = pc.appendGroup(out, g2)
out = pc.appendGroup(out, g3)
out = pc.appendGroup(out, g4)
}
}
for i+2 < n {
b1, b2, b3 := p[i], p[i+1], p[i+2]
i += 3
g1 := (b1 >> 2) & 0x3F
g2 := ((b1 & 0x03) << 4) | ((b2 >> 4) & 0x0F)
g3 := ((b2 & 0x0F) << 2) | ((b3 >> 6) & 0x03)
g4 := b3 & 0x3F
out = pc.appendGroup(out, g1)
out = pc.appendGroup(out, g2)
out = pc.appendGroup(out, g3)
out = pc.appendGroup(out, g4)
}
for ; i < n; i++ {
b := p[i]
pc.bitBuf = (pc.bitBuf << 8) | uint64(b)
pc.bitCount += 8
for pc.bitCount >= 6 {
pc.bitCount -= 6
group := byte(pc.bitBuf >> pc.bitCount)
if pc.bitCount == 0 {
pc.bitBuf = 0
} else {
pc.bitBuf &= (1 << pc.bitCount) - 1
}
out = pc.appendGroup(out, group&0x3F)
}
}
if pc.bitCount > 0 {
group := byte(pc.bitBuf << (6 - pc.bitCount))
pc.bitBuf = 0
pc.bitCount = 0
out = pc.appendGroup(out, group&0x3F)
out = append(out, pc.padMarker)
}
out = pc.maybeAddPadding(out)
if len(out) > 0 {
pc.writeBuf = out[:0]
return len(p), writeFull(pc.Conn, out)
}
pc.writeBuf = out[:0]
return len(p), nil
}
func (pc *PackedConn) Flush() error {
pc.writeMu.Lock()
defer pc.writeMu.Unlock()
out := pc.writeBuf[:0]
if pc.bitCount > 0 {
group := byte(pc.bitBuf << (6 - pc.bitCount))
pc.bitBuf = 0
pc.bitCount = 0
out = append(out, pc.table.layout.groupByte(group&0x3F))
out = append(out, pc.padMarker)
}
out = pc.maybeAddPadding(out)
if len(out) > 0 {
pc.writeBuf = out[:0]
return writeFull(pc.Conn, out)
}
return nil
}
func writeFull(w io.Writer, b []byte) error {
for len(b) > 0 {
n, err := w.Write(b)
if err != nil {
return err
}
if n == 0 {
return io.ErrShortWrite
}
b = b[n:]
}
return nil
}
func (pc *PackedConn) Read(p []byte) (int, error) {
if n, ok := drainPending(p, &pc.pendingData); ok {
return n, nil
}
for {
nr, rErr := pc.reader.Read(pc.rawBuf)
if nr > 0 {
rBuf := pc.readBitBuf
rBits := pc.readBits
padMarker := pc.padMarker
layout := pc.table.layout
for _, b := range pc.rawBuf[:nr] {
if !layout.hintTable[b] {
if b == padMarker {
rBuf = 0
rBits = 0
}
continue
}
group, ok := layout.decodePackedGroup(b)
if !ok {
return 0, ErrInvalidSudokuMapMiss
}
rBuf = (rBuf << 6) | uint64(group)
rBits += 6
if rBits >= 8 {
rBits -= 8
val := byte(rBuf >> rBits)
pc.pendingData.appendByte(val)
if rBits == 0 {
rBuf = 0
} else {
rBuf &= (uint64(1) << rBits) - 1
}
}
}
pc.readBitBuf = rBuf
pc.readBits = rBits
}
if rErr != nil {
if rErr == io.EOF {
pc.readBitBuf = 0
pc.readBits = 0
}
if pc.pendingData.available() > 0 {
break
}
return 0, rErr
}
if pc.pendingData.available() > 0 {
break
}
}
n, _ := drainPending(p, &pc.pendingData)
return n, nil
}
func (pc *PackedConn) getPaddingByte() byte {
return pc.padPool[pc.rng.Intn(len(pc.padPool))]
}

View File

@@ -0,0 +1,42 @@
package sudoku
const probOne = uint64(1) << 32
func pickPaddingThreshold(r randomSource, pMin, pMax int) uint64 {
if r == nil {
return 0
}
if pMin < 0 {
pMin = 0
}
if pMax < pMin {
pMax = pMin
}
if pMax > 100 {
pMax = 100
}
if pMin > 100 {
pMin = 100
}
min := uint64(pMin) * probOne / 100
max := uint64(pMax) * probOne / 100
if max <= min {
return min
}
u := uint64(r.Uint32())
return min + (u * (max - min) >> 32)
}
func shouldPad(r randomSource, threshold uint64) bool {
if threshold == 0 {
return false
}
if threshold >= probOne {
return true
}
if r == nil {
return false
}
return uint64(r.Uint32()) < threshold
}

View File

@@ -0,0 +1,57 @@
package sudoku
type pendingBuffer struct {
data []byte
off int
}
func newPendingBuffer(capacity int) pendingBuffer {
return pendingBuffer{data: make([]byte, 0, capacity)}
}
func (p *pendingBuffer) available() int {
if p == nil {
return 0
}
return len(p.data) - p.off
}
func (p *pendingBuffer) reset() {
if p == nil {
return
}
p.data = p.data[:0]
p.off = 0
}
func (p *pendingBuffer) ensureAppendCapacity(extra int) {
if p == nil || extra <= 0 || p.off == 0 {
return
}
if cap(p.data)-len(p.data) >= extra {
return
}
unread := len(p.data) - p.off
copy(p.data[:unread], p.data[p.off:])
p.data = p.data[:unread]
p.off = 0
}
func (p *pendingBuffer) appendByte(b byte) {
p.ensureAppendCapacity(1)
p.data = append(p.data, b)
}
func drainPending(dst []byte, pending *pendingBuffer) (int, bool) {
if pending == nil || pending.available() == 0 {
return 0, false
}
n := copy(dst, pending.data[pending.off:])
pending.off += n
if pending.off == len(pending.data) {
pending.reset()
}
return n, true
}

View File

@@ -0,0 +1,56 @@
package sudoku
import (
crypto_rand "crypto/rand"
"encoding/binary"
"time"
)
type randomSource interface {
Uint32() uint32
Uint64() uint64
Intn(n int) int
}
type sudokuRand struct {
state uint64
}
func newSeededRand() *sudokuRand {
seed := time.Now().UnixNano()
var seedBytes [8]byte
if _, err := crypto_rand.Read(seedBytes[:]); err == nil {
seed = int64(binary.BigEndian.Uint64(seedBytes[:]))
}
return newSudokuRand(seed)
}
func newSudokuRand(seed int64) *sudokuRand {
state := uint64(seed)
if state == 0 {
state = 0x9e3779b97f4a7c15
}
return &sudokuRand{state: state}
}
func (r *sudokuRand) Uint64() uint64 {
if r == nil {
return 0
}
r.state += 0x9e3779b97f4a7c15
z := r.state
z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9
z = (z ^ (z >> 27)) * 0x94d049bb133111eb
return z ^ (z >> 31)
}
func (r *sudokuRand) Uint32() uint32 {
return uint32(r.Uint64() >> 32)
}
func (r *sudokuRand) Intn(n int) int {
if n <= 1 {
return 0
}
return int((uint64(r.Uint32()) * uint64(n)) >> 32)
}

View File

@@ -0,0 +1,214 @@
package sudoku
import (
"crypto/sha256"
"encoding/binary"
"errors"
"math/rand"
"strings"
)
var (
ErrInvalidSudokuMapMiss = errors.New("INVALID_SUDOKU_MAP_MISS")
)
type Table struct {
EncodeTable [256][][4]byte
DecodeMap map[uint32]byte
PaddingPool []byte
IsASCII bool // 标记当前模式
layout *byteLayout
opposite *Table
hint uint32
}
// NewTable initializes the obfuscation tables with built-in layouts.
// Equivalent to calling NewTableWithCustom(key, mode, "").
func NewTable(key string, mode string) *Table {
t, err := NewTableWithCustom(key, mode, "")
if err != nil {
panic(err)
}
return t
}
// NewTableWithCustom initializes the uplink/probe Sudoku table using either predefined
// or directional layouts. Directional modes such as "up_ascii_down_entropy" return the
// client->server table and internally attach the opposite direction table for runtime use.
// The customPattern must contain 8 characters with exactly 2 x, 2 p, and 4 v (case-insensitive).
func NewTableWithCustom(key string, mode string, customPattern string) (*Table, error) {
asciiMode, err := ParseASCIIMode(mode)
if err != nil {
return nil, err
}
uplinkPattern := customPatternForToken(asciiMode.Uplink, customPattern)
downlinkPattern := customPatternForToken(asciiMode.Downlink, customPattern)
hint := tableHintFingerprint(key, asciiMode.Canonical(), uplinkPattern, downlinkPattern)
uplink, err := newSingleDirectionTable(key, asciiMode.uplinkPreference(), uplinkPattern)
if err != nil {
return nil, err
}
uplink.hint = hint
if asciiMode.Uplink == asciiMode.Downlink {
uplink.opposite = uplink
return uplink, nil
}
downlink, err := newSingleDirectionTable(key, asciiMode.downlinkPreference(), downlinkPattern)
if err != nil {
return nil, err
}
downlink.hint = hint
uplink.opposite = downlink
downlink.opposite = uplink
return uplink, nil
}
func newSingleDirectionTable(key string, mode string, customPattern string) (*Table, error) {
layout, err := resolveLayout(mode, customPattern)
if err != nil {
return nil, err
}
t := &Table{
DecodeMap: make(map[uint32]byte),
IsASCII: layout.name == "ascii",
layout: layout,
}
t.PaddingPool = append(t.PaddingPool, layout.paddingPool...)
// 生成数独网格 (逻辑不变)
allGrids := GenerateAllGrids()
h := sha256.New()
h.Write([]byte(key))
seed := int64(binary.BigEndian.Uint64(h.Sum(nil)[:8]))
rng := rand.New(rand.NewSource(seed))
shuffledGrids := make([]Grid, 288)
copy(shuffledGrids, allGrids)
rng.Shuffle(len(shuffledGrids), func(i, j int) {
shuffledGrids[i], shuffledGrids[j] = shuffledGrids[j], shuffledGrids[i]
})
// 预计算组合
var combinations [][]int
var combine func(int, int, []int)
combine = func(s, k int, c []int) {
if k == 0 {
tmp := make([]int, len(c))
copy(tmp, c)
combinations = append(combinations, tmp)
return
}
for i := s; i <= 16-k; i++ {
c = append(c, i)
combine(i+1, k-1, c)
c = c[:len(c)-1]
}
}
combine(0, 4, []int{})
// 构建映射表
for byteVal := 0; byteVal < 256; byteVal++ {
targetGrid := shuffledGrids[byteVal]
for _, positions := range combinations {
var currentHints [4]byte
// 1. 计算抽象提示 (Abstract Hints)
// 我们先计算出 val 和 pos后面再根据模式编码成 byte
var rawParts [4]struct{ val, pos byte }
for i, pos := range positions {
val := targetGrid[pos] // 1..4
rawParts[i] = struct{ val, pos byte }{val, uint8(pos)}
}
// 检查唯一性 (数独逻辑)
matchCount := 0
for _, g := range allGrids {
match := true
for _, p := range rawParts {
if g[p.pos] != p.val {
match = false
break
}
}
if match {
matchCount++
if matchCount > 1 {
break
}
}
}
if matchCount == 1 {
// 唯一确定,生成最终编码字节
for i, p := range rawParts {
currentHints[i] = t.layout.hintByte(p.val-1, p.pos)
}
t.EncodeTable[byteVal] = append(t.EncodeTable[byteVal], currentHints)
// 生成解码键 (需要对 Hints 进行排序以忽略传输顺序)
key := packHintsToKey(currentHints)
t.DecodeMap[key] = byte(byteVal)
}
}
}
return t, nil
}
func customPatternForToken(token string, customPattern string) string {
if token == asciiModeTokenEntropy {
return customPattern
}
return ""
}
func (t *Table) OppositeDirection() *Table {
if t == nil || t.opposite == nil {
return t
}
return t.opposite
}
func (t *Table) Hint() uint32 {
if t == nil {
return 0
}
return t.hint
}
func tableHintFingerprint(key string, mode string, uplinkPattern string, downlinkPattern string) uint32 {
sum := sha256.Sum256([]byte(strings.Join([]string{
"sudoku-table-hint",
key,
mode,
strings.ToLower(strings.TrimSpace(uplinkPattern)),
strings.ToLower(strings.TrimSpace(downlinkPattern)),
}, "\x00")))
return binary.BigEndian.Uint32(sum[:4])
}
func packHintsToKey(hints [4]byte) uint32 {
// Sorting network for 4 elements (Bubble sort unrolled)
// Swap if a > b
if hints[0] > hints[1] {
hints[0], hints[1] = hints[1], hints[0]
}
if hints[2] > hints[3] {
hints[2], hints[3] = hints[3], hints[2]
}
if hints[0] > hints[2] {
hints[0], hints[2] = hints[2], hints[0]
}
if hints[1] > hints[3] {
hints[1], hints[3] = hints[3], hints[1]
}
if hints[1] > hints[2] {
hints[1], hints[2] = hints[2], hints[1]
}
return uint32(hints[0])<<24 | uint32(hints[1])<<16 | uint32(hints[2])<<8 | uint32(hints[3])
}

View File

@@ -0,0 +1,38 @@
package sudoku
import "fmt"
// TableSet is a small helper for managing multiple Sudoku tables (e.g. for per-connection rotation).
// It is intentionally decoupled from the tunnel/app layers.
type TableSet struct {
Tables []*Table
}
// NewTableSet builds one or more tables from key/mode and a list of custom X/P/V patterns.
// If patterns is empty, it builds a single default table (customPattern="").
func NewTableSet(key string, mode string, patterns []string) (*TableSet, error) {
if len(patterns) == 0 {
t, err := NewTableWithCustom(key, mode, "")
if err != nil {
return nil, err
}
return &TableSet{Tables: []*Table{t}}, nil
}
tables := make([]*Table, 0, len(patterns))
for i, pattern := range patterns {
t, err := NewTableWithCustom(key, mode, pattern)
if err != nil {
return nil, fmt.Errorf("build table[%d] (%q): %w", i, pattern, err)
}
tables = append(tables, t)
}
return &TableSet{Tables: tables}, nil
}
func (ts *TableSet) Candidates() []*Table {
if ts == nil {
return nil
}
return ts.Tables
}