rokdocs
v0.1

3.2. net:: - 네트워크 (TCP / UDP / Unix + TLS 1.3)

rok::std::net 모듈은 저수준 네트워크 primitive 와 함께 TLS 1.3 까지 한 묶음으로 제공합니다. 다만 HTTP, HTTP/2, HTTP/3 같은 고수준 프로토콜은 의도적으로 범위 밖에 두었으며, rok/http 등 별도 패키지로 위임합니다. 이는 표준 라이브러리가 1.0 으로 동결된 뒤 진화가 멈춰버리는 Go 의 전철을 피하기 위한 선택이기도 합니다.

3.2.0. 공통 규칙

  • 쓰는 데이터 (write / write_all / send / send_to 와 그 _async) 는 AsBytes (3.5.1) 로 str, List<u8>, Slice<u8> 를 받습니다. 읽는 buffer 는 List<u8> 입니다. read(buf) 는 미리 길이를 잡아 둔 buf 의 앞 n 바이트를 채우고 n 을 돌려줍니다. 0 은 stream 의 끝입니다.
  • 소켓 타입은 OS 핸들입니다. 값을 복사하면 같은 소켓을 공유하며, try_clone 만 fd 를 복제합니다.
  • 소켓은 Drop 을 구현하므로 using 블록을 벗어날 때 닫힙니다 (2.13.4). 그 밖에는 close() 로 닫습니다. 닫힌 소켓을 쓰면 IoError (InvalidInput) 입니다. 다른 task 가 소켓을 닫을 때 그 소켓에서 대기 중인 *_async 연산은 그 에러로 끝나며, shutdown(Read | Both) 뒤에는 대기 중인 read 가 0 으로 끝납니다.
  • TcpStream 은 Reader + Writer (3.5.1) 를 구현합니다.

blocking 과 *_async

메서드는 두 벌입니다.

형태기다리는 방식
accept, read, write ..호출한 OS 스레드를 막습니다 (task 안이면 그 worker 도)
accept_async, read_async ..task 만 중단하고 worker 는 다른 task 를 실행합니다

*_async 메서드는 async fn 이며 try await s.read_async(buf) 처럼 씁니다. 준비를 기다리는 일은 I/O 드라이버 가 맡습니다. 보통 함수는 stackless 코루틴을 중단시킬 수 없으므로, 같은 이름의 sync 호출이 task 안에서 저절로 중단되지는 않습니다.

blockingasync
TcpListener::acceptTcpListener::accept_async
TcpStream::connectTcpStream::connect_async
TcpStream::readTcpStream::read_async
TcpStream::read_to_endTcpStream::read_to_end_async
TcpStream::writeTcpStream::write_async
TcpStream::write_allTcpStream::write_all_async
TcpStream::write_strTcpStream::write_str_async
UdpSocket::send_toUdpSocket::send_to_async
UdpSocket::recv_fromUdpSocket::recv_from_async
UdpSocket::sendUdpSocket::send_async
UdpSocket::recvUdpSocket::recv_async
tcp_connecttcp_connect_async
tls::Client::new / with_configtls::Client::new_async / with_config_async
tls::Server::newtls::Server::new_async
tls::Client::read ... key_update (Server 도)read_async ... key_update_async

timeout 은 blocking 쪽에만 적용됩니다 (set_read_timeout / set_write_timeout, 넘으면 TimedOut). async 쪽은 task::timeout (task 를 기다림) 이나 task::select (2.18.1) 로 감쌉니다. select 는 진 쪽을 취소하므로 읽기를 포기할 때 쓰기 좋습니다.

enum Got { Data(usize), Timeout }

let (_, got) = await task::select([
    async { Got::Data(try await s.read_async(buf) catch _ { 0 }) },
    async { await task::sleep(Duration::sec(5)); Got::Timeout },
]);

set_nonblocking(true) 이면 blocking 메서드는 기다리지 않고 WouldBlock 을 던집니다.

wasm32-wasi 에서 std::net (TCP / UDP connect, bind, listen, accept, DNS) 은 wasi:sockets 로 동작하며, host 가 네트워크 접근을 허락해야 합니다 (예: wasmtime -S inherit-network -S allow-ip-name-lookup). wasm32-wasi-threads 에서는 TcpListener::inherited() 의 listener 만 동작하고, connect / bind / DNS 는 Unsupported 를 throw 합니다.

