Abyss
Abyss is a modern, pure Elixir UDP server library that provides a high-performance foundation for building UDP-based services like DNS servers, DHCP servers, or custom UDP applications. It implements a supervisor-based architecture with connection pooling and pluggable transport modules.
Features
- High Performance: Supervisor-based architecture with configurable connection pooling
- Flexible Handler System: Pluggable handler modules for custom protocol implementations
- Real-time Metrics: Built-in telemetry with connection counts, throughput rates, and response times
- Security Features: Configurable packet size validation
- Broadcast Support: Native support for broadcast and multicast applications
- Persistent Dispatching: Optional bounded dispatcher for stateful datagram protocols
- Graceful Shutdown: Coordinated shutdown with configurable timeouts
- Extensible Transport: Pluggable transport layer (currently UDP)
Installation
The package can be installed by adding abyss to your list of dependencies in mix.exs:
def deps do
[
{:abyss, "~> 0.7.0"}
]
end
UDP lifecycle, interface selection, runtime membership control, client limits,
and tested platform restrictions are documented in
UDP broadcast and multicast. Each ordinary
packet gets an execution process; {:continue, state} keeps that process alive
without automatically routing the sender's next packet to it. Stateless handlers
should finish with {:close, state}.
Quick Start
Basic Echo Server
defmodule MyEchoHandler do
use Abyss.Handler
@impl true
def handle_data({ip, port, data}, state) do
# Echo the data back to the client
Abyss.Transport.UDP.send(state.socket, ip, port, data)
{:close, state}
end
end
# Start the server
{:ok, _pid} = Abyss.start_link([
handler_module: MyEchoHandler,
port: 1234,
num_listeners: 10
])
Monitoring and Telemetry
Abyss provides built-in real-time metrics for monitoring server performance:
# Get current metrics
metrics = Abyss.Telemetry.get_metrics()
# => %{
# connections_active: 15,
# connections_total: 1250,
# accepts_total: 1250,
# responses_total: 1198,
# accepts_per_second: 25,
# responses_per_second: 23
# }
# Set up response time monitoring
:telemetry.attach_many(
"response-monitor",
[[:abyss, :metrics, :response_time]],
fn [:abyss, :metrics, :response_time], measurements, _metadata, _config ->
IO.puts("Response time: \#{measurements.response_time}ms")
end,
%{}
)
# Reset metrics
Abyss.Telemetry.reset_metrics()
Testing with netcat
# Send a test packet
echo "Hello, UDP" | nc -4 -u -w1 127.0.0.1 1234
# Continuous testing
while true; do echo "Hello, UDP $(date +%T)" | nc -4 -u -w1 127.0.0.1 1234; done
API Reference
Core Functions
start_link/1
Starts an Abyss server with the given options.
@spec start_link(options()) :: Supervisor.on_start()
Options:
handler_module(required) - Module implementingAbyss.Handlerbehaviorport- UDP port to listen on (default: 4000)num_listeners- Number of listener processes (default: 100)num_connections- Max concurrent connections (default: 16_384)transport_options- Keyword list passed to UDP transportread_timeout- Connection read timeout (default: 60_000ms)shutdown_timeout- Graceful shutdown timeout (default: 15_000ms)max_packet_size- Maximum packet size in bytes (default: 8192)broadcast- Enable broadcast mode (default: false)datagram_dispatcher- Optional module implementingAbyss.DatagramDispatcher(unicast only)dispatcher_options- Options passed to the dispatcher callback (default:[])dispatcher_max_queue- Maximum queued dispatcher sends (default: 128)dispatcher_max_queue_bytes- Maximum queued dispatcher bytes (default: 1,048,576)connection_telemetry_sample_rate- Sampling rate for connection telemetry (default: 0.05)handler_memory_check_interval- Memory check interval in ms (default: 10,000)handler_memory_warning_threshold- Memory warning threshold in MB (default: 100)handler_memory_hard_limit- Memory hard limit in MB (default: 150)
stop/2
Stops the server gracefully, waiting for connections to finish.
@spec stop(Supervisor.supervisor(), timeout()) :: :ok
suspend/1 and resume/1
Temporarily stop accepting new connections while maintaining existing ones.
def suspend(supervisor), do: Abyss.Server.suspend(supervisor)
def resume(supervisor), do: Abyss.Server.resume(supervisor)
Handler Behavior
Implement the Abyss.Handler behavior to process UDP packets:
defmodule MyHandler do
use Abyss.Handler
@impl true
def handle_data({ip, port, data}, state) do
# Process incoming UDP packet
response = process_data(data)
# Send response back to client
Abyss.Transport.UDP.send(state.socket, ip, port, response)
{:close, state} # Finish this datagram
# or
{:close, state} # Close connection after response
end
# Optional callbacks
@impl true
def handle_timeout(state) do
Logger.warn("Connection timed out")
{:close, state}
end
@impl true
def handle_error(reason, state) do
Logger.error("Handler error: #{inspect(reason)}")
{:continue, state}
end
@impl true
def init(state) do
{:ok, Map.put(state, :counter, 0)}
end
@impl true
def terminate(_reason, state) do
# Cleanup resources
:ok
end
end
Configuration Examples
Basic Configuration
Abyss.start_link([
handler_module: MyHandler,
port: 8080
])
Performance Tuning
Abyss.start_link([
handler_module: MyHandler,
port: 8080,
num_listeners: 200, # Increase for high throughput
num_connections: 32_768, # Allow more concurrent connections
read_timeout: 30_000 # Shorter timeout for faster cleanup
])
Security Configuration
Abyss.start_link([
handler_module: MyHandler,
port: 8080,
max_packet_size: 1024 # Limit packet size to prevent DoS
])
Broadcast/Multicast Configuration
Abyss.start_link([
handler_module: MyDatagramHandler,
port: 49_001,
broadcast: true,
transport_module: Abyss.Transport.UDP.Broadcast,
transport_options: [
ip: {0, 0, 0, 0}
]
])
multicast_if selects a local outgoing multicast interface, not a broadcast or
group destination. Receiving a group requires add_membership. See the
broadcast and multicast guide for tested
interface selection and IPv4/IPv6 membership examples.
Persistent Datagram Dispatcher
Stateful protocols can opt into one persistent dispatcher per listener while
the default remains one handler process per datagram. See
docs/dispatcher.md for the callback contract, bounded
writer behavior, and routing lifecycle.
Telemetry Events
Abyss emits comprehensive telemetry events for monitoring:
Listener Events
[:abyss, :listener, :start][:abyss, :listener, :ready][:abyss, :listener, :waiting][:abyss, :listener, :receiving][:abyss, :listener, :stop]
Connection Events
[:abyss, :connection, :start][:abyss, :connection, :ready][:abyss, :connection, :send][:abyss, :connection, :recv][:abyss, :connection, :stop]
Security Events
[:abyss, :listener, :packet_too_large]
Enabling Logging
# Attach structured logger at different levels
Abyss.Logger.attach_logger(:error) # Errors only
Abyss.Logger.attach_logger(:info) # General events
Abyss.Logger.attach_logger(:debug) # Detailed debugging
Abyss.Logger.attach_logger(:trace) # Verbose tracing
Performance Considerations
Tuning Guidelines
-
Listener Pool Size (
num_listeners)- Default: 100
- Increase for high-throughput scenarios
- Typical range: 10-1000
-
Connection Limits (
num_connections)- Default: 16_384
- Based on available memory and expected load
- Use
:infinityfor unlimited (with caution)
-
Buffer Sizes
- Configure via
transport_options
transport_options: [ recbuf: 8192, # Receive buffer sndbuf: 8192 # Send buffer ] - Configure via
Monitoring
Monitor key metrics via telemetry:
:telemetry.attach_many(
"abyss-monitor",
[
[:abyss, :listener, :start],
[:abyss, :connection, :start],
[:abyss, :listener, :packet_too_large]
],
&handle_metrics/4,
%{}
)
Security Best Practices
Production Deployment
When deploying Abyss to production, consider these security configurations:
Abyss.start_link([
handler_module: MyHandler,
port: 8080,
# Limit packet size to prevent memory exhaustion
max_packet_size: 8192,
# Use reasonable connection limits
num_connections: 10_000,
# Configure socket buffers
transport_options: [
recbuf: 262_144, # 256KB receive buffer
sndbuf: 262_144 # 256KB send buffer
]
])
Security Considerations
- Packet Size Limits: Set appropriate
max_packet_sizelimits - Connection Limits: Monitor and adjust
num_connectionsbased on resources - Network Access: Use firewall rules to restrict access when possible
- Monitoring: Set up alerts for security events
Monitoring Security Events
:telemetry.attach_many(
"security-monitor",
[
[:abyss, :listener, :packet_too_large]
],
&handle_security_event/4,
%{}
)
defp handle_security_event(event, measurements, metadata, config) do
case event do
[:abyss, :listener, :packet_too_large] ->
Logger.warn("Oversized packet from #{metadata.remote_address}: #{metadata.packet_size} bytes")
end
end
Examples
Echo Server
# Run echo server with trace logging
mix run --no-halt -e 'Code.require_file("example/echo.ex"); Abyss.Logger.attach_logger(:trace); Abyss.start_link(handler_module: Echo, port: 1234); Process.sleep(:infinity)'
# Test with netcat
echo "Hello, UDP" | nc -4 -u -w1 127.0.0.1 1234
DNS Server
# DNS forwarder
mix run --no-halt -e 'Code.require_file("example/dns_forwarder.ex"); Abyss.start_link(handler_module: HandleDNS, port: 53); Process.sleep(:infinity)'
# DNS recursive resolver
mix run --no-halt -e 'Code.require_file("example/dns_recursive.ex"); Abyss.start_link(handler_module: HandleDNS, port: 53); Process.sleep(:infinity)'
Broadcast Services
# Protocol-neutral broadcast receiver on an isolated test interface
mix run example/udp_one_to_many.exs receive 255.255.255.255 test0 49001
# Group receiver; test0 must exist in your isolated fixture
mix run example/udp_one_to_many.exs receive 239.255.42.1 test0 49002
# Send a group query from the selected test interface
mix run example/udp_one_to_many.exs query 239.255.42.1 test0 49002 probe
Architecture
Abyss implements a hierarchical supervision tree:
Abyss (main supervisor)
├── Abyss.ListenerPool (supervisor)
│ ├── Abyss.Listener (listener process 1)
│ ├── Abyss.Listener (listener process 2)
│ └── ... (up to num_listeners processes)
├── DynamicSupervisor (connection supervisor)
│ ├── Handler process 1 (per UDP packet)
│ ├── Handler process 2 (per UDP packet)
│ └── ... (up to num_connections processes)
├── Task (activator - starts listeners)
└── Abyss.ShutdownListener (coordinates graceful shutdown)
Request Flow
- Listener Pool: Manages multiple listener processes for load distribution
- Listener: Waits for UDP packets on the bound port
- Connection: Creates handler processes for incoming packets
- Handler: Processes packet data using user-defined logic
- Transport: Handles low-level UDP socket operations
With datagram_dispatcher configured, accepted unicast packets go through the
persistent dispatcher instead of creating a handler process for each packet.
Transport Layer
Abyss uses specialized UDP transport modules:
Abyss.Transport.UDP.Core- Shared socket operationsAbyss.Transport.UDP.Unicast- Unicast socket behaviorAbyss.Transport.UDP.Broadcast- Broadcast and multicast behavior
Supervision Strategy
- Rest for One: If a listener crashes, other listeners continue
- Dynamic Supervisor: Handler processes are isolated
- Graceful Shutdown: Coordinated termination with timeouts
Contributing
- Fork the repository
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Open a Pull Request
Development
# Install dependencies
mix deps.get
# Run tests
mix test
# Run tests with coverage
mix test --cover
# Format code
mix format
# Run dialyzer
mix dialyzer
# Run credo
mix credo
# Generate documentation
mix docs
License
This project is licensed under the MIT License - see the LICENSE file for details.