websocket tunneling

published: August 12, 2025

websocket tunneling exploits the websocket protocol’s persistent, bidirectional communication to create high-performance covert channels. after initial http handshake, websocket provides near-native tcp performance with minimal overhead.

technical description

websocket (rfc 6455) upgrades http connections to full-duplex communication channels:

  • initial http upgrade request
  • persistent connection after handshake
  • binary or text frame support
  • minimal framing overhead (~2-14 bytes)
  • works over standard ports (80/443)
  • tls encryption on wss://

protocol upgrade:

GET /socket HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

implementation: wstunnel

overview

wstunnel (https://github.com/erebe/wstunnel) provides high-performance tunneling:

  • websocket and http/2 transport
  • tls/mtls support
  • udp over websocket
  • reverse tunneling
  • achieves gigabit speeds

installation

# download binary
wget https://github.com/erebe/wstunnel/releases/latest/download/wstunnel-linux-x64
chmod +x wstunnel-linux-x64
sudo mv wstunnel-linux-x64 /usr/local/bin/wstunnel

# or using cargo
cargo install wstunnel

server setup

# basic websocket server
wstunnel server ws://0.0.0.0:8080

# with tls (wss)
wstunnel server wss://0.0.0.0:443 --tls-cert cert.pem --tls-key key.pem

# with client authentication
wstunnel server wss://0.0.0.0:443 \
  --tls-cert cert.pem \
  --tls-key key.pem \
  --restrict-to 127.0.0.1:22

# http2 transport
wstunnel server --http2 https://0.0.0.0:443

client usage

# forward local port to remote
wstunnel client -L tcp://8000:localhost:22 ws://server:8080

# multiple forwards
wstunnel client \
  -L tcp://8000:localhost:22 \
  -L tcp://8080:localhost:80 \
  ws://server:8080

# udp forwarding
wstunnel client -L udp://5353:8.8.8.8:53 ws://server:8080

# reverse tunnel (expose local service)
wstunnel client -R tcp://0.0.0.0:8080:localhost:3000 ws://server:8080

# through http proxy
wstunnel client --http-proxy proxy:3128 \
  -L tcp://8000:localhost:22 \
  wss://server:443

implementation: websockify

overview

websockify (https://github.com/novnc/websockify) bridges websocket to tcp:

  • originally for novnc
  • python and c implementations
  • supports multiple backends
  • token-based authentication

setup

# install
pip install websockify

# basic proxy
websockify 6080 localhost:5900

# with ssl
websockify --cert cert.pem --key key.pem 6443 localhost:22

# with authentication
websockify --auth-plugin BasicAuth --auth-source users.txt 6080 localhost:5900

# token-based targets
echo "token1: localhost:22" > tokens.txt
websockify --token-plugin TokenFile --token-source tokens.txt 6080

javascript client

// browser websocket client
const ws = new WebSocket('wss://server.com:6443');
ws.binaryType = 'arraybuffer';

ws.onopen = () => {
  console.log('tunnel connected');
  // send ssh traffic through websocket
};

ws.onmessage = (event) => {
  // receive tunneled data
  const data = new Uint8Array(event.data);
  processData(data);
};

traffic analysis

websocket frame structure

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+-------------------------------+
|     Extended payload length continued, if payload len == 127 |
+-------------------------------+-------------------------------+
|                               | Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------+-------------------------------+

packet analysis

from scapy.all import *
import struct

def analyze_websocket(packet):
    if packet.haslayer(TCP) and packet[TCP].payload:
        payload = bytes(packet[TCP].payload)

        # check for websocket upgrade
        if b'Upgrade: websocket' in payload:
            print("websocket handshake detected")
            return

        # analyze websocket frame
        if len(payload) >= 2:
            byte1, byte2 = struct.unpack('BB', payload[:2])
            fin = (byte1 >> 7) & 1
            opcode = byte1 & 0x0f
            masked = (byte2 >> 7) & 1
            payload_len = byte2 & 0x7f

            if opcode in [0x1, 0x2]:  # text or binary frame
                print(f"websocket data: opcode={opcode}, len={payload_len}")

sniff(filter="tcp port 8080", prn=analyze_websocket)

performance characteristics

bandwidth

  • overhead: ~2% compared to raw tcp
  • throughput: 95-98% of tcp capacity
  • gigabit speeds achievable
  • minimal latency addition

scalability

  • thousands of concurrent connections
  • low memory footprint per connection
  • efficient event-driven architecture
  • multiplexing over single tcp connection

comparison

transportoverheadlatencydetection
raw tcp0%baselineeasy
websocket2-5%+1-5msmedium
http polling50-200%+100-500mseasy
http/25-10%+5-10mshard

detection methods

behavioral indicators

  • long-duration websocket connections
  • high data volume over websocket
  • websocket to unusual ports
  • consistent bidirectional traffic
  • connections to non-web services

detection rules

# suricata rule
alert http any any -> any any (
    msg:"websocket tunnel - suspicious upgrade";
    content:"Upgrade|3a 20|websocket";
    flow:to_server,established;
    threshold: type limit, track by_src, count 5, seconds 60;
    sid:1000005;
)

# zeek script
event websocket_established(c: connection, aid: count, resp: response) {
    if (c$duration > 3600)  # connection over 1 hour
        print fmt("long websocket: %s -> %s", c$id$orig_h, c$id$resp_h);
}

traffic analysis

def detect_websocket_tunnel(flows):
    suspicious = []

    for flow in flows:
        if flow['protocol'] == 'websocket':
            # check data patterns
            if flow['bytes'] > 10_000_000:  # >10MB
                suspicious.append(flow)

            # check duration
            if flow['duration'] > 3600:  # >1 hour
                suspicious.append(flow)

            # check packet frequency
            if flow['packets_per_second'] > 100:
                suspicious.append(flow)

    return suspicious

countermeasures

proxy filtering

# nginx websocket restrictions
location /websocket {
    # limit connection duration
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;

    # restrict origins
    if ($http_origin !~ '^https://trusted\.com$') {
        return 403;
    }

    # rate limiting
    limit_req zone=ws_limit burst=10;

    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

content inspection

# websocket frame inspection
def inspect_websocket_frame(frame_data):
    # check for tunneling patterns
    if len(frame_data) > 1000:  # large frames
        entropy = calculate_entropy(frame_data)
        if entropy > 7.5:  # likely encrypted
            return "suspicious"

    # check for protocol signatures
    signatures = [
        b'SSH-2.0',  # ssh protocol
        b'\x05\x01\x00',  # socks5
        b'GET /',  # http in websocket
    ]

    for sig in signatures:
        if sig in frame_data:
            return "tunnel_detected"

    return "clean"

advantages and limitations

advantages

  • standard protocol, widely supported
  • works through proxies
  • tls encryption built-in
  • excellent performance
  • bidirectional communication
  • browser compatibility

limitations

  • requires websocket support
  • initial http handshake visible
  • connection-oriented (tcp only)
  • some firewalls block websocket
  • easier to detect than http polling

real-world usage

legitimate uses

  • real-time web applications
  • chat applications
  • live streaming
  • gaming
  • financial trading platforms

malicious usage

  • 2019: apt groups using websocket c2
  • 2020: cryptominers via websocket
  • 2021: ransomware c2 channels

commercial tools

  • cobalt strike: websocket listeners
  • metasploit: websocket payloads
  • pupy rat: websocket transport

implementation variations

socket.io tunneling

// server
const io = require('socket.io')(3000);
const net = require('net');

io.on('connection', (socket) => {
  const client = net.connect(22, 'localhost');

  socket.on('data', (data) => {
    client.write(data);
  });

  client.on('data', (data) => {
    socket.emit('data', data);
  });
});

// client
const socket = io('http://server:3000');
socket.on('data', (data) => {
  // handle tunneled data
});

golang implementation

// simple websocket tunnel
package main

import (
    "github.com/gorilla/websocket"
    "net"
)

func tunnel(ws *websocket.Conn, target string) {
    tcp, _ := net.Dial("tcp", target)

    // ws -> tcp
    go func() {
        for {
            _, data, _ := ws.ReadMessage()
            tcp.Write(data)
        }
    }()

    // tcp -> ws
    buffer := make([]byte, 4096)
    for {
        n, _ := tcp.Read(buffer)
        ws.WriteMessage(websocket.BinaryMessage, buffer[:n])
    }
}

testing setup

local testing environment

# start websocket tunnel server
wstunnel server ws://0.0.0.0:8080

# setup client tunnel
wstunnel client -L tcp://2222:localhost:22 ws://localhost:8080

# test ssh through tunnel
ssh -p 2222 localhost

# monitor traffic
tcpdump -i lo -w websocket_tunnel.pcap 'port 8080'

performance testing

# iperf through websocket tunnel
# server side
iperf3 -s

# client through tunnel
wstunnel client -L tcp://5201:server:5201 ws://server:8080
iperf3 -c localhost -t 60

# compare with direct connection
iperf3 -c server -t 60

references

on this page