3.2.1. TCP

TCP 쪽은 TcpListener 와 TcpStream 두 타입을 축으로 구성합니다. 리스너는 바인드와 accept 흐름을 담당하고, 스트림은 읽기·쓰기와 옵션 제어를 모두 노출합니다.

struct TcpListener { /* OS handle */ }
struct TcpStream { /* OS handle */ }

impl TcpListener {
    fn bind(addr: SocketAddr) throws (IoError) -> TcpListener;      // SO_REUSEADDR, backlog 128
    fn inherited() -> List<TcpListener>;                            // host 가 프로세스에 넘긴 listener
    fn accept(self) throws (IoError) -> (TcpStream, SocketAddr);
    async fn accept_async(self) throws (IoError) -> (TcpStream, SocketAddr);
    fn local_addr(self) throws (IoError) -> SocketAddr;              // port 0 으로 bind 한 뒤 실제 port
    fn try_clone(self) throws (IoError) -> TcpListener;
    fn set_nonblocking(self, nb: bool) throws (IoError);
    fn incoming(self) -> Incoming;       // Iterator<Item = Result<TcpStream, IoError>> 구현
    fn fd(self) -> i32;
    fn close(self);
}

impl TcpStream {
    fn connect(addr: SocketAddr) throws (IoError) -> TcpStream;
    fn connect_timeout(addr: SocketAddr, dur: Duration) throws (IoError) -> TcpStream;
    async fn connect_async(addr: SocketAddr) throws (IoError) -> TcpStream;

    fn read(self, buf: List<u8>) throws (IoError) -> usize;
    fn read_to_end(self) throws (IoError) -> List<u8>;                  // 끝까지
    fn write<B: AsBytes>(self, buf: B) throws (IoError) -> usize;
    fn write_all<B: AsBytes>(self, buf: B) throws (IoError);
    fn write_str(self, s: str) throws (IoError);
    fn flush(self) throws (IoError);                                 // no-op (버퍼 없음)
    // + read_async / read_to_end_async / write_async / write_all_async / write_str_async

    fn peer_addr(self) throws (IoError) -> SocketAddr;
    fn local_addr(self) throws (IoError) -> SocketAddr;
    fn shutdown(self, how: Shutdown) throws (IoError);
    fn try_clone(self) throws (IoError) -> TcpStream;
    fn fd(self) -> i32;
    fn close(self);

    // TCP 옵션
    fn set_nodelay(self, nodelay: bool) throws (IoError);           // TCP_NODELAY
    fn set_keepalive(self, alive: Duration | none) throws (IoError); // SO_KEEPALIVE + idle (초), none = 끔
    fn set_read_timeout(self, dur: Duration | none) throws (IoError);
    fn set_write_timeout(self, dur: Duration | none) throws (IoError);
    fn read_timeout(self) -> Duration | none;
    fn write_timeout(self) -> Duration | none;
    fn set_nonblocking(self, nb: bool) throws (IoError);
}

impl Reader for TcpStream { ... }
impl Writer for TcpStream { ... }
impl Drop for TcpStream { ... }          // close
impl Drop for TcpListener { ... }

enum Shutdown { Read, Write, Both }

TcpListener::inherited() 는 host 가 프로세스에 넘긴 listener 를 돌려줍니다: WASI pre-opened 소켓, Unix 의 systemd LISTEN_FDS socket activation, Windows 에서는 없음.

Incoming 은 끝나지 않습니다. 원소마다 accept 를 한 번 부르며, 그 에러는 Err 원소로 나옵니다. 쓰기에서 peer 가 닫혀도 SIGPIPE 로 프로세스가 끝나지 않고 IoError (BrokenPipe) 가 됩니다.

사용 예

use rok::std::net::{tcp_listen, TcpListener, TcpStream};

// blocking - 이 스레드가 accept 와 handle 에서 기다림
using l = try tcp_listen("127.0.0.1:8080") {
    for c in l.incoming() {
        handle(try c.get());
    }
}

// async - 연결마다 task, 기다리는 동안 worker 는 다른 task 실행
async fn serve_all(l: TcpListener) throws (IoError) {
    loop {
        let (s, peer) = try await l.accept_async();
        task::spawn(serve(s));          // async fn serve(s: TcpStream)
    }
}

3.2.2. UDP

