VLESS over WebSocket
Xray VLESS over WebSocket is a protocol used for creating secure, high-performance, and flexible network connections, commonly used in scenarios like bypassing network restrictions or enhancing privacy.
V2rayN,V2rayA,Clash or ShadowRocket
+------------------+
| VLESS Client |
| +-----------+ |
| | TLS Layer | |
| +-----------+ |
| | WebSocket | |
| +-----------+ |
+--------|---------+
|
| Encrypted VLESS Traffic (wss://)
|
+--------------------------------------+
| Internet (TLS Secured) |
+--------------------------------------+
|
|
+-----------------------------------+
| Reverse Proxy Server |
| (e.g., Nginx or Cloudflare) |
| |
| +---------------------------+ |
| | HTTPS/TLS Termination | |
| +---------------------------+ |
| | WebSocket Proxy (wss://) | |
| +---------------------------+ |
| Forward to VLESS Server |
+------------------|----------------+
|
+--------------------------------+
| Unchain Server |
| |
| +------------------------+ |
| | WebSocket Handler | |
| +------------------------+ |
| | VLESS Core Processing | |
| +------------------------+ |
| |
| Forward Traffic to Target |
+------------------|-------------+
|
+-----------------+
| Target Server |
| or Destination |
+-----------------+
VLESS
VLESS is a stateless lightweight transmission protocol that can be used as a bridge between Xray clients and servers.
Request Data Packet
1 byte | 16 bytes | 1 byte | M bytes | 1 byte | 2 bytes | 1 byte | S bytes | X bytes |
---|---|---|---|---|---|---|---|---|
Protocol Version | Equivalent UUID | Additional Information Length M | Additional Information ProtoBuf | Instruction | Port | Address Type | Address | Request Data |
Response Data Packet
1 Byte | 1 Byte | N Bytes | Y Bytes |
---|---|---|---|
Protocol Version, consistent with the request | Length of additional information N | Additional information in ProtoBuf | Response data |
VLESS over WebSocket
VLESS over WebSocket is a specific V2Ray configuration where the VLESS protocol, which is used for encrypted, high-performance communication, is encapsulated in WebSocket frames to traverse firewalls and bypass network restrictions.
WebSocket, as a protocol designed for persistent, bidirectional communication (often used for real-time web applications), is not typically blocked by firewalls or DPI (Deep Packet Inspection) systems. By using WebSocket as a transport layer for VLESS traffic, users can make their V2Ray connection look like normal web traffic, which is much harder to detect or block. This makes it a highly effective method to bypass internet censorship and restrictive network environments.
1. HTTP server
We use the Go standard library to start an HTTP server with a single WebSocket handler.
package node
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/unchainese/unchain/internal/global"
"log"
"log/slog"
"net/http"
"os"
"runtime"
"sync"
"time"
)
type App struct {
cfg *global.Config
userUsedTrafficKb sync.Map // string -> int64
svr *http.Server
exitSignal chan os.Signal
}
func (app *App) httpSvr() {
mux := http.NewServeMux()
mux.HandleFunc("/ws-vless", app.WsVLESS) // proxy of VLESS over WebSocket
mux.HandleFunc("/", app.Ping)
server := &http.Server{
Addr: app.cfg.ListenAddr,
Handler: mux,
}
app.svr = server
}
2. HTTP Handler
The WsVLESS
handler checks the request path and upgrades the connection to a WebSocket connection if the path is /ws-vless
.
If the path is not /ws-vless
, the handler responds with a JSON message. If the connection is upgraded to a WebSocket connection,
the handler reads the early data from the WebSocket connection and parses the VLESS
data.
const (
buffSize = 8 << 10
contentTypeHeader = "Content-Type"
contentTypeJSON = "application/json"
upgradeHeader = "Upgrade"
websocketProtocol = "websocket"
secWebSocketProto = "sec-websocket-protocol"
)
var upGrader = websocket.Upgrader{
ReadBufferSize: buffSize,
WriteBufferSize: buffSize,
CheckOrigin: func(r *http.Request) bool {
// Allow all connections by default
return true
},
}
func (app *App) WsVLESS(w http.ResponseWriter, r *http.Request) {
uid := r.PathValue("uid")
//check can upgrade websocket
if r.Header.Get(upgradeHeader) != websocketProtocol {
//json response hello world
w.Header().Set(contentTypeHeader, contentTypeJSON)
w.WriteHeader(http.StatusOK)
data := map[string]string{"msg": "pong", "uid": uid}
json.NewEncoder(w).Encode(data)
return
}
ctx := r.Context()
earlyDataHeader := r.Header.Get(secWebSocketProto)
earlyData, err := base64.RawURLEncoding.DecodeString(earlyDataHeader)
if err != nil {
log.Println("Error decoding early data:", err)
}
ws, err := upGrader.Upgrade(w, r, nil)
if err != nil {
fmt.Println("Error upgrading to websocket:", err)
return
}
defer ws.Close()
if len(earlyData) == 0 {
mt, p, err := ws.ReadMessage()
if err != nil {
log.Println("Error reading message:", err)
return
}
if mt == websocket.BinaryMessage {
earlyData = p
}
}
vData, err := schema.VLESSParse(earlyData)
if err != nil {
log.Println("Error parsing vless data:", err)
return
}
if app.IsUserNotAllowed(vData.UUID()) {
return
}
sessionTrafficByteN := int64(len(earlyData))
if vData.DstProtocol == "udp" {
sessionTrafficByteN += vlessUDP(ctx, vData, ws)
} else if vData.DstProtocol == "tcp" {
sessionTrafficByteN += vlessTCP(ctx, vData, ws)
} else {
log.Println("Error unsupported protocol:", vData.DstProtocol)
return
}
app.trafficInc(vData.UUID(), sessionTrafficByteN)
}
3. Handle VLESS data packet
The ProtoVLESS
struct represents the VLESS data packet, and the VLESSParse
function parses the VLESS data packet.
The DataUdp
and DataTcp
methods return the UDP and TCP data, respectively, and the AddrUdp
and HostIP
methods return the UDP address and host IP, respectively.
Read the document to learn more.
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"log/slog"
"net"
"github.com/google/uuid"
)
type ProtoVLESS struct {
userID uuid.UUID
DstProtocol string //tcp or udp
dstHost string
dstHostType string //ipv6 or ipv4,domain
dstPort uint16
Version byte
payload []byte
}
func (h ProtoVLESS) UUID() string {
return h.userID.String()
}
func (h ProtoVLESS) DataUdp() []byte {
allData := make([]byte, 0)
chunk := h.payload
for index := 0; index < len(chunk); {
if index+2 > len(chunk) {
fmt.Println("Incomplete length buffer")
return nil
}
lengthBuffer := chunk[index : index+2]
udpPacketLength := binary.BigEndian.Uint16(lengthBuffer)
if index+2+int(udpPacketLength) > len(chunk) {
fmt.Println("Incomplete UDP packet")
return nil
}
udpData := chunk[index+2 : index+2+int(udpPacketLength)]
index = index + 2 + int(udpPacketLength)
allData = append(allData, udpData...)
}
return allData
}
func (h ProtoVLESS) DataTcp() []byte {
return h.payload
}
func (h ProtoVLESS) AddrUdp() *net.UDPAddr {
return &net.UDPAddr{IP: h.HostIP(), Port: int(h.dstPort)}
}
func (h ProtoVLESS) HostIP() net.IP {
ip := net.ParseIP(h.dstHost)
if ip == nil {
ips, err := net.LookupIP(h.dstHost)
if err != nil {
h.Logger().Error("failed to resolve domain", "err", err.Error())
return net.IPv4zero
}
if len(ips) == 0 {
return net.IPv4zero
}
return ips[0]
}
return ip
}
func (h ProtoVLESS) HostPort() string {
return net.JoinHostPort(h.dstHost, fmt.Sprintf("%d", h.dstPort))
}
func (h ProtoVLESS) Logger() *slog.Logger {
return slog.With("userID", h.userID.String(), "network", h.DstProtocol, "addr", h.HostPort())
}
// VLESSParse https://xtls.github.io/development/protocols/vless.html
func VLESSParse(buf []byte) (*ProtoVLESS, error) {
payload := &ProtoVLESS{
userID: uuid.Nil,
DstProtocol: "",
dstHost: "",
dstPort: 0,
Version: 0,
payload: nil,
}
if len(buf) < 24 {
return payload, errors.New("invalid payload length")
}
payload.Version = buf[0]
payload.userID = uuid.Must(uuid.FromBytes(buf[1:17]))
extraInfoProtoBufLen := buf[17]
command := buf[18+extraInfoProtoBufLen]
switch command {
case 1:
payload.DstProtocol = "tcp"
case 2:
payload.DstProtocol = "udp"
default:
return payload, fmt.Errorf("command %d is not supported, command 01-tcp, 02-udp, 03-mux", command)
}
portIndex := 18 + extraInfoProtoBufLen + 1
payload.dstPort = binary.BigEndian.Uint16(buf[portIndex : portIndex+2])
addressIndex := portIndex + 2
addressType := buf[addressIndex]
addressValueIndex := addressIndex + 1
switch addressType {
case 1: // IPv4
if len(buf) < int(addressValueIndex+net.IPv4len) {
return nil, fmt.Errorf("invalid IPv4 address length")
}
payload.dstHost = net.IP(buf[addressValueIndex : addressValueIndex+net.IPv4len]).String()
payload.payload = buf[addressValueIndex+net.IPv4len:]
payload.dstHostType = "ipv4"
case 2: // domain
addressLength := buf[addressValueIndex]
addressValueIndex++
if len(buf) < int(addressValueIndex)+int(addressLength) {
return nil, fmt.Errorf("invalid domain address length")
}
payload.dstHost = string(buf[addressValueIndex : int(addressValueIndex)+int(addressLength)])
payload.payload = buf[int(addressValueIndex)+int(addressLength):]
payload.dstHostType = "domain"
case 3: // IPv6
if len(buf) < int(addressValueIndex+net.IPv6len) {
return nil, fmt.Errorf("invalid IPv6 address length")
}
payload.dstHost = net.IP(buf[addressValueIndex : addressValueIndex+net.IPv6len]).String()
payload.payload = buf[addressValueIndex+net.IPv6len:]
payload.dstHostType = "ipv6"
default:
return nil, fmt.Errorf("addressType %d is not supported", addressType)
}
return payload, nil
}
4. Pipe TCP
The vlessTCP
function, which handles TCP connections for the VLESS protocol over WebSocket.
The function reads from the WebSocket connection and writes to the TCP connection, and vice versa.
func vlessTCP(ctx context.Context, sv *schema.ProtoVLESS, ws *websocket.Conn) int64 {
logger := sv.Logger()
conn, headerVLESS, err := startDstConnection(sv, time.Millisecond*1000)
if err != nil {
logger.Error("Error starting session:", "err", err)
return 0
}
defer conn.Close()
logger.Info("Session started tcp")
//write early data
_, err = conn.Write(sv.DataTcp())
if err != nil {
logger.Error("Error writing early data to TCP connection:", "err", err)
return 0
}
var trafficMeter atomic.Int64
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
default:
mt, message, err := ws.ReadMessage()
trafficMeter.Add(int64(len(message)))
if websocket.IsCloseError(err, websocket.CloseNormalClosure) {
return
}
if err != nil {
logger.Error("Error reading message:", "err", err)
return
}
if mt != websocket.BinaryMessage {
continue
}
_, err = conn.Write(message)
if err != nil {
logger.Error("Error writing to TCP connection:", "err", err)
return
}
}
}
}()
go func() {
defer wg.Done()
hasNotSentHeader := true
buf := make([]byte, buffSize)
for {
select {
case <-ctx.Done():
return
default:
n, err := conn.Read(buf)
trafficMeter.Add(int64(n))
if errors.Is(err, io.EOF) {
return
}
if err != nil {
logger.Error("Error reading from TCP connection:", "err", err)
return
}
data := buf[:n]
// send header data only for the first time
if hasNotSentHeader {
hasNotSentHeader = false
data = append(headerVLESS, data...)
}
err = ws.WriteMessage(websocket.BinaryMessage, data)
if err != nil {
logger.Error("Error writing to websocket:", "err", err)
return
}
}
}
}()
wg.Wait()
return trafficMeter.Load()
}
5. Pipe UDP
The vlessUDP
function, which handles UDP connections for the VLESS protocol over WebSocket.
func vlessUDP(_ context.Context, sv *schema.ProtoVLESS, ws *websocket.Conn) (trafficMeter int64) {
logger := sv.Logger()
conn, headerVLESS, err := startDstConnection(sv, time.Millisecond*1000)
if err != nil {
logger.Error("Error starting session:", "err", err)
return
}
defer conn.Close()
trafficMeter += int64(len(sv.DataUdp()))
//write early data
_, err = conn.Write(sv.DataUdp())
if err != nil {
logger.Error("Error writing early data to TCP connection:", "err", err)
return
}
buf := make([]byte, buffSize)
n, err := conn.Read(buf)
if err != nil {
logger.Error("Error reading from TCP connection:", "err", err)
return
}
udpDataLen1 := (n >> 8) & 0xff
udpDataLen2 := n & 0xff
headerVLESS = append(headerVLESS, byte(udpDataLen1), byte(udpDataLen2))
headerVLESS = append(headerVLESS, buf[:n]...)
trafficMeter += int64(len(headerVLESS))
err = ws.WriteMessage(websocket.BinaryMessage, headerVLESS)
if err != nil {
logger.Error("Error writing to websocket:", "err", err)
return
}
return trafficMeter
}
Conclusion
The above code is the simplest example of how to develop a VLESS over WebSocket server in Go.
You can find the full code in the VLESS over WebSocket repository