This document describes the internal architecture of Wasabi. It is intended for contributors, advanced users, and anyone who wants to understand how the module works under the hood.
Wasabi is a single-file VBA module that implements a complete WebSocket client stack using only native Windows APIs. It does not depend on any external library, COM component, or registered DLL.
The module is organized into five distinct layers, each responsible for one aspect of the communication pipeline.
graph TD
A["Your VBA Code<br/>WebSocketConnect / Send / Receive"]
B["Public API Layer<br/>Handle resolution, validation, routing"]
C["WebSocket Protocol Layer<br/>Frame construction, parsing, masking,<br/>fragmentation, control frame handling"]
D["TLS Layer (Schannel)<br/>Handshake, EncryptMessage, DecryptMessage"]
E["Transport Layer (Winsock)<br/>socket, connect, send, recv, select"]
F["Windows Kernel<br/>TCP/IP stack, network driver"]
A --> B --> C --> D --> E --> F
style A fill:#f9f,stroke:#333,stroke-width:2px
style B fill:#ccf,stroke:#333,stroke-width:2px
style C fill:#cfc,stroke:#333,stroke-width:2px
style D fill:#ffc,stroke:#333,stroke-width:2px
style E fill:#fca,stroke:#333,stroke-width:2px
style F fill:#ddd,stroke:#333,stroke-width:2px
Wasabi manages all connections through a statically allocated pool of 64
WasabiConnection entries. Each entry holds the complete state of one
WebSocket session.
The pool is an array of WasabiConnection user-defined types, initialized
on the first call to any Wasabi function via InitConnectionPool.
graph LR
subgraph Pool["Connection Pool (64 slots)"]
direction LR
A0[0]:::active --- A1[1]:::inactive --- A2[2]:::active --- A3[3]:::inactive --- DOTS[...] --- A62[62]:::inactive --- A63[63]:::active
end
subgraph Legend
L1[● Active]:::active --- L2[○ Available]:::inactive
end
classDef active fill:#2ecc71,stroke:#27ae60,color:#fff
classDef inactive fill:#bdc3c7,stroke:#95a5a6,color:#333
classDef dots fill:none,stroke:none,color:#333
class DOTS dots
style Pool fill:none,stroke:#3498db,stroke-width:2px
style Legend fill:none,stroke:none
When WebSocketConnect is called, AllocConnection scans the pool for the
first slot where Connected = False and Socket = INVALID_SOCKET. It
initializes all fields to their defaults and returns the index as the
connection handle.
When WebSocketDisconnect is called, CleanupHandle closes the socket,
releases TLS resources, and resets all fields in the slot. The slot becomes
available for reuse.
Most public functions accept an optional handle parameter. The internal
function ResolveHandle translates this.
If handle = INVALID_CONN_HANDLE (-1)
→ use m_DefaultHandle
Else
→ use the provided handle directly
[!NOTE] The default handle is updated automatically by
WebSocketConnectto point to the most recently opened connection.
Each WasabiConnection entry contains:
| Category | Fields |
|---|---|
| Socket | Connected, TLS, Host, Port, Path |
| TLS | hCred, hContext, Sizes, hNtlmCred |
| Receive | RecvBuffer(), RecvLen, DecryptBuffer(), DecryptLen |
| Text Queue | MsgQueue(), MsgHead, MsgTail, MsgCount |
| Binary Queue | BinaryQueue(), BinaryHead, BinaryTail, BinaryCount |
| Offline Queue | OfflineQueueEnabled, OfflineTextQueue(), OfflineTextCount, OfflineBinaryQueue(), OfflineBinaryCount |
| Fragmentation | FragmentBuffer(), FragmentLen, FragmentOpcode, Fragmenting |
| Reconnect | AutoReconnect, ReconnectMaxAttempts, ReconnectAttempts, ReconnectBaseDelayMs |
| Proxy | ProxyHost, ProxyPort, ProxyUser, ProxyPass, ProxyEnabled, ProxyType, ProxyUseNtlm |
| Heartbeat | PingIntervalMs, CurrentPingIntervalMs, PingJitterMaxMs, LastPingSentAt, LastPingTimestamp |
| Timeouts | ReceiveTimeoutMs, InactivityTimeoutMs, LastActivityAt |
| Headers | CustomHeaders(), CustomHeaderCount, SubProtocol |
| Statistics | Stats (BytesSent, BytesReceived, MessagesSent, MessagesReceived, ConnectedAt) |
| Diagnostics | LastError, LastErrorCode, TechnicalDetails, LastRttMs |
| Logging | LogCallback, EnableErrorDialog |
| MTU | MTU (CurrentMTU, MaxSegmentSize, OptimalFrameSize, LastProbeTime, ProbeEnabled, UseTLSFragmentation) |
| Security | ValidateServerCert, EnableRevocationCheck, ClientCertThumb, ClientCertPfxPath, ClientCertPfxPass |
| HTTP/2 | UseHttp2 |
| MQTT | MqttParserStage, MqttBuffer(), MqttBufLen, MqttExpectedRemaining, MqttCurrentPacketType, MqttCurrentFlags, MqttInFlight(), MqttInFlightCount, MqttNextPacketId |
| Configuration | NoDelay, CustomBufferSize, CustomFragmentSize, OriginalUrl, AutoMTU, PreferIPv6, ZeroCopyEnabled |
The full connection sequence is handled by the internal ConnectHandle
function. Every connection, including reconnections, passes through this
same path.
graph TD
A[WebSocketConnect]
B["ParseURL<br/>extract host, port, path, scheme"]
C["ResolveAndConnect<br/>getaddrinfo → Happy Eyeballs → TCP connect"]
D["ApplySocketOptions<br/>TCP_NODELAY, SO_KEEPALIVE, buffer sizes"]
E{ProxyEnabled?}
F["DoProxyHTTP or DoProxySOCKS5<br/>(with optional NTLM/Kerberos auth)"]
G{TLS wss://?}
H["AcquireCredentialsHandle<br/>DoTLSHandshake<br/>QueryContextAttributes<br/>ValidateServerCert (opt)<br/>CRL/OCSP check (opt)"]
I["DoWebSocketHandshake<br/>HTTP upgrade + Sec-WebSocket-Accept validation"]
J["Connected = True<br/>Stats reset<br/>Flush Offline Queues (opt)"]
A --> B --> C --> D --> E
E -- YES --> F --> G
E -- NO --> G
G -- YES --> H --> I
G -- NO --> I
I --> J
style A fill:#e1f5fe,stroke:#0277bd
style J fill:#c8e6c9,stroke:#2e7d32
style E fill:#fff9c4,stroke:#f9a825
style G fill:#fff9c4,stroke:#f9a825
The connection phase implements the Happy Eyeballs algorithm for dual-stack hosts. When both IPv6 and IPv4 addresses are resolved:
connect() called immediately.This guarantees the fastest possible connection while preferring IPv6 when both are equally fast.
The TLS layer is implemented through the Windows SSPI Schannel provider. Wasabi performs the entire handshake manually rather than delegating to WinHTTP or any higher-level abstraction.
Before the handshake begins, Wasabi initializes a SCHANNEL_CRED structure
with the following configuration:
| Field | Value | Purpose |
|---|---|---|
dwVersion |
SCHANNEL_CRED_VERSION (4) |
Structure version |
grbitEnabledProtocols |
SP_PROT_TLS1_2_CLIENT \| SP_PROT_TLS1_3_CLIENT |
Accepted TLS versions |
dwFlags |
SCH_CRED_NO_DEFAULT_CREDS |
Do not use Windows credential store |
dwFlags |
SCH_CRED_MANUAL_CRED_VALIDATION |
Skip automatic certificate chain validation |
dwFlags |
SCH_CRED_IGNORE_NO_REVOCATION_CHECK |
Do not fail if CRL is unavailable (unless revocation check is enabled) |
dwFlags |
SCH_CRED_IGNORE_REVOCATION_OFFLINE |
Do not fail if CRL server is unreachable (unless revocation check is enabled) |
This credential is passed to AcquireCredentialsHandle with the package
name "Microsoft Unified Security Protocol Provider".
[!IMPORTANT] Certificate revocation checking (
CRL/OCSP) is configurable viaWebSocketSetRevocationCheck. When disabled (the default), Wasabi passesSCH_CRED_IGNORE_NO_REVOCATION_CHECKandSCH_CRED_IGNORE_REVOCATION_OFFLINEto maximize compatibility with firewalled and offline corporate environments. When enabled, these flags are removed, andCertGetCertificateChainis called withCERT_CHAIN_REVOCATION_CHECK_CHAIN.
The handshake is a multi-round exchange between the client and server.
The internal function DoTLSHandshake implements this as a loop.
Round 1: InitializeSecurityContext (first call, no input)
→ sends ClientHello
→ receives ServerHello + Certificate + ServerHelloDone
Round 2: InitializeSecurityContext (with server response)
→ sends ClientKeyExchange + ChangeCipherSpec + Finished
→ receives server ChangeCipherSpec + Finished
Result: SEC_E_OK → handshake complete
Each round follows this pattern:
InitializeSecurityContext with the accumulated server datasock_sendSEC_I_CONTINUE_NEEDED, read more data from the serverSEC_E_INCOMPLETE_MESSAGE, read more data and retrySECBUFFER_EXTRA is returned, preserve the extra bytes for the next roundSEC_E_OK, the handshake is completeThe loop is protected by a maximum iteration count of 30 to prevent infinite loops on malformed server responses.
Wasabi explicitly does not support server-initiated TLS renegotiation. If
DecryptMessage returns SEC_I_RENEGOTIATE, the connection is closed with
error ERR_TLS_RENEGOTIATE and, if auto-reconnect is enabled, a new
connection is established automatically.
[!NOTE] TLS renegotiation is rarely used in modern servers and its complexity outweighs the benefit in a single-threaded VBA environment. Auto-reconnect serves as the practical recovery mechanism.
After the handshake completes, Wasabi queries the context for stream sizes
using QueryContextAttributes with SECPKG_ATTR_STREAM_SIZES. This returns:
| Field | Purpose |
|---|---|
cbHeader |
Size of the TLS record header (prepended to each encrypted block) |
cbTrailer |
Size of the TLS record trailer (appended to each encrypted block) |
cbMaximumMessage |
Maximum plaintext size per TLS record |
These values are used by TLSSend to correctly frame outgoing data.
When TLSSend is called with plaintext data:
graph TD
subgraph Plaintext["Plaintext Record"]
P1["Header<br/>[cbHeader]"]:::header ---
P2["Plaintext Data"]:::data ---
P3["Trailer<br/>[cbTrailer]"]:::trailer
end
Plaintext --> EM["EncryptMessage"]:::process
subgraph Encrypted["Encrypted Record"]
E1["Header"]:::header ---
E2["Encrypted Data"]:::data ---
E3["Trailer"]:::trailer
end
EM --> Encrypted
Encrypted --> SS["sock_send"]:::send
classDef header fill:#fff9c4,stroke:#f9a825,color:#333
classDef data fill:#e1f5fe,stroke:#0277bd,color:#333
classDef trailer fill:#f3e5f5,stroke:#7b1fa2,color:#333
classDef process fill:#c8e6c9,stroke:#2e7d32,color:#333
classDef send fill:#ffcc80,stroke:#e65100,color:#333
style Plaintext fill:none,stroke:#333,stroke-width:1px
style Encrypted fill:none,stroke:#333,stroke-width:1px
TLSSend automatically splits data larger than cbMaximumMessage into
multiple TLS records, each encrypted separately and sent sequentially.
When TLSDecrypt processes buffered data:
graph TD
A["RecvBuffer<br/>(raw bytes from socket)"]
B["DecryptMessage"]
C["SECBUFFER_DATA<br/>(decrypted)"]
D["SECBUFFER_EXTRA<br/>(next record)"]
E["Append to<br/>DecryptBuffer"]
F["Move to start<br/>of RecvBuffer"]
A --> B
B --> C
B --> D
C --> E
D --> F
style A fill:#e1f5fe,stroke:#0277bd
style B fill:#fff9c4,stroke:#f9a825
style C fill:#c8e6c9,stroke:#2e7d32
style D fill:#f3e5f5,stroke:#7b1fa2
style E fill:#c8e6c9,stroke:#2e7d32
style F fill:#f3e5f5,stroke:#7b1fa2
The SECBUFFER_EXTRA handling is critical. When the OS socket delivers
multiple TLS records in a single recv() call, DecryptMessage only
processes the first complete record and flags the remaining bytes as
SECBUFFER_EXTRA. Wasabi moves these bytes to the beginning of
RecvBuffer and loops to decrypt again.
[!NOTE] If
DecryptMessagereturnsSEC_E_INCOMPLETE_MESSAGE, the currentRecvBufferdoes not contain a complete TLS record. Wasabi exits the decrypt loop and waits for more data to arrive on the next polling cycle.
After the TCP connection (and optional TLS handshake) is established, Wasabi performs the WebSocket protocol upgrade as defined by RFC 6455 Section 4.
Wasabi constructs and sends an HTTP/1.1 GET request:
GET /path HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
The Sec-WebSocket-Key is a Base64-encoded 16-byte random value generated
by GenerateWSKey. Wasabi uses CryptGenRandom (from advapi32.dll) to
obtain cryptographically strong randomness for this key. If CryptGenRandom
were to fail (which should never happen on a normal system), the code falls
back to the VBA Rnd function.
If custom headers, subprotocol, or proxy credentials are configured, they are appended before the final blank line.
Wasabi validates the server response in two steps:
1. Status code check
The response must contain HTTP status 101 (Switching Protocols). Any
other status triggers ERR_HANDSHAKE_REJECTED.
2. Accept key validation
Wasabi computes the expected accept value:
expected = Base64(SHA1(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
This is compared against the Sec-WebSocket-Accept header in the server
response. A mismatch triggers ERR_HANDSHAKE_REJECTED.
The SHA-1 implementation is internal to Wasabi and does not depend on any external library. See the SHA-1 section below.
WebSocket communication happens through frames as defined by RFC 6455 Section 5.
graph LR
subgraph Frame["WebSocket Frame (RFC 6455)"]
direction LR
B0["FIN (1) RSV (3) Opcode (4)"]
B1["MASK (1) Payload len (7)"]
Ext16["Extended len (16)"]
Ext64["Extended len (64)"]
Mask["Masking Key (32)"]
Payload["Payload Data"]
end
B0 --> B1
B1 -->|len=126| Ext16
B1 -->|len=127| Ext64
B1 -->|len<126| Payload
Ext16 --> Payload
Ext64 --> Payload
B1 -->|MASK=1| Mask
Mask --> Payload
style Frame fill:none,stroke:#333,stroke-width:2px
style B0 fill:#e1f5fe,stroke:#0277bd
style B1 fill:#e1f5fe,stroke:#0277bd
style Ext16 fill:#fff9c4,stroke:#f9a825
style Ext64 fill:#fff9c4,stroke:#f9a825
style Mask fill:#f3e5f5,stroke:#7b1fa2
style Payload fill:#c8e6c9,stroke:#2e7d32
When WebSocketSend or WebSocketSendBinary is called:
CryptGenRandom (FillRandomBytes)0x01 for text, 0x02 for binary), and the MASK bit setmask(i Mod 4)RawSendFor (or TLSSend for TLS)[!NOTE] The RSV1 bit (
0x40) is reserved for futurepermessage-deflatesupport.BuildWSFramealready accepts an optionalrsv1parameter for this purpose.
The internal function ProcessFrames parses frames from the
DecryptBuffer:
graph TD
A[DecryptBuffer]
B["Read byte 0<br/>FIN bit + opcode"]
C["Read byte 1<br/>MASK bit + payload length"]
D{"Payload length?"}
E["Use as-is<br/>length < 126"]
F["Read 2 more bytes<br/>length = 126"]
G["Read 8 more bytes<br/>length = 127"]
H{"MASK bit set?"}
I["Read 4-byte mask key"]
J["Extract payload<br/>using CopyMemory"]
K{"Opcode?"}
L["0x00 Continuation<br/>append to fragment buffer"]
M["0x01 Text<br/>UTF-8 decode, enqueue"]
N["0x02 Binary<br/>enqueue raw bytes"]
O["0x08 Close<br/>send close response, disconnect"]
P["0x09 Ping<br/>send pong with same payload"]
Q["0x0A Pong<br/>update RTT measurement"]
A --> B --> C --> D
D -- "<126" --> E --> H
D -- "126" --> F --> H
D -- "127" --> G --> H
H -- YES --> I --> J
H -- NO --> J
J --> K
K -- "0x00" --> L
K -- "0x01" --> M
K -- "0x02" --> N
K -- "0x08" --> O
K -- "0x09" --> P
K -- "0x0A" --> Q
style A fill:#e1f5fe,stroke:#0277bd
style D fill:#fff9c4,stroke:#f9a825
style H fill:#fff9c4,stroke:#f9a825
style K fill:#fff9c4,stroke:#f9a825
style L fill:#f3e5f5,stroke:#7b1fa2
style M fill:#c8e6c9,stroke:#2e7d32
style N fill:#c8e6c9,stroke:#2e7d32
style O fill:#ffcdd2,stroke:#c62828
style P fill:#ffe0b2,stroke:#e65100
style Q fill:#e1f5fe,stroke:#0277bd
Large messages may arrive split across multiple frames. The first frame
has a non-zero opcode and the FIN bit cleared. Continuation frames use
opcode 0x00. The final frame has the FIN bit set.
Frame 1: FIN=0, opcode=0x01 (Text), payload="Hello "
Frame 2: FIN=0, opcode=0x00 (Continuation), payload="from "
Frame 3: FIN=1, opcode=0x00 (Continuation), payload="Wasabi"
Result: "Hello from Wasabi"
Wasabi accumulates fragments in the per-connection FragmentBuffer using
CopyMemory. When the final FIN frame arrives, the complete payload is
assembled and enqueued as a single message.
[!NOTE] The fragment buffer size defaults to 256KB and can be configured via
WebSocketSetBufferSizesbefore connecting. If a fragmented message exceeds this limit, the connection is closed withERR_FRAGMENT_OVERFLOW.
Each connection maintains two independent circular queues (ring buffers): one for text messages and one for binary messages.
graph LR
subgraph Queue["Ring Buffer (512 entries)"]
direction LR
S0[0]:::empty --- S1[1]:::empty --- S2["M1"]:::filled --- S3["M2"]:::filled --- S4["M3"]:::filled --- S5["M4"]:::filled --- S6[6]:::empty --- S7[7]:::empty
end
H["Head"]:::pointer --> S2
T["Tail"]:::pointer --> S6
Info["MsgCount = 4"]
classDef empty fill:#bdc3c7,stroke:#95a5a6,color:#333
classDef filled fill:#c8e6c9,stroke:#2e7d32,color:#333
classDef pointer fill:#fff9c4,stroke:#f9a825,color:#333
style Queue fill:none,stroke:#3498db,stroke-width:2px
style Info fill:none,stroke:none,color:#333
Enqueue (new message arrives):
MsgQueue(MsgTail) = message
MsgTail = (MsgTail + 1) Mod MSG_QUEUE_SIZE
MsgCount = MsgCount + 1
Dequeue (WebSocketReceive called):
result = MsgQueue(MsgHead)
MsgHead = (MsgHead + 1) Mod MSG_QUEUE_SIZE
MsgCount = MsgCount - 1
Both operations are O(1) with no memory allocation or copying beyond the initial array setup.
[!WARNING] When
MsgCountreachesMSG_QUEUE_SIZE(512), new messages are discarded and a warning is logged.
If WebSocketSetOfflineQueueing is enabled, Wasabi utilizes a secondary set of dynamic queues (OfflineTextQueue and OfflineBinaryQueue).
When the connection is in a disconnected state (STATE_CLOSED), calling WebSocketSend or WebSocketSendBinary pushes the message to this secondary queue instead of returning an error. Once the AutoReconnect subsystem successfully re-establishes the connection, FlushOfflineQueues is called automatically to replay all buffered messages in order.
The complete data flow from network to your code.
graph TD
A[Network]
B["sock_recv"]
C["tempBuf"]
D{"Connection type?"}
E["RecvBuffer<br/>↓↓<br/>DecryptMessage<br/>↓↓<br/>DecryptBuffer"]
F["DecryptBuffer<br/>(directly)"]
G["ProcessFrames"]
H{"Frame opcode?"}
I["Text frame<br/>Utf8ToString<br/>MsgQueue enqueue"]
J["Binary frame<br/>BinaryQueue enqueue"]
K["Ping<br/>SendPongFrame<br/>(automatic response)"]
L["Close<br/>WebSocketSendClose<br/>+ disconnect"]
M["Continuation<br/>FragmentBuffer<br/>accumulation"]
N["WebSocketReceive"]
O["Your VBA code"]
A --> B --> C --> D
D -- "TLS" --> E --> G
D -- "Plain" --> F --> G
G --> H
H -- "Text" --> I
H -- "Binary" --> J
H -- "Ping" --> K
H -- "Close" --> L
H -- "Cont." --> M
I --> N
J --> N
K --> N
L --> N
M --> N
N --> O
style A fill:#e1f5fe,stroke:#0277bd
style D fill:#fff9c4,stroke:#f9a825
style H fill:#fff9c4,stroke:#f9a825
style I fill:#c8e6c9,stroke:#2e7d32
style J fill:#c8e6c9,stroke:#2e7d32
style K fill:#ffe0b2,stroke:#e65100
style L fill:#ffcdd2,stroke:#c62828
style M fill:#f3e5f5,stroke:#7b1fa2
style N fill:#e1f5fe,stroke:#0277bd
style O fill:#e1f5fe,stroke:#0277bd
| Buffer | Default Size | Configurable |
|---|---|---|
RecvBuffer |
4096 B (auto-grows) | Yes, via WebSocketSetBufferSizes |
DecryptBuffer |
4096 B (auto-grows) | Yes, via WebSocketSetBufferSizes |
FragmentBuffer |
4096 B (auto-grows) | Yes, via WebSocketSetBufferSizes |
| Text queue | 512 entries | No (compile-time constant) |
| Binary queue | 512 entries | No (compile-time constant) |
When a connection loss is detected during polling and auto-reconnect is enabled, Wasabi executes the following sequence.
graph TD
A["Connection lost detected"]
B["Save all settings<br/>(URL, proxy, headers, subprotocol,<br/>timeouts, callbacks, NoDelay)"]
C["CleanupHandle<br/>(close socket, release TLS, clear buffers)"]
D["Calculate delay<br/>delay = baseDelay * 2^(attempt-1)<br/>cap at MAX_RECONNECT_DELAY_MS (30s)"]
E["Wait<br/>(DoEvents loop)"]
F["Reallocate buffers"]
G["Restore all saved settings"]
H["ConnectHandle(handle, savedUrl)"]
I["Success → ReconnectAttempts = 0"]
K["Flush Offline Queues<br/>(if enabled)"]
J["Failure → increment attempt counter<br/>try again if under max"]
A --> B --> C --> D --> E --> F --> G --> H
H --> I
I --> K
H --> J
style A fill:#ffcdd2,stroke:#c62828
style B fill:#e1f5fe,stroke:#0277bd
style C fill:#e1f5fe,stroke:#0277bd
style D fill:#fff9c4,stroke:#f9a825
style E fill:#fff9c4,stroke:#f9a825
style F fill:#e1f5fe,stroke:#0277bd
style G fill:#e1f5fe,stroke:#0277bd
style H fill:#fff9c4,stroke:#f9a825
style I fill:#c8e6c9,stroke:#2e7d32
style K fill:#c8e6c9,stroke:#2e7d32
style J fill:#ffcdd2,stroke:#c62828
| Attempt | Delay (base = 1000ms) |
|---|---|
| 1 | 1000ms |
| 2 | 2000ms |
| 3 | 4000ms |
| 4 | 8000ms |
| 5 | 16000ms |
| 6+ | 30000ms (capped) |
[!IMPORTANT] The reconnect delay loop uses
DoEvents, which yields to the Windows message pump but does not fully release the VBA thread. The Office UI remains partially responsive during this wait.
Wasabi includes a complete SHA-1 implementation in pure VBA for computing
the Sec-WebSocket-Accept header during the WebSocket handshake, as
required by RFC 6455 Section 4.2.2.
The SHA-1 hash is needed exactly once per connection. Using an external
dependency (such as ScriptControl or a COM hash object) would break
Wasabi’s zero-dependency constraint. The internal implementation ensures
the module remains a single portable .bas file.
VBA’s Long type is a signed 32-bit integer. SHA-1 requires unsigned
32-bit addition and rotation. Wasabi works around this with three helper
functions:
| Function | Purpose |
|---|---|
ADD32(a, b) |
Unsigned 32-bit addition using split high/low halves |
ROTL32(v, n) |
Left rotation by n bits using iterative shift and carry |
U32Shl1(v) |
Single-bit left shift handling the sign bit explicitly |
These functions use bitmasks (&H7FFF, &HFFFF, &H80000000) to
isolate and manipulate individual bit ranges without triggering VBA
overflow errors.
| Round Range | Constant | Hex |
|---|---|---|
| 0 to 19 | 0x5A827999 |
&H5A827999 |
| 20 to 39 | 0x6ED9EBA1 |
&H6ED9EBA1 |
| 40 to 59 | 0x8F1BBCDC |
&H8F1BBCDC |
| 60 to 79 | 0xCA62C1D6 |
&HCA62C1D6 |
h0 = 0x67452301
h1 = 0xEFCDAB89
h2 = 0x98BADCFE
h3 = 0x10325476
h4 = 0xC3D2E1F0
When proxy is enabled, Wasabi establishes an HTTP CONNECT tunnel (or a SOCKS5 tunnel) before performing TLS or WebSocket handshaking. Both proxy types support authentication: HTTP Basic for HTTP proxies and username/password for SOCKS5.
For corporate environments that require integrated Windows authentication,
Wasabi supports NTLM/Kerberos via WebSocketSetProxyNtlm. When enabled
and the proxy returns 407 Proxy Authentication Required with
Proxy-Authenticate: NTLM (or Negotiate), Wasabi performs a full
SSPI NTLM handshake via GenerateNtlmToken. This function uses
AcquireCredentialsHandle with the "NTLM" package and
InitializeSecurityContext to produce a token sent in the
Proxy-Authorization: NTLM <base64> header.
sequenceDiagram
participant C as Client
participant P as Proxy
participant S as Server
C->>+P: CONNECT host:port HTTP/1.1<br/>[Proxy-Authorization: NTLM TlRMT...]
P-->>-C: HTTP/1.1 407 Proxy Authentication Required<br/>Proxy-Authenticate: NTLM <challenge>
C->>+P: CONNECT host:port HTTP/1.1<br/>Proxy-Authorization: NTLM <response>
P->>+S: TCP connect
P-->>-C: HTTP/1.1 200 Connection Established
Note over C,S: TLS Handshake through tunnel
Note over C,S: WebSocket Handshake through tunnel
Note over C,S: WebSocket Frames through tunnel
sequenceDiagram
participant C as Client
participant P as Proxy
participant S as Server
C->>+P: CONNECT host:port HTTP/1.1<br/>Host: host:port<br/>[Proxy-Authorization]
P->>+S: TCP connect
P-->>-C: HTTP/1.1 200 Connection Established
Note over C,S: TLS Handshake through tunnel
Note over C,S: WebSocket Handshake through tunnel
Note over C,S: WebSocket Frames through tunnel
[!NOTE] The proxy only sees the CONNECT request. After the tunnel is established, all subsequent traffic (TLS, WebSocket) passes through opaquely. The proxy cannot inspect the encrypted content.
Wasabi provides round-trip time (RTT) measurement through the
WebSocketGetLatency function. When WebSocketSendPing is called, the
current tick count is stored in LastPingTimestamp. When a Pong frame
is received, ProcessPongForLatency calculates the elapsed time and
stores it in LastRttMs.
sequenceDiagram
participant Client
participant Server
Client->>Server: Ping (opcode 0x09)\nLastPingTimestamp = now
Server-->>Client: Pong (opcode 0x0A)\nLastRttMs = now - timestamp
[!NOTE] RTT values are updated on every Pong frame received, including automatic pongs triggered by the server’s response to periodic pings configured via
WebSocketSetPingInterval.
Wasabi includes a minimal MQTT 3.1.1 client that operates over its existing WebSocket transport. This enables direct integration with IoT brokers (like Mosquitto, HiveMQ, AWS IoT) from any VBA host without additional dependencies.
| Function | MQTT Packet | Description |
|---|---|---|
MqttConnect |
CONNECT | Authenticates and establishes an MQTT session |
MqttPublish |
PUBLISH | Sends a message to a topic with QoS 0, 1, or 2 |
MqttSubscribe |
SUBSCRIBE | Subscribes to one or more topics |
MqttUnsubscribe |
UNSUBSCRIBE | Removes a topic subscription |
MqttDisconnect |
DISCONNECT | Gracefully terminates the MQTT session |
MqttPingReq |
PINGREQ | Sends a keep-alive ping |
MqttReceive |
— | Polls for and parses incoming MQTT packets, and automatically handles QoS 1 & 2 acknowledgments (PUBACK, PUBREC, PUBREL, PUBCOMP) |
The MQTT subsystem reuses Wasabi’s existing WebSocket connection. MQTT
packets are built as binary WebSocket frames and sent via
WebSocketSendBinary. Incoming binary frames are fed byte-by-byte into
MqttParseByte, a state machine that decodes the MQTT fixed header,
remaining length, and variable payload.
The parser state is stored per-connection in MqttParserStage,
MqttBuffer, and related fields.
[!NOTE] The MQTT client supports QoS 0 (at most once), QoS 1 (at least once), and QoS 2 (exactly once) for publish, utilizing an internal In-Flight queue and Packet ID tracking to guarantee delivery without duplication.
Every call to WebSocketReceive triggers an internal maintenance pass
via TickMaintenance. This is the only mechanism for time-based
features because VBA does not support background timers.
| Check | Condition | Action |
|---|---|---|
| Automatic Ping | PingIntervalMs > 0 and CurrentPingIntervalMs elapsed |
Send Ping frame, record timestamp for RTT, calculate next CurrentPingIntervalMs (applying Jitter) |
| Inactivity Timeout | InactivityTimeoutMs > 0 and threshold exceeded |
Close connection, trigger reconnect if enabled |
| MTU Probe | AutoMTU and ProbeEnabled and interval elapsed |
Call ProbeMTU to re-measure MSS |
[!IMPORTANT] If your code stops calling
WebSocketReceive, maintenance also stops. Automatic pings will not be sent and inactivity timeouts will not fire.
Wasabi uses pre-allocated byte arrays instead of dynamic string concatenation to minimize heap fragmentation in long-running sessions.
Per connection memory footprint (default settings):
RecvBuffer: 4 KB (auto-grows up to CustomBufferSize)
DecryptBuffer: 4 KB (auto-grows up to CustomBufferSize)
FragmentBuffer: 4 KB (auto-grows up to CustomFragmentSize)
MsgQueue: 512 × String pointer
BinaryQueue: 512 × Byte array pointer
CustomHeaders: 32 × String pointer
MqttBuffer: 4 KB
Total baseline: ~36 KB + queue overhead per connection
[!NOTE] The actual memory consumed depends on the size of queued messages and dynamically grown buffers. The baseline above represents the fixed allocation before any large messages are received.
Errors in Wasabi propagate through two parallel paths.
graph TD
A[Internal error occurs]
B["Per-connection state<br/>m_Connections(h).LastError<br/>m_Connections(h).LastErrorCode<br/>m_Connections(h).TechnicalDetails"]
C["Global state<br/>m_LastError<br/>m_LastErrorCode<br/>m_TechnicalDetails"]
D["WebSocketGetLastError(h)<br/>(handle-specific)"]
E["WebSocketGetLastError()<br/>(global fallback)"]
A --> B
A --> C
B --> D
C --> E
style A fill:#ffcdd2,stroke:#c62828
style B fill:#e1f5fe,stroke:#0277bd
style C fill:#e1f5fe,stroke:#0277bd
style D fill:#c8e6c9,stroke:#2e7d32
style E fill:#fff9c4,stroke:#f9a825
When a function is called with a valid handle, the per-connection error state is returned. When called without a handle or with an invalid handle, the global error state is returned.
The WasabiError enumeration includes 29 distinct error codes:
| Code | Name | Description |
|---|---|---|
| 0 | ERR_NONE |
No error |
| 1 | ERR_WSA_STARTUP_FAILED |
WSAStartup failed |
| 2 | ERR_SOCKET_CREATE_FAILED |
socket() returned invalid handle |
| 3 | ERR_DNS_RESOLVE_FAILED |
gethostbyname() could not resolve host |
| 4 | ERR_CONNECT_FAILED |
TCP connection could not be established |
| 5 | ERR_TLS_ACQUIRE_CREDS_FAILED |
AcquireCredentialsHandle failed |
| 6 | ERR_TLS_HANDSHAKE_FAILED |
InitializeSecurityContext returned fatal error |
| 7 | ERR_TLS_HANDSHAKE_TIMEOUT |
TLS handshake exceeded iteration limit or timeout |
| 8 | ERR_WEBSOCKET_HANDSHAKE_FAILED |
Could not send or receive HTTP upgrade |
| 9 | ERR_WEBSOCKET_HANDSHAKE_TIMEOUT |
No response to upgrade request |
| 10 | ERR_SEND_FAILED |
send() returned zero or negative |
| 11 | ERR_RECV_FAILED |
recv() returned negative value |
| 12 | ERR_NOT_CONNECTED |
Operation attempted on disconnected handle |
| 13 | ERR_ALREADY_CONNECTED |
Reserved for future use |
| 14 | ERR_TLS_ENCRYPT_FAILED |
EncryptMessage failed |
| 15 | ERR_TLS_DECRYPT_FAILED |
DecryptMessage failed (non-renegotiation) |
| 16 | ERR_INVALID_URL |
URL could not be parsed |
| 17 | ERR_HANDSHAKE_REJECTED |
Server rejected WebSocket upgrade |
| 18 | ERR_CONNECTION_LOST |
Connection dropped during operation |
| 19 | ERR_INVALID_HANDLE |
Handle out of valid range |
| 20 | ERR_MAX_CONNECTIONS |
Pool exhausted (64 connections) |
| 21 | ERR_PROXY_CONNECT_FAILED |
Could not reach proxy |
| 22 | ERR_PROXY_AUTH_FAILED |
Proxy returned 407 (or SOCKS5 auth rejected) |
| 23 | ERR_PROXY_TUNNEL_FAILED |
Proxy rejected CONNECT |
| 24 | ERR_INACTIVITY_TIMEOUT |
No data received within inactivity window |
| 25 | ERR_CERT_LOAD_FAILED |
Client certificate could not be loaded |
| 26 | ERR_CERT_VALIDATE_FAILED |
Server certificate chain validation failed |
| 27 | ERR_FRAGMENT_OVERFLOW |
Fragmented message exceeded buffer size |
| 28 | ERR_TLS_RENEGOTIATE |
Server requested TLS renegotiation |
[!TIP] Always pass the handle when checking errors to get the most specific information.