UDP 는 단일 UdpSocket 타입이 모든 역할을 맡습니다. 데이터그램 송수신을 기본으로 하되, connect 로 peer 를 고정해두면 send / recv 만으로도 다룰 수 있도록 했습니다. 멀티캐스트 가입과 TTL 같은 옵션도 함께 노출하며, QUIC 같은 워크로드를 염두에 두고 GSO/GRO 활용을 위한 segment size 제어까지 포함합니다.

struct UdpSocket { /* OS handle */ }

impl UdpSocket {
    fn bind(addr: SocketAddr) throws (IoError) -> UdpSocket;

    fn send_to<B: AsBytes>(self, buf: B, addr: SocketAddr) throws (IoError) -> usize;
    fn recv_from(self, buf: List<u8>) throws (IoError) -> (usize, SocketAddr);
    fn connect(self, addr: SocketAddr) throws (IoError);             // peer 고정
    fn send<B: AsBytes>(self, buf: B) throws (IoError) -> usize;
    fn recv(self, buf: List<u8>) throws (IoError) -> usize;
    // + send_to_async / recv_from_async / send_async / recv_async

    fn local_addr(self) throws (IoError) -> SocketAddr;
    fn peer_addr(self) throws (IoError) -> SocketAddr;
    fn set_broadcast(self, on: bool) throws (IoError);              // SO_BROADCAST
    fn set_read_timeout(self, dur: Duration | none) throws (IoError);
    fn set_write_timeout(self, dur: Duration | none) throws (IoError);
    fn set_nonblocking(self, nb: bool) throws (IoError);
    fn try_clone(self) throws (IoError) -> UdpSocket;
    fn fd(self) -> i32;
    fn close(self);

    // 멀티캐스트 (IPv4)
    fn join_multicast_v4(self, addr: Ipv4Addr, iface: Ipv4Addr) throws (IoError);
    fn leave_multicast_v4(self, addr: Ipv4Addr, iface: Ipv4Addr) throws (IoError);
    fn set_multicast_loop_v4(self, on: bool) throws (IoError);
    fn set_multicast_ttl_v4(self, ttl: u32) throws (IoError);

    // QUIC 용 (GSO / GRO)
    fn set_segment_size(self, size: u16) throws (IoError);           // UDP_SEGMENT, Linux 외에는 Unsupported
}

impl Drop for UdpSocket { ... }          // close

recv_from / recv 는 데이터그램 하나를 buf 에 받습니다. buf 보다 긴 데이터그램의 나머지는 버려집니다.

3.2.3. Unix domain socket

Unix domain socket 은 UnixListener, UnixStream, UnixDatagram 세 타입으로 정리합니다. 전체적인 모양은 TCP 와 닮아 있지만, 같은 호스트 안에서만 통신하므로 파일 경로를 식별자로 쓰며 pair() 처럼 socketpair(2) 에 대응되는 편의 메서드도 제공합니다. 또한 SCM_RIGHTS 를 통한 fd 전달은 Unix 소켓에서만 의미가 있으므로 send_with_fd / recv_with_fd 로 분리해 두었습니다.

struct UnixListener { /* unix only */ }
struct UnixStream { /* unix only */ }
struct UnixDatagram { /* unix only */ }

#[cfg(target_family = "unix")]
impl UnixListener {
    fn bind(path: Path) throws (IoError) -> UnixListener;
    fn accept(self) throws (IoError) -> (UnixStream, Path | none);   // peer 가 bind 한 경로
    // ... TcpListener 와 비슷
}

#[cfg(target_family = "unix")]
impl UnixStream {
    fn connect(path: Path) throws (IoError) -> UnixStream;
    fn pair() throws (IoError) -> (UnixStream, UnixStream);          // socketpair

    // SCM_RIGHTS - fd passing
    fn send_with_fd(self, buf: List<u8>, fd: RawFd) throws (IoError) -> usize;
    fn recv_with_fd(self, buf: List<u8>) throws (IoError) -> (usize, RawFd | none);
}

3.2.4. SocketAddr / IpAddr

주소 표현은 IPv4 와 IPv6 를 합집합으로 묶은 SocketAddr enum 을 중심으로 합니다. 각 변종은 IP 와 포트, 그리고 IPv6 의 경우 flow info 와 scope id 까지 그대로 노출하므로, 필요할 때 직접 들여다볼 수 있습니다. 문자열 파싱은 FromStr 구현을 통해 자연스럽게 이뤄지며, 호스트네임을 다루는 DNS 조회는 별도 함수로 제공합니다.

