release: 1.4.4 — channel «Облако» (WebSocket via KeenDNS cloud to own Keenetic): wsgw gateway, Android 1.4.4, iOS, docs/cloud.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Бобырев Андрей
2026-10-31 05:16:31 +03:00
parent d795bc6f70
commit dde6403cea
38 changed files with 855 additions and 58 deletions

147
wsgw/main.go Normal file
View File

@@ -0,0 +1,147 @@
// wsgw: WebSocket -> UDP gateway for the PCA Phobos "Keenetic cloud" transport.
// Keenetic `ip http proxy` (KeenDNS cloud, TLS on the router) forwards wss://<sub>/ here as plain ws.
// One binary WS message = one obfuscated UDP packet; each WS connection gets its own UDP socket to -t.
package main
import (
"bufio"
"crypto/sha1"
"encoding/base64"
"encoding/binary"
"flag"
"io"
"log"
"net"
"net/http"
"strings"
"time"
)
const wsGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
func main() {
listen := flag.String("l", "0.0.0.0:18090", "listen addr (http)")
target := flag.String("t", "", "UDP target host:port (server obfuscator)")
flag.Parse()
if *target == "" {
log.Fatal("-t host:port required")
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if !strings.EqualFold(r.Header.Get("Upgrade"), "websocket") {
http.Error(w, "ok", http.StatusUpgradeRequired)
return
}
h, ok := w.(http.Hijacker)
if !ok {
return
}
conn, rw, err := h.Hijack()
if err != nil {
return
}
sum := sha1.Sum([]byte(r.Header.Get("Sec-WebSocket-Key") + wsGUID))
rw.WriteString("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: " +
base64.StdEncoding.EncodeToString(sum[:]) + "\r\n\r\n")
rw.Flush()
go serve(conn, rw.Reader, *target)
})
log.Printf("wsgw %s -> udp %s", *listen, *target)
log.Fatal(http.ListenAndServe(*listen, nil))
}
func serve(c net.Conn, br *bufio.Reader, target string) {
defer c.Close()
u, err := net.Dial("udp", target)
if err != nil {
log.Printf("udp %s: %v", target, err)
return
}
defer u.Close()
go func() { // udp -> ws
buf := make([]byte, 65535)
for {
u.SetReadDeadline(time.Now().Add(5 * time.Minute))
n, err := u.Read(buf)
if err != nil {
c.Close()
return
}
if writeFrame(c, 2, buf[:n]) != nil {
return
}
}
}()
for { // ws -> udp
op, p, err := readFrame(br)
if err != nil {
return
}
switch op {
case 2, 0:
u.Write(p)
case 8:
return
case 9:
writeFrame(c, 10, p)
}
}
}
// readFrame reads one client frame (masked). Fragmented messages are not used by our clients.
func readFrame(r *bufio.Reader) (byte, []byte, error) {
var h [2]byte
if _, err := io.ReadFull(r, h[:]); err != nil {
return 0, nil, err
}
n := uint64(h[1] & 0x7f)
switch n {
case 126:
var e [2]byte
if _, err := io.ReadFull(r, e[:]); err != nil {
return 0, nil, err
}
n = uint64(binary.BigEndian.Uint16(e[:]))
case 127:
var e [8]byte
if _, err := io.ReadFull(r, e[:]); err != nil {
return 0, nil, err
}
n = binary.BigEndian.Uint64(e[:])
}
if n > 65535 {
return 0, nil, io.ErrUnexpectedEOF
}
var mask [4]byte
masked := h[1]&0x80 != 0
if masked {
if _, err := io.ReadFull(r, mask[:]); err != nil {
return 0, nil, err
}
}
p := make([]byte, n)
if _, err := io.ReadFull(r, p); err != nil {
return 0, nil, err
}
if masked {
for i := range p {
p[i] ^= mask[i&3]
}
}
return h[0] & 0x0f, p, nil
}
func writeFrame(w io.Writer, op byte, p []byte) error {
hdr := []byte{0x80 | op, 0}
switch n := len(p); {
case n < 126:
hdr[1] = byte(n)
case n < 65536:
hdr[1] = 126
hdr = binary.BigEndian.AppendUint16(hdr, uint16(n))
default:
hdr[1] = 127
hdr = binary.BigEndian.AppendUint64(hdr, uint64(n))
}
_, err := w.Write(append(hdr, p...))
return err
}