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 byte16 bytes1 byteM bytes1 byte2 bytes1 byteS bytesX bytes
Protocol VersionEquivalent UUIDAdditional Information Length MAdditional Information ProtoBufInstructionPortAddress TypeAddressRequest Data

Response Data Packet

1 Byte1 ByteN BytesY Bytes
Protocol Version, consistent with the requestLength of additional information NAdditional information in ProtoBufResponse 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