enum SocketAddr {
    V4(SocketAddrV4),
    V6(SocketAddrV6),
}

struct SocketAddrV4 { ip: Ipv4Addr; port: u16; }
struct SocketAddrV6 { ip: Ipv6Addr; port: u16; flow_info: u32; scope_id: u32; }

impl SocketAddr {
    fn v4(ip: Ipv4Addr, port: u16) -> SocketAddr;
    fn v6(ip: Ipv6Addr, port: u16) -> SocketAddr;    // flow_info = scope_id = 0
    fn port(self) -> u16;
    fn with_port(self, port: u16) -> SocketAddr;
    fn is_ipv4(self) -> bool;
    fn is_ipv6(self) -> bool;
}

struct Ipv4Addr { octets: [u8; 4]; }
struct Ipv6Addr { segments: [u16; 8]; }

impl Ipv4Addr {
    const LOCALHOST: Ipv4Addr = Ipv4Addr { octets: [127, 0, 0, 1] };
    const UNSPECIFIED: Ipv4Addr = Ipv4Addr { octets: [0, 0, 0, 0] };
    fn new(a: u8, b: u8, c: u8, d: u8) -> Ipv4Addr;
    fn to_bits(self) -> u32;
    fn is_loopback(self) -> bool;
    fn is_unspecified(self) -> bool;
    fn is_multicast(self) -> bool;
}

impl Ipv6Addr {
    const LOCALHOST: Ipv6Addr;           // ::1
    const UNSPECIFIED: Ipv6Addr;         // ::
    fn new(a: u16, b: u16, c: u16, d: u16, e: u16, f: u16, g: u16, h: u16) -> Ipv6Addr;
    fn is_loopback(self) -> bool;
}

// 파싱 / 출력 - 세 타입 모두 FromStr (Err = ParseError), Display, Debug, PartialEq
impl FromStr for SocketAddr { ... }       // "127.0.0.1:8080" / "[::1]:8080"
impl FromStr for Ipv4Addr { ... }
impl FromStr for Ipv6Addr { ... }         // "::" 축약 허용

// DNS (getaddrinfo)
fn resolve(host: str) throws (IoError) -> List<SocketAddr>;
fn resolve_one(host: str, port: u16) throws (IoError) -> SocketAddr;

resolve 는 OS 의 getaddrinfo 로 주소 목록을 얻으며, 호출한 스레드를 막습니다. host 는 다음을 받습니다.

host결과
소켓 주소 ("10.0.0.1:80", "[::1]:80")그 주소 하나 (조회 없음)
"name:port"name 의 주소들, port 는 port
"[v6]:port"그 IPv6 주소, port 는 port
이름 또는 IP 만그 주소들, port 0

resolve_one 은 이름과 port 를 따로 받아 첫 주소를 돌려줍니다. 이름을 찾지 못하면 IoError (NotFound) 이며, 메시지에 resolver 의 설명이 들어갑니다.

3.2.5. TLS 1.3 - net::tls

TLS 는 net::tls 하위 모듈에 모아 두었고, 처음부터 1.3 만을 기본 가정으로 잡았습니다. ClientConfig 와 ServerConfig 가 인증서, 암호 모음, ALPN 프로토콜 목록을 표현하며, post-quantum 옵션을 통해 ML-KEM 하이브리드 키 교환을 활성화할 수 있습니다. 핸드셰이크 이후에는 Client / Server 가 평문 스트림처럼 동작하므로 기존 I/O 코드를 거의 그대로 얹을 수 있습니다.

use rok::std::net::tls;

struct ClientConfig {
    root_certs: List<Certificate>;          // 신뢰 앵커
    cipher_suites: List<CipherSuite>;       // TLS 1.3 suite, 선호 순서
    alpn_protocols: List<List<u8>>;         // ["h2", "http/1.1"] 등
    enable_pq: bool;                        // post-quantum (X25519MLKEM768 hybrid)
    client_cert_chain: List<Certificate>;   // 서버가 요청하면 보냄: leaf 먼저; 비면 없음
    client_key: PrivateKey | none;          // 클라이언트 leaf 의 키
    session_cache: SessionCache | none;     // resume 할 ticket; none: resume 안 함
    early_data: List<u8>;                   // 0-RTT 데이터, 아니면 핸드셰이크 뒤의 첫 데이터
}

