Add tls obfs outbound

This commit is contained in:
bdbai
2023-03-27 19:51:16 +08:00
committed by eycorsican
parent 85cfa4ce69
commit 8ad2ad1836
8 changed files with 548 additions and 69 deletions

View File

@@ -192,10 +192,10 @@ impl OutboundManager {
"http" => Box::new(obfs::HttpObfsStreamHandler::new(
settings.path.as_bytes(),
settings.host.as_bytes(),
)),
"tls" => unimplemented!(
"Box::new(obfs::TlsObfsStreamHandler::new(settings.host))"
),
)) as _,
"tls" => {
Box::new(obfs::TlsObfsStreamHandler::new(settings.host.as_bytes())) as _
}
method => {
return Err(anyhow!(
"invalid [{}] outbound settings: unknown obfs method {}",

View File

@@ -1,5 +1,5 @@
// This file is generated by rust-protobuf 3.2.0. Do not edit
// .proto file is parsed by protoc 3.21.12
// .proto file is parsed by protoc 22.2
// @generated
// https://github.com/rust-lang/rust-clippy/issues/702

View File

@@ -1,5 +1,5 @@
// This file is generated by rust-protobuf 3.2.0. Do not edit
// .proto file is parsed by protoc 3.21.12
// .proto file is parsed by protoc 22.2
// @generated
// https://github.com/rust-lang/rust-clippy/issues/702

View File

@@ -7,6 +7,7 @@ use base64::prelude::*;
use memchr::memmem;
use rand::{thread_rng, RngCore};
use tokio::io::ReadBuf;
use tokio_util::io::poll_write_buf;
use crate::proxy::*;
@@ -16,17 +17,22 @@ pub struct Handler {
req_line: Arc<[u8]>,
}
enum StreamState {
Initial { req_line: Arc<[u8]> },
WritingRequest { req: Cursor<Vec<u8>> },
enum ReadState {
AwaitingResponse { res_buf: Vec<u8> },
ConsumingResponse { res: Cursor<Vec<u8>> },
Transfer,
}
enum WriteState {
Initial { req_line: Arc<[u8]> },
WritingRequest(Cursor<Vec<u8>>),
Transfer,
}
struct Stream {
stream: AnyStream,
state: StreamState,
read_state: ReadState,
write_state: WriteState,
}
impl Handler {
@@ -61,16 +67,21 @@ impl OutboundStreamHandler for Handler {
_sess: &'a Session,
stream: Option<AnyStream>,
) -> io::Result<AnyStream> {
let Some(stream) = stream else {
return Err(io::Error::new(io::ErrorKind::Other, "invalid tls input"));
};
let stream = stream.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "invalid input"))?;
Ok(Box::new(Stream {
Ok(Box::new(Stream::new(self.req_line.clone(), stream)))
}
}
impl Stream {
fn new(req_line: Arc<[u8]>, stream: AnyStream) -> Self {
Self {
stream,
state: StreamState::Initial {
req_line: self.req_line.clone(),
read_state: ReadState::AwaitingResponse {
res_buf: Vec::with_capacity(RESPONSE_BUFFER_SIZE),
},
}))
write_state: WriteState::Initial { req_line },
}
}
}
@@ -81,23 +92,11 @@ impl AsyncRead for Stream {
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
loop {
let Self { state, stream } = &mut *self;
match state {
StreamState::Initial { req_line } => {
// Client expects a response before sending a request.
// Generate a request with content length 0.
let req = generate_http_request(req_line, &[]);
*state = StreamState::WritingRequest {
req: Cursor::new(req),
};
continue;
}
StreamState::WritingRequest { .. } => {
// Finish writing the request as much as possible.
ready!(self.as_mut().poll_flush(cx))?;
continue;
}
StreamState::AwaitingResponse { res_buf } => {
let Self {
read_state, stream, ..
} = &mut *self;
match read_state {
ReadState::AwaitingResponse { res_buf } => {
if res_buf.len() >= RESPONSE_BUFFER_SIZE {
// The response may be too large. This should not happen in obfs.
return Poll::Ready(Err(io::Error::new(
@@ -105,7 +104,8 @@ impl AsyncRead for Stream {
"obfs response too large",
)));
}
let read_len = ready!(tokio_util::io::poll_read_buf(Pin::new(stream), cx, res_buf))?;
let read_len =
ready!(tokio_util::io::poll_read_buf(Pin::new(stream), cx, res_buf))?;
if read_len == 0 {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::Other,
@@ -118,20 +118,19 @@ impl AsyncRead for Stream {
};
let mut payload = Cursor::new(std::mem::take(res_buf));
payload.set_position(req_body_pos as u64);
*state = StreamState::ConsumingResponse { res: payload };
continue;
*read_state = ReadState::ConsumingResponse { res: payload };
}
StreamState::ConsumingResponse { res } => {
ReadState::ConsumingResponse { res } => {
let remaining = &res.get_ref()[res.position() as usize..];
let to_write = remaining.len().min(buf.remaining());
buf.put_slice(&remaining[..to_write]);
res.set_position(res.position() + to_write as u64);
if res.position() as usize == res.get_ref().len() {
*state = StreamState::Transfer;
*read_state = ReadState::Transfer;
}
return Poll::Ready(Ok(()));
}
StreamState::Transfer => return Pin::new(stream).poll_read(cx, buf),
ReadState::Transfer => return Pin::new(stream).poll_read(cx, buf),
};
}
}
@@ -143,53 +142,47 @@ impl AsyncWrite for Stream {
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
let Self { state, stream } = &mut *self;
let Self {
write_state,
stream,
..
} = &mut *self;
loop {
match state {
StreamState::Initial { req_line } => {
match write_state {
WriteState::Initial { req_line } => {
let req = generate_http_request(req_line, buf);
*state = StreamState::WritingRequest {
req: Cursor::new(req),
};
continue;
*write_state = WriteState::WritingRequest(Cursor::new(req));
}
StreamState::WritingRequest { req } => {
ready!(tokio_util::io::poll_write_buf(Pin::new(stream), cx, req))?;
WriteState::WritingRequest(req) => {
ready!(poll_write_buf(Pin::new(stream), cx, req))?;
if req.position() as usize == req.get_ref().len() {
*state = StreamState::AwaitingResponse {
res_buf: Vec::with_capacity(RESPONSE_BUFFER_SIZE),
};
*write_state = WriteState::Transfer;
return Poll::Ready(Ok(buf.len()));
}
continue;
}
StreamState::AwaitingResponse { .. } => break,
StreamState::ConsumingResponse { .. } => break,
StreamState::Transfer => break,
WriteState::Transfer => break,
}
}
Pin::new(&mut *stream).poll_write(cx, buf)
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
let Self { state, stream } = &mut *self;
let Self {
write_state,
stream,
..
} = &mut *self;
loop {
match state {
StreamState::Initial { .. } => return Poll::Ready(Ok(())),
StreamState::WritingRequest { req } => {
ready!(tokio_util::io::poll_write_buf(Pin::new(stream), cx, req))?;
match write_state {
WriteState::Initial { .. } => return Poll::Ready(Ok(())),
WriteState::WritingRequest(req) => {
ready!(poll_write_buf(Pin::new(stream), cx, req))?;
if req.position() as usize == req.get_ref().len() {
*state = StreamState::AwaitingResponse {
res_buf: Vec::with_capacity(RESPONSE_BUFFER_SIZE),
};
*write_state = WriteState::Transfer;
return Poll::Ready(Ok(()));
}
continue;
}
StreamState::AwaitingResponse { .. } => break,
StreamState::ConsumingResponse { .. } => break,
StreamState::Transfer => break,
WriteState::Transfer => break,
}
}
Pin::new(&mut *stream).poll_flush(cx)

View File

@@ -1,3 +1,5 @@
pub mod http;
pub mod tls;
pub use self::http::Handler as HttpObfsStreamHandler;
pub use self::tls::Handler as TlsObfsStreamHandler;

327
leaf/src/proxy/obfs/tls.rs Normal file
View File

@@ -0,0 +1,327 @@
use std::io::{Cursor, IoSlice};
use std::mem::MaybeUninit;
use std::pin::Pin;
use std::task::{ready, Context, Poll};
use async_trait::async_trait;
use rand::{thread_rng, RngCore};
use tokio::io::ReadBuf;
use tokio_util::io::poll_write_buf;
use crate::proxy::*;
mod packet;
mod template;
const RESPONSE_HANDSHAKE_SIZE: usize = 96 /* server hello */
+ 6 /* change cipher spec */ + 3 /* encrypted handshake */ ;
const LEN_HEADER_BUFFER_SIZE: usize = 3;
const LEN_BUFFER_SIZE: usize = 5;
const MAX_TLS_CHUNK_SIZE: u16 = 16 * 1024;
pub struct Handler {
host: Arc<[u8]>,
}
enum ReadState {
AwaitingResponse { remaining_read_len: usize },
HeaderIncomplete(Cursor<[u8; LEN_BUFFER_SIZE]>),
HeaderComplete { chunk_remaining: usize },
}
enum WriteState {
Initial {
host: Arc<[u8]>,
},
WritingRequest(Cursor<Vec<u8>>),
WritingHeader {
payload_len: u16,
write_offset: usize,
},
WritingPayload {
chunk_remaining: usize,
},
}
struct Stream {
stream: AnyStream,
read_state: ReadState,
write_state: WriteState,
}
impl Handler {
pub fn new(host: &[u8]) -> Self {
Self { host: host.into() }
}
}
#[async_trait]
impl OutboundStreamHandler for Handler {
fn connect_addr(&self) -> OutboundConnect {
OutboundConnect::Next
}
async fn handle<'a>(
&'a self,
_sess: &'a Session,
stream: Option<AnyStream>,
) -> io::Result<AnyStream> {
let stream = stream.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "invalid input"))?;
Ok(Box::new(Stream::new(self.host.clone(), stream)))
}
}
impl ReadState {
fn merge_chunks(&mut self, data: &mut [u8]) -> usize {
let mut write_index = 0;
let mut read_index = 0;
while read_index < data.len() {
let mut part = &mut data[read_index..];
match self {
ReadState::AwaitingResponse { remaining_read_len } => {
let len = part.len().min(*remaining_read_len);
read_index += len;
*remaining_read_len -= len;
if *remaining_read_len == 0 {
let mut cursor = Cursor::new([0; LEN_BUFFER_SIZE]);
cursor.set_position(LEN_HEADER_BUFFER_SIZE as u64);
*self = ReadState::HeaderIncomplete(cursor);
}
}
ReadState::HeaderIncomplete(header_buf) => {
let pos = header_buf.position() as usize;
let len = part.len().min(LEN_BUFFER_SIZE - pos);
part = &mut part[..len];
read_index += len;
header_buf.get_mut()[pos..][..len].copy_from_slice(part);
header_buf.set_position(pos as u64 + len as u64);
if header_buf.position() as usize == LEN_BUFFER_SIZE {
let len = u16::from_be_bytes(
header_buf.get_ref()[LEN_HEADER_BUFFER_SIZE..]
.try_into()
.expect("obfs tls packet len size != 2"),
) as usize;
*self = ReadState::HeaderComplete {
chunk_remaining: len,
};
}
}
ReadState::HeaderComplete { chunk_remaining } => {
let len = part.len().min(*chunk_remaining);
*chunk_remaining -= len;
data.copy_within(read_index..(read_index + len), write_index);
write_index += len;
read_index += len;
if *chunk_remaining == 0 {
*self = ReadState::HeaderIncomplete(Default::default());
}
}
}
}
write_index
}
}
impl Stream {
fn new(host: Arc<[u8]>, stream: AnyStream) -> Self {
Self {
stream,
read_state: ReadState::AwaitingResponse {
remaining_read_len: RESPONSE_HANDSHAKE_SIZE,
},
write_state: WriteState::Initial { host },
}
}
}
impl AsyncRead for Stream {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
let mut total_read_len = 0;
let mut new_inited = 0;
while total_read_len == 0 {
let Self {
read_state, stream, ..
} = &mut *self;
total_read_len = if let ReadState::AwaitingResponse { remaining_read_len } = read_state
{
let mut header_buf = [MaybeUninit::<u8>::uninit(); RESPONSE_HANDSHAKE_SIZE];
let mut read_buf = ReadBuf::uninit(&mut header_buf[..*remaining_read_len]);
ready!(Pin::new(stream).poll_read(cx, &mut read_buf))?;
if read_buf.filled().is_empty() {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"obfs tls server hello eof",
)));
}
read_state.merge_chunks(read_buf.filled_mut()); // Data should not contain any chunks.
continue;
} else {
// Safety: the uninitialized part is managed by the new ReadBuf.
let mut read_buf = ReadBuf::uninit(unsafe { buf.unfilled_mut() });
ready!(Pin::new(stream).poll_read(cx, &mut read_buf))?;
if read_buf.filled().is_empty() {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"obfs tls eof",
)));
}
new_inited = new_inited.max(read_buf.initialized().len());
read_state.merge_chunks(read_buf.filled_mut())
};
}
// Safety: new_inited bytes is initialized by an inner read_buf.
unsafe {
buf.assume_init(new_inited);
}
buf.advance(total_read_len);
Poll::Ready(Ok(()))
}
}
impl AsyncWrite for Stream {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
mut buf: &[u8],
) -> Poll<io::Result<usize>> {
let mut payload_written = 0;
while payload_written == 0 {
let Self {
stream,
write_state,
..
} = &mut *self;
match write_state {
WriteState::Initial { host } => {
buf = &buf[..buf.len().min(MAX_TLS_CHUNK_SIZE as usize)];
let req = generate_tls_request(&host, buf);
*write_state = WriteState::WritingRequest(Cursor::new(req));
}
WriteState::WritingRequest(req) => {
ready!(poll_write_buf(Pin::new(stream), cx, req))?;
if req.position() as usize == req.get_ref().len() {
*write_state = WriteState::WritingPayload { chunk_remaining: 0 };
return Poll::Ready(Ok(buf.len()));
}
}
WriteState::WritingHeader {
payload_len,
write_offset,
} => {
let header = generate_header(*payload_len);
let buf_len = buf.len().min(*payload_len as usize);
let iov = [
IoSlice::new(&header[*write_offset..]),
IoSlice::new(&buf[..buf_len]),
];
let write_len = ready!(Pin::new(stream).poll_write_vectored(cx, &iov))?;
*write_offset += write_len;
if let Some(chunk_written) = write_offset.checked_sub(LEN_BUFFER_SIZE) {
*write_state = WriteState::WritingPayload {
chunk_remaining: *payload_len as usize - chunk_written,
};
payload_written += chunk_written;
buf = &buf[chunk_written..];
}
}
WriteState::WritingPayload { chunk_remaining: 0 } => {
*write_state = WriteState::WritingHeader {
payload_len: buf.len().try_into().unwrap_or(MAX_TLS_CHUNK_SIZE),
write_offset: 0,
};
}
WriteState::WritingPayload { chunk_remaining } => {
let buf_len = buf.len().min(*chunk_remaining);
let write_len = ready!(Pin::new(stream).poll_write(cx, &buf[..buf_len]))?;
*chunk_remaining -= write_len;
payload_written += write_len;
buf = &buf[write_len..];
}
}
}
Poll::Ready(Ok(payload_written))
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
let Self {
stream,
write_state,
..
} = &mut *self;
loop {
match write_state {
WriteState::Initial { .. } => return Poll::Ready(Ok(())),
WriteState::WritingRequest(req) => {
ready!(poll_write_buf(Pin::new(stream), cx, req))?;
if req.position() as usize == req.get_ref().len() {
*write_state = WriteState::WritingPayload { chunk_remaining: 0 };
return Poll::Ready(Ok(()));
}
}
_ => break,
}
}
Pin::new(&mut *stream).poll_flush(cx)
}
fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Pin::new(&mut self.stream).poll_shutdown(cx)
}
}
fn generate_tls_request(host: &[u8], payload: &[u8]) -> Vec<u8> {
use std::mem::{size_of_val, transmute};
use std::time::SystemTime;
let mut rng = thread_rng();
let mut hello = template::CLIENT_HELLO;
let mut server_name = template::EXT_SERVER_NAME;
let mut ticket = template::EXT_SESSION_TICKET;
let other = template::EXT_OTHERS;
let total_len = payload.len()
+ size_of_val(&hello)
+ size_of_val(&server_name)
+ host.len()
+ size_of_val(&ticket)
+ size_of_val(&other);
hello.0.len = (total_len as u16 - 5).to_be();
hello.0.handshake_len_2 = (total_len as u16 - 9).to_be();
hello.0.random_unix_time = (SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as u32)
.to_be();
rng.fill_bytes(&mut hello.0.random_bytes);
rng.fill_bytes(&mut hello.0.session_id);
hello.0.ext_len = ((total_len - size_of_val(&hello)) as u16).to_be();
ticket.0.session_ticket_ext_len = (payload.len() as u16).to_be();
server_name.0.ext_len = (host.len() as u16 + 3 + 2).to_be();
server_name.0.server_name_list_len = (host.len() as u16 + 3).to_be();
server_name.0.server_name_len = host.len() as u16;
let mut req = Vec::with_capacity(total_len);
unsafe {
req.extend_from_slice(&transmute::<_, [u8; 138]>(hello));
req.extend_from_slice(&transmute::<_, [u8; 4]>(ticket));
req.extend_from_slice(payload);
req.extend_from_slice(&transmute::<_, [u8; 9]>(server_name));
req.extend_from_slice(host);
req.extend_from_slice(&transmute::<_, [u8; 66]>(other));
}
req
}
fn generate_header(payload_len: u16) -> [u8; LEN_BUFFER_SIZE] {
let mut tls_data_header = [
0x17, 0x03, 0x03, /* 2 bytes of len goes here */ 0x00, 0x00,
];
tls_data_header[3] = payload_len.to_be_bytes()[0];
tls_data_header[4] = payload_len.to_be_bytes()[1];
tls_data_header
}

View File

@@ -0,0 +1,82 @@
#[derive(Copy, Clone)]
#[repr(C, align(1))]
pub struct tls_client_hello(pub tls_client_hello_Inner);
#[derive(Copy, Clone)]
#[repr(C, packed)]
pub struct tls_client_hello_Inner {
pub content_type: u8,
pub version: u16,
pub len: u16,
pub handshake_type: u8,
pub handshake_len_1: u8,
pub handshake_len_2: u16,
pub handshake_version: u16,
pub random_unix_time: u32,
pub random_bytes: [u8; 28],
pub session_id_len: u8,
pub session_id: [u8; 32],
pub cipher_suites_len: u16,
pub cipher_suites: [u8; 56],
pub comp_methods_len: u8,
pub comp_methods: [u8; 1],
pub ext_len: u16,
}
#[allow(dead_code, non_upper_case_globals)]
const tls_client_hello_PADDING: usize =
::std::mem::size_of::<tls_client_hello>() - ::std::mem::size_of::<tls_client_hello_Inner>();
#[derive(Copy, Clone)]
#[repr(C, align(1))]
pub struct tls_ext_server_name(pub tls_ext_server_name_Inner);
#[derive(Copy, Clone)]
#[repr(C, packed)]
pub struct tls_ext_server_name_Inner {
pub ext_type: u16,
pub ext_len: u16,
pub server_name_list_len: u16,
pub server_name_type: u8,
pub server_name_len: u16,
}
#[allow(dead_code, non_upper_case_globals)]
const tls_ext_server_name_PADDING: usize = ::std::mem::size_of::<tls_ext_server_name>()
- ::std::mem::size_of::<tls_ext_server_name_Inner>();
#[derive(Copy, Clone)]
#[repr(C, align(1))]
pub struct tls_ext_session_ticket(pub tls_ext_session_ticket_Inner);
#[derive(Copy, Clone)]
#[repr(C, packed)]
pub struct tls_ext_session_ticket_Inner {
pub session_ticket_type: u16,
pub session_ticket_ext_len: u16,
}
#[allow(dead_code, non_upper_case_globals)]
const tls_ext_session_ticket_PADDING: usize = ::std::mem::size_of::<tls_ext_session_ticket>()
- ::std::mem::size_of::<tls_ext_session_ticket_Inner>();
#[derive(Copy, Clone)]
#[repr(C, align(1))]
pub struct tls_ext_others(pub tls_ext_others_Inner);
#[derive(Copy, Clone)]
#[repr(C, packed)]
pub struct tls_ext_others_Inner {
pub ec_point_formats_ext_type: u16,
pub ec_point_formats_ext_len: u16,
pub ec_point_formats_len: u8,
pub ec_point_formats: [u8; 3],
pub elliptic_curves_type: u16,
pub elliptic_curves_ext_len: u16,
pub elliptic_curves_len: u16,
pub elliptic_curves: [u8; 8],
pub sig_algos_type: u16,
pub sig_algos_ext_len: u16,
pub sig_algos_len: u16,
pub sig_algos: [u8; 30],
pub encrypt_then_mac_type: u16,
pub encrypt_then_mac_ext_len: u16,
pub extended_master_secret_type: u16,
pub extended_master_secret_ext_len: u16,
}
#[allow(dead_code, non_upper_case_globals)]
const tls_ext_others_PADDING: usize =
::std::mem::size_of::<tls_ext_others>() - ::std::mem::size_of::<tls_ext_others_Inner>();

View File

@@ -0,0 +1,75 @@
use super::packet;
pub const CLIENT_HELLO: packet::tls_client_hello =
packet::tls_client_hello(packet::tls_client_hello_Inner {
content_type: 0x16,
version: 0x0301u16.to_be(),
len: 0,
handshake_type: 1,
handshake_len_1: 0,
handshake_len_2: 0,
handshake_version: 0x0303u16.to_be(),
random_unix_time: 0,
random_bytes: [0; 28],
session_id_len: 32,
session_id: [0; 32],
cipher_suites_len: 56u16.to_be(),
cipher_suites: [
0xc0, 0x2c, 0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0xaa, 0xc0, 0x2b,
0xc0, 0x2f, 0x00, 0x9e, 0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23, 0xc0, 0x27,
0x00, 0x67, 0xc0, 0x0a, 0xc0, 0x14, 0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33,
0x00, 0x9d, 0x00, 0x9c, 0x00, 0x3d, 0x00, 0x3c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0xff,
],
comp_methods_len: 1,
comp_methods: [0],
ext_len: 0,
});
pub const EXT_SERVER_NAME: packet::tls_ext_server_name =
packet::tls_ext_server_name(packet::tls_ext_server_name_Inner {
ext_type: 0,
ext_len: 0,
server_name_list_len: 0,
server_name_type: 0,
server_name_len: 0,
});
pub const EXT_SESSION_TICKET: packet::tls_ext_session_ticket =
packet::tls_ext_session_ticket(packet::tls_ext_session_ticket_Inner {
session_ticket_type: 0x0023u16.to_be(),
session_ticket_ext_len: 0,
});
pub const EXT_OTHERS: packet::tls_ext_others =
packet::tls_ext_others(packet::tls_ext_others_Inner {
ec_point_formats_ext_type: 0x000Bu16.to_be(),
ec_point_formats_ext_len: 4u16.to_be(),
ec_point_formats_len: 3,
ec_point_formats: [0x01, 0x00, 0x02],
elliptic_curves_type: 0x000au16.to_be(),
elliptic_curves_ext_len: 10u16.to_be(),
elliptic_curves_len: 8u16.to_be(),
elliptic_curves: [0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, 0x00, 0x18],
sig_algos_type: 0x000du16.to_be(),
sig_algos_ext_len: 32u16.to_be(),
sig_algos_len: 30u16.to_be(),
sig_algos: [
0x06, 0x01, 0x06, 0x02, 0x06, 0x03, 0x05, 0x01, 0x05, 0x02, 0x05, 0x03, 0x04, 0x01,
0x04, 0x02, 0x04, 0x03, 0x03, 0x01, 0x03, 0x02, 0x03, 0x03, 0x02, 0x01, 0x02, 0x02,
0x02, 0x03,
],
encrypt_then_mac_type: 0x0016u16.to_be(),
encrypt_then_mac_ext_len: 0,
extended_master_secret_type: 0x0017u16.to_be(),
extended_master_secret_ext_len: 0,
});