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:
21
wsgw/README.md
Normal file
21
wsgw/README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# wsgw — шлюз WebSocket → UDP (транспорт «Облако» / Keenetic-облако)
|
||||
|
||||
Телефон без доступа к серверу → облако KeenDNS (HTTPS по SNI) → свой Keenetic → `wsgw` → UDP обфускатора сервера.
|
||||
Одно бинарное WS-сообщение = один обфусцированный пакет; на каждое WS-соединение — свой UDP-сокет к `-t`.
|
||||
|
||||
Роутер (Entware):
|
||||
```sh
|
||||
cp bin/wsgw-aarch64 /opt/sbin/wsgw # mipsel/mips — по uname -m
|
||||
cp router/S80wsgw /opt/etc/init.d/ && chmod +x /opt/sbin/wsgw /opt/etc/init.d/S80wsgw
|
||||
printf 'WSGW_LISTEN=192.168.1.1:18090\nWSGW_TARGET=<сервер>:<порт обфускатора>\n' > /opt/etc/wsgw.conf
|
||||
/opt/etc/init.d/S80wsgw start
|
||||
ndmc -c 'ip http proxy phobos'; ndmc -c 'ip http proxy phobos upstream http 192.168.1.1 18090'
|
||||
ndmc -c 'ip http proxy phobos domain ndns'; ndmc -c 'ip http proxy phobos security-level public'
|
||||
ndmc -c 'ndns book-name <имя> <домен> cloud'; ndmc -c 'ip http ssl acme get phobos.<имя>.<домен>'
|
||||
ndmc -c 'system configuration save'
|
||||
```
|
||||
Требования: `ip http port 80` (иначе ACME не выпустит сертификат), WAN:443 не проброшен на другой хост
|
||||
(облако шлёт TLS поддоменов на WAN:443). Если `acme get` висит в `get-pending` — перезагрузка роутера и команда ещё раз.
|
||||
|
||||
Клиент Android 1.4.4: канал «Облако», адрес `wss://phobos.<имя>.<домен>/` (или `# pca-ws = wss://…` в ссылке).
|
||||
Сборка: `./build.sh` (Go, статические linux arm64/mipsle/mips/amd64).
|
||||
BIN
wsgw/bin/wsgw-aarch64
Executable file
BIN
wsgw/bin/wsgw-aarch64
Executable file
Binary file not shown.
BIN
wsgw/bin/wsgw-mips
Executable file
BIN
wsgw/bin/wsgw-mips
Executable file
Binary file not shown.
BIN
wsgw/bin/wsgw-mipsel
Executable file
BIN
wsgw/bin/wsgw-mipsel
Executable file
Binary file not shown.
BIN
wsgw/bin/wsgw-x86_64
Executable file
BIN
wsgw/bin/wsgw-x86_64
Executable file
Binary file not shown.
7
wsgw/build.sh
Executable file
7
wsgw/build.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
# Static builds for Keenetic (Entware) and Linux servers.
|
||||
cd "$(dirname "$0")"; mkdir -p bin
|
||||
for a in arm64:aarch64 mipsle:mipsel mips:mips amd64:x86_64; do
|
||||
GOOS=linux GOARCH=${a%%:*} GOMIPS=softfloat CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o bin/wsgw-${a##*:} . || exit 1
|
||||
done
|
||||
ls -l bin
|
||||
3
wsgw/go.mod
Normal file
3
wsgw/go.mod
Normal file
@@ -0,0 +1,3 @@
|
||||
module wsgw
|
||||
|
||||
go 1.22
|
||||
147
wsgw/main.go
Normal file
147
wsgw/main.go
Normal 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
|
||||
}
|
||||
10
wsgw/router/S80wsgw
Normal file
10
wsgw/router/S80wsgw
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
# PCA Phobos: WebSocket -> UDP gateway (transport "Keenetic cloud"). Config: /opt/etc/wsgw.conf
|
||||
[ -f /opt/etc/wsgw.conf ] && . /opt/etc/wsgw.conf
|
||||
ENABLED=yes
|
||||
PROCS=wsgw
|
||||
ARGS="-l ${WSGW_LISTEN:-127.0.0.1:18090} -t $WSGW_TARGET"
|
||||
PREARGS=""
|
||||
DESC=$PROCS
|
||||
PATH=/opt/sbin:/opt/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
. /opt/etc/init.d/rc.func
|
||||
Reference in New Issue
Block a user