impl ClientConfig {
    fn new() -> ClientConfig;               // system_roots(), 모든 suite, ALPN 없음, enable_pq = true,
                                            // 클라이언트 인증서 없음, SessionCache::new(), early data 없음
}

struct ServerConfig {
    cert_chain: List<Certificate>;          // leaf 먼저, 그다음 중간 인증서
    private_key: PrivateKey;                // leaf 의 키
    cipher_suites: List<CipherSuite>;
    alpn_protocols: List<List<u8>>;
    client_roots: List<Certificate>;        // 비어 있지 않으면 클라이언트 인증서를 요청하고 이것으로 검증
    require_client_cert: bool;              // client_roots 와 함께: 인증서 없는 클라이언트 거부
    ocsp_response: List<u8>;                // 요청받으면 staple 하는 DER OCSPResponse; 비면 없음
    ticket_store: TicketStore | none;       // resumption; none: ticket 없음
    max_early_data: u32;                    // 연결마다 받는 0-RTT 바이트; 0: 없음
}

impl ServerConfig {
    fn new(cert_chain: List<Certificate>, private_key: PrivateKey) throws (TlsError) -> ServerConfig;
        // 키는 cert_chain[0] 의 것이어야 함; 모든 suite, ALPN 없음, 클라이언트 인증서 없음,
        // OCSP 응답 없음, TicketStore::new(), require_client_cert = true, max_early_data = 0
}

struct SessionCache {                       // 서버 이름별 클라이언트의 ticket; 복사본은 공유
    fn new() -> SessionCache;
    fn len(self) -> usize;
    fn clear(self);
}

struct TicketStore {                        // 서버의 resume 가능한 세션; 복사본은 공유
    fn new() -> TicketStore;
    fn len(self) -> usize;
}

enum CipherSuite { Aes128GcmSha256, Aes256GcmSha384, ChaCha20Poly1305Sha256 }

struct Certificate {
    fn from_der(der: List<u8> | Slice<u8>) throws (TlsError) -> Certificate;
    fn from_pem(text: str) throws (TlsError) -> List<Certificate>;
    fn der(self) -> List<u8>;
    fn dns_names(self) -> List<str>;
    fn not_before(self) -> i64;             // Unix 초
    fn not_after(self) -> i64;
}

struct PrivateKey {                         // Ed25519, ECDSA P-256 / P-384, RSA (2048..4096 bits), RSASSA-PSS
    fn from_der(der: List<u8> | Slice<u8>) throws (TlsError) -> PrivateKey;   // PKCS #8
    fn from_pem(text: str) throws (TlsError) -> PrivateKey;  // PRIVATE KEY, EC PRIVATE KEY 또는 RSA PRIVATE KEY
}

fn system_roots() throws (TlsError) -> List<Certificate>;   // OS trust store (호출마다 읽음)

enum TlsErrorKind { Io, Certificate, Protocol, PeerAlert }
struct TlsError { kind: TlsErrorKind; msg: str; alert: u8; }

struct Client {
    fn new(stream: TcpStream, server_name: str) throws (TlsError) -> Client;
    fn with_config(stream: TcpStream, server_name: str, cfg: ClientConfig) throws (TlsError) -> Client;
    async fn new_async(stream: TcpStream, server_name: str) throws (TlsError) -> Client;
    async fn with_config_async(stream: TcpStream, server_name: str, cfg: ClientConfig) throws (TlsError) -> Client;

    fn read(self, buf: List<u8>) throws (IoError) -> usize;
    fn read_to_end(self) throws (IoError) -> List<u8>;
    fn write<B: AsBytes>(self, buf: B) throws (IoError) -> usize;
    fn write_all<B: AsBytes>(self, buf: B) throws (IoError);
    fn flush(self) throws (IoError);
    fn close_notify(self) throws (IoError);
    fn key_update(self) throws (IoError);
    fn close(self);

    async fn read_async(self, buf: List<u8>) throws (IoError) -> usize;
    async fn read_to_end_async(self) throws (IoError) -> List<u8>;
    async fn write_async<B: AsBytes>(self, buf: B) throws (IoError) -> usize;
    async fn write_all_async<B: AsBytes>(self, buf: B) throws (IoError);
    async fn flush_async(self) throws (IoError);
    async fn close_notify_async(self) throws (IoError);
    async fn key_update_async(self) throws (IoError);

