Getting Started

This guide explains how to get started with io-endpoint, a library that provides a separation of concerns interface for network I/O endpoints.

Installation

Add the gem to your project:

$ bundle add io-endpoint

Core Concepts

io-endpoint provides a unified interface for working with network endpoints, allowing you to write code that is agnostic to the underlying transport mechanism (TCP, UDP, UNIX sockets, SSL/TLS). This separation of concerns makes it easier to:

  • Write transport-agnostic code: Your application logic doesn't need to know whether it's using TCP, UDP, or UNIX sockets.
  • Test with different transports: Easily swap between transports during testing.
  • Handle multiple addresses: Automatically handle IPv4 and IPv6 addresses.
  • Compose endpoints: Combine multiple endpoints for failover or load distribution.

The library centers around the class IO::Endpoint::Generic class, which represents a network endpoint that can be bound (for servers) or connected to (for clients). Different endpoint types handle different scenarios:

Usage

Creating a TCP Server

When you need to create a server that listens on a specific port, you can use IO::Endpoint.tcp to create a TCP endpoint:

require "io/endpoint"

# Create a TCP endpoint listening on localhost port 8080:
endpoint = IO::Endpoint.tcp("localhost", 8080)

# Bind to the endpoint and accept connections:
endpoint.bind do |server|
	# The server socket is automatically closed when the block exits
	server.listen(10)
	
	loop do
		client, address = server.accept
		# Handle the client connection
		client.close
	end
end

Creating a TCP Client

To connect to a remote server, use the connect method:

require "io/endpoint"

# Create a TCP endpoint for the remote server:
endpoint = IO::Endpoint.tcp("example.com", 80)

# Connect to the server:
endpoint.connect do |socket|
	# The socket is automatically closed when the block exits
	socket.write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
	response = socket.read
	puts response
end

Using UNIX Domain Sockets

For inter-process communication on the same machine, UNIX domain sockets provide better performance than TCP:

require "io/endpoint"

# Create a UNIX socket endpoint:
endpoint = IO::Endpoint.unix("/tmp/myapp.sock")

# Bind to the socket:
endpoint.bind do |server|
	server.listen(10)
	
	loop do
		client, address = server.accept
		# Handle the client connection
		client.close
	end
end

Using SSL/TLS

To add encryption to your connections, wrap a TCP endpoint with SSL:

require "io/endpoint"

# Create an SSL endpoint:
endpoint = IO::Endpoint.ssl("example.com", 443, hostname: "example.com")

# Connect with SSL encryption:
endpoint.connect do |socket|
	# The socket is automatically encrypted
	socket.write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
	response = socket.read
	puts response
end

Configuring TLS Certificate Material

Use class IO::Endpoint::TLS::Configuration to provide certificate and private key material without coupling application configuration to OpenSSL. Certificate material is supplied as PEM content rather than file paths, so it can be loaded from files, environment variables, or secret stores:

trust_store = IO::Endpoint::TLS::TrustStore.parse(root_certificates)
certificate_chain = IO::Endpoint::TLS::Certificates.parse(certificate_chain_bundle)

tls_configuration = IO::Endpoint::TLS::Configuration.new(
	trust_store: trust_store,
	certificate_chain: certificate_chain,
	private_key: private_key,
)

endpoint = IO::Endpoint.ssl(
	"example.com",
	443,
	hostname: "example.com",
	tls_configuration: tls_configuration,
)

Use TrustStore.new(certificates: certificates) when certificates are already represented as an array of individual PEM strings. Use TrustStore.parse(certificate_bundle) to split a concatenated PEM bundle into that canonical representation, or TrustStore.load("/path/to/certificates.pem") to load and parse a bundle from a file. Options such as system_certificates: true are forwarded from load to parse.

The local certificate chain is an ordered array of individual PEM strings, with the leaf certificate followed by any intermediate certificates. The root certificate is normally omitted. Use Certificates.parse(certificate_bundle) to split a concatenated bundle while preserving that order.

Set system_certificates: true to include system-provided trusted certificates. System and custom certificates can be combined in the same trust store.

The SSL endpoint converts the trust store into an OpenSSL certificate store and the complete configuration into an OpenSSL context. Other endpoint implementations can consume the same trust, identity, and verification configuration using their native TLS implementation.

The OpenSSL conversion is also available directly using IO::Endpoint::TLS::OpenSSL.build_certificate_store(trust_store).

For a server which requires clients to provide a trusted certificate, set verification: :required. Use :peer to verify a certificate when the peer provides one, and :none to explicitly disable peer verification. When a trust store is supplied and no policy is specified, peer verification is enabled by default. On client connections with a hostname, peer verification also checks that the certificate identifies that hostname.