    fn negotiated_alpn(self) -> List<u8> | none;
    fn peer_certificates(self) -> List<Certificate>;
    fn resumed(self) -> bool;               // 핸드셰이크가 세션을 resume 함
    fn early_data_accepted(self) -> bool;   // early_data 가 0-RTT 데이터로 나감
}

struct Server {
    fn new(stream: TcpStream, cfg: ServerConfig) throws (TlsError) -> Server;
    async fn new_async(stream: TcpStream, cfg: ServerConfig) throws (TlsError) -> Server;
    // read, read_to_end, write, write_all, flush, close_notify, key_update,
    // close, 그 *_async 형태, negotiated_alpn, peer_certificates 는
    // Client 와 같고, 추가로:
    fn server_name(self) -> str | none;     // 클라이언트의 SNI
    fn resumed(self) -> bool;
    fn early_data(self) -> List<u8>;        // 받아들인 0-RTT 데이터; read 는 그 뒤의 것을 돌려줌
}

Client 와 Server 는 Reader / Writer (3.5.1) 와 Drop (close_notify 후 socket 을 닫음) 을 구현합니다. 복사본은 하나의 연결을 공유하며, 한 연결은 한 번에 한 thread 만, 한 번에 한 연산만 씁니다 (!Sync).

*_async 메서드는 3.2.0 을 따릅니다: socket 이 준비되지 않은 동안 task 만 중단하며, 핸드셰이크 (new_async, with_config_async) 도 상대의 메시지 사이에서 마찬가지입니다. 한 연결에서 blocking 형태와 async 형태를 섞어 써도 됩니다.

스택은 구현이 제공하며 rok::core::crypto (2.21) 위에 rok 로 작성됩니다. OS 나 서드파티 TLS 라이브러리는 쓰지 않으며, 사용자가 그런 라이브러리를 호출할 일도 없습니다. TLS 1.3 (RFC 8446) 만 사용하고, TLS 1.2 이하는 거부합니다. 핸드셰이크는 X25519 와 X25519MLKEM768 하이브리드 (ML-KEM-768 + X25519, enable_pq), HelloRetryRequest (group 또는 cookie), ALPN, SNI, KeyUpdate, close_notify, 클라이언트 인증서, OCSP stapling, 그리고 0-RTT 데이터를 포함한 resumption 을 지원합니다.

Resumption: ticket_store 가 있는 서버는 핸드셰이크마다 NewSessionTicket 하나를 보냅니다. store 는 세션을 보관하며 (최대 4096 개, 가장 오래된 것부터 버림; 수명 2 시간) ticket 하나를 많아야 한 번 사용합니다. session_cache 가 있는 클라이언트는 받은 ticket 을 접속한 서버 이름 아래에 보관하며 (최대 64 개), 자기도 제시하는 cipher suite 의 해시를 쓰는 살아 있는 ticket 중 가장 새 것을 한 번 제시합니다 (psk_dhe_ke 만: resume 한 핸드셰이크도 새 키 교환을 합니다). resume 한 연결은 인증서 인증을 건너뛰며, peer_certificates() 는 원래 연결의 chain 을 돌려줍니다. ticket 은 클라이언트가 read 하는 동안 받습니다.

0-RTT: early_data 가 비어 있지 않고 ticket 이 그만큼의 바이트를 허용하면, 클라이언트는 그것을 ClientHello 와 함께 early data 로 보냅니다. 그렇지 않거나 서버가 거절하면 핸드셰이크 뒤의 첫 application data 로 보냅니다 (어느 쪽인지는 early_data_accepted() 가 알려 줌). 서버는 max_early_data > 0 이고, 첫 번째로 제시된 ticket 이며, HelloRetryRequest 가 없고, ticket 의 cipher suite 와 ALPN 프로토콜이 같으며, 클라이언트의 ticket age 가 서버의 것과 10 초 안일 때만 early data 를 받습니다. 받은 데이터는 early_data() 로 내주고, 거절한 early data 는 건너뜁니다. 모든 ticket 은 그 TicketStore 에서 한 번만 사용되므로, 재전송된 ClientHello 는 아무것도 resume 하지 못하고 그 early data 는 건너뜁니다 (RFC 8446 8.1). 서로의 재전송을 거부해야 하는 서버들은 store 하나를 공유해야 합니다. early data 는 여전히 ticket 에 대해 forward-secret 이 아니므로 멱등이어야 합니다.

클라이언트 인증서: client_roots 가 비어 있지 않은 서버는 CertificateRequest 를 보냅니다. 클라이언트의 chain 은 서버의 것처럼 (아래) client_roots 에 대해 검증하되 extendedKeyUsage clientAuth 를 요구하고 이름은 검사하지 않으며, 그 CertificateVerify 도 검사합니다. 인증서가 없는 클라이언트는 require_client_cert 이면 certificate_required 로 핸드셰이크가 실패하고, 아니면 peer_certificates() 없이 계속합니다. 클라이언트는 서버가 client_key 에 맞는 scheme 을 받으면 CertificateRequest 에 client_cert_chain 과 client_key 로 만든 CertificateVerify 로 답하고, 아니면 빈 Certificate 로 답합니다. 첫 인증서의 것이 아닌 client_key 는 아무것도 보내기 전에 Client::with_config 를 실패시킵니다.

서버 인증: 인증서 경로는 상대의 chain 에서 root_certs 중 하나까지 구성하고 RFC 5280 에 따라 검증합니다 (서명, 현재 시각 기준 유효 기간, basicConstraints, keyUsage, extendedKeyUsage serverAuth, DNS 및 IP name constraints; 모르는 critical extension 은 실패). 서버 이름은 subjectAltName 하고만 대조합니다 (RFC 6125: 대소문자 무시, wildcard 는 가장 왼쪽 label 전체). IP 리터럴은 iPAddress 항목과 대조하며 SNI 를 보내지 않습니다. 허용하는 서명: Ed25519, SHA-256/384/512 를 쓰는 ECDSA P-256 / P-384, RSA PKCS #1 v1.5 (인증서에만) 와 SHA-256/384/512 를 쓰는 RSA-PSS (2048-4096-bit 키, rsaEncryption 또는 id-RSASSA-PSS; 인증서에서는 salt 길이 무관, id-RSASSA-PSS 키의 해시와 최소 salt 를 따름). Revocation: 클라이언트는 OCSP 응답을 요청합니다 (status_request). staple 된 응답은 성공한 basic OCSP 응답 (RFC 6960) 이어야 하며, leaf 의 발급자나 발급자가 extendedKeyUsage OCSPSigning 으로 발급한 인증서가 서명하고, leaf 를 가리키는 SHA-1 또는 SHA-2 CertID 를 가지며, thisUpdate 가 5 분 넘게 앞서지 않고 nextUpdate 가 있으며 지나지 않아야 합니다. 상태가 revoked 이면 certificate_revoked 로, 그 밖의 문제 (unknown 상태 포함) 는 bad_certificate_status_response 로 실패합니다. status_request 에 대한 TLS feature extension (RFC 7633) 이 있는 leaf 는 staple 된 응답을 요구합니다. staple 도 그 extension 도 없으면 인증서를 받아들입니다. CRL 과 네트워크 OCSP 조회는 쓰지 않습니다.

만료되었거나 아직 유효하지 않은 인증서는 certificate_expired 로, extendedKeyUsage 가 그 역할을 허용하지 않는 인증서는 unsupported_certificate 로, root_certs 까지의 경로가 없는 인증서는 unknown_ca 로, 그 밖의 실패는 bad_certificate 로 실패합니다. ServerHello, HelloRetryRequest, EncryptedExtensions 또는 Certificate 항목에 클라이언트가 요청하지 않은 extension 이 있으면 unsupported_extension 으로 실패합니다. 실패한 핸드셰이크는 그 alert 를 보낸 뒤 stream 을 닫습니다.

양쪽 모두 CertificateVerify 를 키의 scheme 으로 서명합니다: ed25519, ecdsa_secp256r1_sha256, ecdsa_secp384r1_sha384, rsaEncryption 인증서는 상대가 제시한 rsa_pss_rsae_sha256 / _sha384 / _sha512 중 첫 번째, id-RSASSA-PSS 인증서는 상대가 제시한 rsa_pss_pss_sha256 / _sha384 / _sha512 중 키의 매개변수가 허용하는 첫 번째. PrivateKey 는 PKCS #8 (Ed25519, P-256 / P-384 의 id-ecPublicKey, rsaEncryption, id-RSASSA-PSS), RFC 5915 EC PRIVATE KEY, PKCS #1 RSA PRIVATE KEY 를 읽습니다. ServerConfig::new 는 키의 공개 부분이 leaf 의 것이 아니면 실패합니다 (TlsErrorKind::Certificate).

스택은 traffic, handshake, resumption secret, record 키, 키 교환 secret, ticket PSK, Ed25519 / EC 개인 키를 비밀 저장소 (2.21.5) 에 두며, KeyUpdate 때, 핸드셰이크가 끝날 때, close 때 지웁니다.

system_roots() 는 OS trust store 를 읽습니다: macOS keychain 의 anchor (와 무조건 신뢰된 admin / user 인증서), Windows ROOT store, Linux 에서 처음 발견되는 CA bundle (/etc/ssl/certs/ca-certificates.crt, /etc/pki/tls/certs/ca-bundle.crt, ...). SSL_CERT_FILE 이 이를 덮어씁니다. wasm target 에는 store 가 없으므로 root_certs 를 넘겨야 합니다.

read 는 상대의 close_notify 이후 0 을 돌려줍니다. close_notify 없이 끝난 연결은 IoError (UnexpectedEof) 이며, 프로토콜 실패는 fatal alert 를 보낸 뒤 IoError (InvalidData) 입니다. TLS record 는 통째로 쓰이므로 write 는 buf 전체를 씁니다.

사용 예

실제 사용 흐름은 평문 TCP 연결 위에 TLS 클라이언트를 얹고, 그 위에서 평문 I/O 를 호출하는 모양으로 단순합니다.

use rok::std::net::{tcp_connect, tls};

let conn = try tcp_connect("example.com:443");
let tls_conn = try tls::Client::new(conn, "example.com");
try tls_conn.write_all(b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n");
let resp = try tls_conn.read_to_end();
try tls_conn.close_notify();

3.2.6. 편의 함수

자주 쓰는 패턴은 짧은 helper 로 감싸 두었습니다. 문자열을 받는 형태에서는 resolve (3.2.4) 로 DNS 해석까지 수행하므로, tcp_connect("example.com:80") 처럼 호스트네임을 그대로 넘길 수 있습니다.

fn tcp_connect(addr: str | SocketAddr) throws (IoError) -> TcpStream;
async fn tcp_connect_async(addr: str | SocketAddr) throws (IoError) -> TcpStream;
fn tcp_listen(addr: str | SocketAddr) throws (IoError) -> TcpListener;
fn udp_bind(addr: str | SocketAddr) throws (IoError) -> UdpSocket;

tcp_connect 는 해석된 주소를 차례로 시도해 처음 연결되는 것을 돌려주고, 모두 실패하면 마지막 에러를 던집니다. tcp_listen / udp_bind 는 첫 주소에 bind 합니다.

3.2.7. zerocopy hooks (advanced)

처리량이 곧 비용이 되는 자리, 예컨대 프록시나 파일 서버 같은 경우를 위해 zerocopy 계열 hook 도 별도로 노출합니다. 대부분의 사용자에게는 필요하지 않으며, 정말 필요할 때 손에 닿는 곳에 두는 정도가 목적입니다.

impl TcpStream {
    fn sendfile(self, ref file: File, offset: u64, count: u64) throws (IoError) -> u64;
                                            // sendfile(2) / TransmitFile
    fn splice(self, ref src: File, count: u64) throws (IoError) -> u64;
                                            // Linux splice(2)

    fn write_zerocopy(self, buf: List<u8>) throws (IoError) -> usize;
                                            // MSG_ZEROCOPY (Linux 4.14+)
}

3.2.8. 모듈 매핑

플랫폼별 지원 범위는 다음 표에 정리됩니다. 핵심 기능은 데스크톱 OS 와 wasi 양쪽에서 모두 동작하지만, Unix socket 이나 zerocopy hook 처럼 Linux 고유 자원에 의존하는 항목은 제한적으로만 활성화됩니다.

항목linux/macos/windowswasi비고
TCP✓✓wasi:sockets
UDP✓✓wasi:sockets
Unix socketlinux/macos✗Windows 부분 (named pipe 별도)
TLS 1.3✓✓ (trust store 없음: root_certs 전달)rok 자체 구현, rok::core::crypto 위
sendfilelinux✗
splicelinux✗
MSG_ZEROCOPYlinux 4.14+✗

GitHub에서 이 페이지 고치기