In the world of modern software development, creating tools that enhance productivity and solve real-world problems is both challenging and rewarding. This article walks through the process of building a Ruby gem with CLI capabilities and advanced networking features, using my project Lanet as a case study.
Introduction: From Idea to Implementation
Ruby gems are self-contained packages of code that extend Ruby's functionality. When combined with a command-line interface (CLI), they become powerful tools for developers and system administrators. The journey from concept to published gem involves several key steps, which I'll share based on my experience developing Lanet, a comprehensive local network communication tool.
What is Lanet?
Lanet is a lightweight, powerful LAN communication tool that enables secure message exchange between devices on the same network. It provides an intuitive interface for network discovery, secure messaging, and diagnostics—all from either a Ruby API or command-line interface.
Key features include:
- Network scanning and device discovery
- Encrypted messaging between devices
- Message broadcasting to all network devices
- Digital signatures for message authenticity
- Host pinging with detailed metrics
- Simple yet powerful command-line interface
Step 1: Setting Up the Gem Structure
Every successful gem begins with a solid structure. Here's how to set up your project:
# Install the bundler gem if you haven't already
gem install bundler
# Create a new gem scaffold
bundle gem your_gem_name
For Lanet, I used:
bundle gem lanet
This creates the basic directory structure:
lanet/
├── bin/
├── lib/
│ ├── lanet.rb
│ └── lanet/
│ └── version.rb
├── spec/
├── Gemfile
├── LICENSE.txt
├── README.md
└── lanet.gemspec
Step 2: Defining Your Gem's Specifications
The .gemspec
file defines your gem's metadata, dependencies, and packaging information. Here's a simplified version of Lanet's gemspec:
require_relative "lib/lanet/version"
Gem::Specification.new do |spec|
spec.name = "lanet"
spec.version = Lanet::VERSION
spec.authors = ["Davide Santangelo"]
spec.email = ["davide.santangelo@example.com"]
spec.summary = "CLI/API tool for local network communication"
spec.description = "LAN device discovery, secure messaging, and network monitoring"
spec.homepage = "https://github.com/davidesantangelo/lanet"
spec.license = "MIT"
spec.required_ruby_version = ">= 3.1.0"
# Dependencies
spec.add_dependency "thor", "~> 1.2"
# Executable
spec.bindir = "bin"
spec.executables = ["lanet"]
end
Step 3: Building the Core Functionality
For Lanet, I divided the functionality into several modules, each responsible for a specific aspect:
Network Scanner
The scanner identifies active devices on the network using multiple detection methods:
module Lanet
class Scanner
def scan(cidr, timeout = 1, max_threads = 32, verbose = false)
# Convert CIDR notation to individual IP addresses
# Scan each IP using multiple techniques
# Return list of active devices
end
private
def tcp_port_scan(ip, ports)
# Try connecting to common ports
end
def ping_check(ip)
# Send ICMP ping
end
def udp_check(ip)
# Check UDP ports
end
end
end
Message Encryption
Security is critical for network communication. Lanet implements AES encryption and RSA digital signatures:
module Lanet
class Encryptor
def self.prepare_message(message, encryption_key, private_key = nil)
# Encrypt with AES if key provided
# Sign with RSA if private key provided
end
def self.process_message(data, encryption_key = nil, public_key = nil)
# Decrypt if needed
# Verify signature if present
# Return processed content
end
end
end
Network Communication
For sending and receiving messages:
module Lanet
class Sender
def initialize(port)
@port = port
@socket = UDPSocket.new
end
def send_to(target_ip, message)
# Send UDP packet to specific IP
end
def broadcast(message)
# Send UDP broadcast to entire subnet
end
end
class Receiver
def listen(&block)
# Listen for incoming UDP packets
# Pass received messages to callback
end
end
end
Step 4: Creating the Command-Line Interface
The CLI translates user commands into actions. I used Thor, a powerful toolkit for building command-line interfaces:
require "thor"
module Lanet
class CLI < Thor
desc "scan --range CIDR", "Scan for active devices"
option :range, required: true
def scan
scanner = Scanner.new
results = scanner.scan(options[:range])
# Display results
end
desc "send", "Send a message to a specific target"
option :target, required: true
option :message, required: true
def send
sender = Sender.new
sender.send_to(options[:target], options[:message])
end
# Additional commands...
end
end
Step 5: Testing Your Gem
Thorough testing ensures your gem works as expected. Lanet uses RSpec for both unit and integration tests:
RSpec.describe Lanet::Scanner do
describe "#scan" do
it "finds active hosts on the network" do
scanner = described_class.new
# Test scanning functionality
end
end
end
RSpec.describe "Message flow integration" do
it "can complete a full message cycle" do
# Test end-to-end encryption and communication
end
end
Step 6: Creating an Executable
To make your gem runnable from the command line, create an executable in the bin
directory:
#!/usr/bin/env ruby
# bin/lanet
require "lanet"
require "lanet/cli"
Lanet::CLI.start(ARGV)
Don't forget to make it executable:
chmod +x bin/lanet
Step 7: Documentation and Examples
Good documentation is crucial for adoption. I created comprehensive documentation in Lanet's README.md, including:
- Installation instructions
- Usage examples for both CLI and API
- Configuration options
- Real-world use cases
Step 8: Publishing Your Gem
Once your gem is ready:
# Build the gem
gem build lanet.gemspec
# Push to RubyGems
gem push lanet-0.1.0.gem
Networking Concepts in Lanet
Developing Lanet required understanding several key networking concepts:
UDP vs TCP
Lanet primarily uses UDP for messaging because:
- It's lightweight and has lower overhead than TCP
- It's ideal for simple message passing within a LAN
- Its connectionless nature works well for broadcasts
CIDR Notation
CIDR (Classless Inter-Domain Routing) notation (like 192.168.1.0/24) defines network ranges. Understanding it was crucial for implementing the network scanner.
Broadcasting
Network broadcasts send packets to all devices on a subnet by using the special address 255.255.255.255 or subnet-specific broadcast addresses.
ARP (Address Resolution Protocol)
Lanet uses ARP tables to map IP addresses to MAC addresses, helping identify devices even when they don't respond to conventional scans.
Security Considerations
Network tools pose security challenges. Lanet implements:
- Encryption: AES-256-CBC for message confidentiality
- Digital signatures: RSA signatures for authenticity and integrity
- No persistent connections: Reducing potential attack surfaces
Advanced Features: Digital Signatures
One of Lanet's most powerful features is support for digital signatures, which:
- Verify the authenticity of messages
- Ensure messages haven't been tampered with
- Provide non-repudiation (senders can't deny sending signed messages)
Implementation involves:
- Generating RSA key pairs
- Signing messages with the private key
- Verifying signatures with the public key
module Lanet
class Signer
def self.sign(message, private_key_pem)
private_key = OpenSSL::PKey::RSA.new(private_key_pem)
signature = private_key.sign(OpenSSL::Digest.new("SHA256"), message)
Base64.strict_encode64(signature)
end
def self.verify(message, signature_base64, public_key_pem)
public_key = OpenSSL::PKey::RSA.new(public_key_pem)
signature = Base64.strict_decode64(signature_base64)
public_key.verify(OpenSSL::Digest.new("SHA256"), signature, message)
end
end
end
Challenges and Solutions
Creating Lanet wasn't without challenges:
Cross-Platform Compatibility
Challenge: Network commands differ between operating systems.
Solution: OS detection and command adaptation:
def ping_command(host)
case RbConfig::CONFIG["host_os"]
when /mswin|mingw|cygwin/
# Windows command
when /darwin/
# macOS command
else
# Linux/Unix command
end
end
Thread Management
Challenge: Network scanning can be slow if done sequentially.
Solution: Thread pooling with controlled concurrency:
def scan(cidr, timeout = 1, max_threads = 32)
# Create thread pool limited to max_threads
# Process IPs concurrently
# Join threads and collect results
end
UDP Reliability
Challenge: UDP doesn't guarantee delivery.
Solution: Implement application-level acknowledgments for critical messages.
Deep Dive: Scanner Implementation
The network scanner is one of the most sophisticated parts of Lanet. Let's explore its implementation in detail:
Efficient IP Range Processing
Converting a CIDR notation to an IP range efficiently is crucial for performance:
def scan(cidr, timeout = 1, max_threads = 32, verbose = false)
@verbose = verbose
@timeout = timeout
@hosts = []
range = IPAddr.new(cidr).to_range # Converts CIDR to a Ruby range of IP addresses
queue = Queue.new
range.each { |ip| queue << ip.to_s } # Load all IPs into a thread-safe queue
total_ips = queue.size
completed = 0
# Rest of the method...
end
Thread Pool Design Pattern
For optimal scanning performance, we implement a thread pool architecture:
# Create a pool of worker threads
threads = Array.new([max_threads, total_ips].min) do
Thread.new do
loop do
begin
ip = queue.pop(true) # Non-blocking pop that raises when queue is empty
rescue ThreadError
break # Exit thread when no more work
end
scan_host(ip)
# Update progress atomically
@mutex.synchronize do
completed += 1
if total_ips < 100 || (completed % 10).zero? || completed == total_ips
print_progress(completed, total_ips)
end
end
end
end
end
# Periodically update ARP cache in background
arp_updater = Thread.new do
while threads.any?(&:alive?)
sleep 5
@mutex.synchronize { @arp_cache = parse_arp_table }
end
end
This implementation follows several important concurrent programming principles:
- Resource Limiting: The thread count is capped to prevent system overload
- Work Stealing: Threads dynamically grab work from a shared queue
- Thread Safety: A mutex ensures thread-safe updates to shared data
- Progress Reporting: Atomic updates to progress indicators
- Background Processing: ARP cache updates happen in parallel
- Resource Cleanup: Threads are properly joined and terminated
Host Detection Strategies
The scanner uses multiple detection methods in sequence, from fastest to most thorough:
def scan_host(ip)
# Skip broadcast and network addresses
return if ip.end_with?(".255") || (ip.end_with?(".0") && !ip.end_with?(".0.0"))
is_active = false
detection_method = nil
# 1. TCP port scan (fastest for accessible hosts)
tcp_result = tcp_port_scan(ip, QUICK_CHECK_PORTS)
if tcp_result[:active]
is_active = true
detection_method = "TCP"
open_ports = tcp_result[:open_ports]
end
# 2. ICMP ping (reliable but may be blocked by firewalls)
if !is_active && ping_check(ip)
is_active = true
detection_method = "ICMP"
end
# 3. UDP probe (works for some network devices)
if !is_active && udp_check(ip)
is_active = true
detection_method = "UDP"
end
# 4. ARP cache lookup (local network only, passive)
unless is_active
mac = get_mac_address(ip)
if mac && mac != "(incomplete)"
is_active = true
detection_method = "ARP"
end
end
# Additional processing for active hosts...
end
This graduated approach ensures:
- Minimal network traffic by trying the least intrusive methods first
- Maximum discovery rate by falling back to alternative techniques
- Optimized scan time by only doing detailed port scans for already discovered hosts
TCP Port Scanning with Concurrent Socket Operations
The TCP port scanner optimizes performance through parallel connection attempts:
def tcp_port_scan(ip, ports)
open_ports = []
is_active = false
# Create a separate thread for each port
threads = ports.map do |port|
Thread.new do
begin
Timeout.timeout(@timeout) do
socket = TCPSocket.new(ip, port)
Thread.current[:open] = port # Thread-local storage
socket.close
end
rescue Errno::ECONNREFUSED
# Connection refused means host is active but port is closed
Thread.current[:active] = true
rescue StandardError
# Timeout or other error - port likely closed/filtered
end
end
end
# Process results from threads
threads.each do |thread|
thread.join
if thread[:open] # Port is open
open_ports << thread[:open]
is_active = true
elsif thread[:active] # Port is closed but host is responsive
is_active = true
end
end
{ active: is_active, open_ports: open_ports }
end
The technique of using thread-local variables (via Thread.current
) is particularly useful here as it avoids the need for mutexes when storing per-thread results.
Advanced Encryption Techniques
Message Format and Type Detection
Lanet uses a clever message prefixing system to identify the message type:
def self.process_message(data, encryption_key = nil, public_key = nil)
return { content: "[Empty message]", verified: false } if data.nil? || data.empty?
# Determine message type from prefix
prefix = data[0]
prefix = data[0..1] if data.length > 1 && %w[SE SP].include?(data[0..1])
content = data[prefix.length..]
case prefix
when ENCRYPTED_PREFIX
# Process encrypted message
# ...
when PLAINTEXT_PREFIX
# Process plaintext message
# ...
when SIGNED_ENCRYPTED_PREFIX
# Process signed and encrypted message
# ...
when SIGNED_PLAINTEXT_PREFIX
# Process signed plaintext message
# ...
else
{ content: "[Invalid message format]", verified: false }
end
end
This approach creates a self-describing protocol where each message carries information about how it should be processed.
AES Encryption Implementation
Here's how the AES encryption is implemented:
def self.prepare_unsigned_message(message, key)
return PLAINTEXT_PREFIX + message.to_s if key.nil? || key.empty?
begin
# Create a new AES cipher in CBC mode
cipher = OpenSSL::Cipher.new("AES-128-CBC")
cipher.encrypt
# Generate a secure key from the user's passphrase
cipher.key = derive_key(key)
# Generate a unique initialization vector
iv = cipher.random_iv
# Encrypt the message
encrypted = cipher.update(message.to_s) + cipher.final
# Combine IV and ciphertext, then encode as Base64
encoded = Base64.strict_encode64(iv + encrypted)
# Prepend the message type indicator
"#{ENCRYPTED_PREFIX}#{encoded}"
rescue StandardError => e
raise Error, "Encryption failed: #{e.message}"
end
end
The decryption process mirrors this approach:
def self.decode_encrypted_message(content, key)
# Decode the Base64 string
decoded = Base64.strict_decode64(content)
# Extract the IV from the first 16 bytes
iv = decoded[0...16]
# The rest is ciphertext
ciphertext = decoded[16..]
# Create and configure the decryption cipher
decipher = OpenSSL::Cipher.new("AES-128-CBC")
decipher.decrypt
decipher.key = derive_key(key)
decipher.iv = iv
# Decrypt and return the plaintext
decipher.update(ciphertext) + decipher.final
end
Key security aspects include:
- Unique IV: A new IV is generated for each encryption operation
- Key Derivation: User passwords are processed to create cryptographically strong keys
- Exception Handling: All crypto operations are wrapped in error handling
- Algorithm Selection: AES in CBC mode with 128-bit keys provides strong security
Message Signature Verification
For digital signatures, the signature verification process follows these steps:
def self.process_signed_content(content, public_key)
if content.include?(SIGNATURE_DELIMITER)
# Split the content from the signature
message, signature = content.split(SIGNATURE_DELIMITER, 2)
if public_key.nil? || public_key.strip.empty?
{ content: message, verified: false, verification_status: "No public key provided for verification" }
else
begin
# Verify the signature against the message
verified = Signer.verify(message, signature, public_key)
{
content: message,
verified: verified,
verification_status: verified ? "Verified" : "Signature verification failed"
}
rescue StandardError => e
{ content: message, verified: false, verification_status: "Verification error: #{e.message}" }
end
end
else
{ content: content, verified: false, verification_status: "No signature found" }
end
end
Advanced Testing Strategies for Networking Code
Networking code presents unique testing challenges. Here's how Lanet handles them:
Mocking Network Operations
When testing, we don't want to make actual network calls:
RSpec.describe Lanet::Scanner do
subject(:scanner) { described_class.new }
describe "#scan" do
before do
# Mock network operations to avoid actual network access
allow(scanner).to receive(:tcp_port_scan).and_return({ active: false, open_ports: [] })
allow(scanner).to receive(:ping_check).and_return(false)
allow(scanner).to receive(:get_mac_address).and_return(nil)
end
it "scans the specified network range" do
# Test with a small range
range = "192.168.1.1/30" # Only 4 addresses
# Mock one active host
allow(scanner).to receive(:tcp_port_scan)
.with("192.168.1.2", anything)
.and_return({ active: true, open_ports: [80] })
result = scanner.scan(range, 0.1, 2)
expect(result).to include("192.168.1.2")
expect(result.size).to eq(1)
end
end
end
Testing UDP Sockets with Mocks
Testing UDP communication requires careful mocking since we can't establish real connections in tests:
RSpec.describe "Sender" do
it "can send a message to a specific IP" do
# Create a mock UDP socket
socket_mock = instance_double(UDPSocket)
allow(socket_mock).to receive(:setsockopt)
allow(socket_mock).to receive(:send)
allow(UDPSocket).to receive(:new).and_return(socket_mock)
# Create a test sender with our mock socket
sender = Lanet::Sender.new(5000)
# Test that the send method calls the socket's send method with correct args
message = "Test message"
target_ip = "192.168.1.5"
expect(socket_mock).to receive(:send).with(message, 0, target_ip, 5000)
sender.send_to(target_ip, message)
end
end
Integration Tests for Message Flow
To test the complete message flow:
RSpec.describe "Message flow integration", type: :integration do
let(:message) { "Integration test message" }
let(:encryption_key) { "integration-test-key" }
let(:key_pair) { Lanet::Signer.generate_key_pair }
it "can complete a full message cycle" do
# Step 1: Prepare the message (encrypt + sign)
prepared_message = Lanet::Encryptor.prepare_message(
message,
encryption_key,
key_pair[:private_key]
)
# Step 2: Process the message (decrypt + verify)
result = Lanet::Encryptor.process_message(
prepared_message,
encryption_key,
key_pair[:public_key]
)
# Step 3: Verify the result
expect(result[:content]).to eq(message)
expect(result[:verified]).to be true
end
it "detects tampered messages" do
# Prepare a signed and encrypted message
prepared = Lanet::Encryptor.prepare_message(
message,
encryption_key,
key_pair[:private_key]
)
# Tamper with the message
tampered = prepared[0..-2] + (prepared[-1] == "A" ? "B" : "A")
# Process the tampered message
result = Lanet::Encryptor.process_message(
tampered,
encryption_key,
key_pair[:public_key]
)
# The tampering should be detected
expect(result[:verified]).to be false
end
end
Error Handling and Resilience
Robust networking applications must handle a variety of error conditions gracefully:
def send_with_retry(target, message, retries = 3, delay = 1)
attempts = 0
begin
attempts += 1
@socket.send(message, 0, target, @port)
rescue IOError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH => e
if attempts <= retries
puts "Connection error: #{e.message}. Retrying in #{delay} seconds..."
sleep delay
retry
else
puts "Failed to send message after #{retries} attempts: #{e.message}"
end
rescue StandardError => e
puts "Unexpected error: #{e.message}"
end
end
This implementation adds resilience through:
- Retry Logic: Automatically retries transient network failures
- Graduated Delay: Waits between retries to avoid overwhelming the network
- Specific Exception Handling: Catches only the exceptions it can meaningfully handle
- Graceful Degradation: Fails cleanly when retries are exhausted
Advanced Concurrency Patterns in Ruby
Thread Pool with Work Queue
A more sophisticated thread pool implementation can dynamically adjust to workload:
class ThreadPool
def initialize(min_threads, max_threads, queue_size = 1000)
@min_threads = min_threads
@max_threads = max_threads
@queue = SizedQueue.new(queue_size)
@workers = []
@mutex = Mutex.new
@running = true
# Start minimum number of worker threads
@min_threads.times { add_worker }
end
def schedule(&task)
raise "ThreadPool is shutdown" unless @running
@queue << task
# Add more workers if needed and possible
@mutex.synchronize do
if @workers.size < @max_threads && @queue.size > @workers.size
add_worker
end
end
end
def shutdown
@running = false
@workers.size.times { @queue << ->{ throw :stop } }
@workers.each(&:join)
end
private
def add_worker
@workers << Thread.new do
catch(:stop) do
while @running
task = @queue.pop
task.call
end
end
end
end
end
# Usage with scanner:
pool = ThreadPool.new(4, 32)
ip_addresses.each do |ip|
pool.schedule do
scan_host(ip)
end
end
pool.shutdown
This thread pool offers several advantages:
- Dynamic Scaling: Grows and shrinks based on workload
- Controlled Queuing: Prevents memory issues from unbounded queues
- Clean Shutdown: Workers terminate gracefully
- Exception Isolation: Exceptions in one task don't crash the pool
Actor-Based Message Processing
For complex message handling, the Actor model provides a clean design:
class MessageActor
def initialize(encryption_key = nil, public_key = nil)
@encryption_key = encryption_key
@public_key = public_key
@queue = Queue.new
@thread = Thread.new { process_messages }
end
def receive(message, sender_ip)
@queue << [message, sender_ip]
end
def stop
@queue << :stop
@thread.join
end
private
def process_messages
loop do
message, sender_ip = @queue.pop
break if message == :stop
result = Lanet::Encryptor.process_message(
message,
@encryption_key,
@public_key
)
handle_message(result, sender_ip)
end
end
def handle_message(result, sender_ip)
puts "Message from #{sender_ip}:"
puts "Content: #{result[:content]}"
if result[:verified]
puts "Signature: VERIFIED"
elsif result.key?(:verification_status)
puts "Signature: NOT VERIFIED (#{result[:verification_status]})"
end
end
end
# Usage with receiver:
actor = MessageActor.new("secret_key", public_key)
receiver = Lanet.receiver
receiver.listen do |message, sender_ip|
actor.receive(message, sender_ip)
end
The Actor pattern isolates message processing, providing:
- Message Queuing: Messages are processed in order
- State Encapsulation: Actor maintains internal state safely
- Decoupling: Receiver and processor are separated
- Resource Management: Clean shutdown process
Performance Optimization Techniques
Batch Processing for Network Operations
When scanning large networks, batch processing improves performance:
def scan(cidr, timeout = 1, max_threads = 32, verbose = false)
# Initialize variables...
range = IPAddr.new(cidr).to_range
# Process IPs in batches for better memory efficiency
ip_batches = range.each_slice(1000).to_a
ip_batches.each do |batch|
queue = Queue.new
batch.each { |ip| queue << ip.to_s }
# Create thread pool for this batch
threads = Array.new([max_threads, queue.size].min) do
Thread.new do
until queue.empty?
begin
ip = queue.pop(true)
scan_host(ip)
rescue ThreadError
break
end
end
end
end
threads.each(&:join)
end
# Return results...
end
This approach:
- Limits Memory Usage: By processing in batches instead of loading all IPs at once
- Maintains Responsiveness: Allows progress updates between batches
- Prevents Thread Explosion: Adjusts thread count based on batch size
CPU-Aware Threading
For optimal performance, tune thread count based on available CPU cores:
def optimal_thread_count(work_size)
# Get CPU core count
cpu_count = if RUBY_PLATFORM =~ /darwin/
Integer(`sysctl -n hw.logicalcpu` rescue '2')
elsif RUBY_PLATFORM =~ /linux/
Integer(`nproc` rescue '2')
else
2 # Default fallback
end
# Calculate thread count based on cores and work type
# For I/O-bound work, 2-4x CPU count often works well
# For CPU-bound work, stay close to CPU count
io_multiplier = 3
# Limit based on work size and system capabilities
[cpu_count * io_multiplier, work_size, 32].min
end
# Usage:
thread_count = optimal_thread_count(ip_addresses.size)
This dynamic approach ensures your application:
- Scales with Hardware: Uses more threads on systems with more cores
- Avoids Oversubscription: Doesn't create more threads than necessary
- Adapts to Workload: Creates appropriate threads for the actual work size
Real-World Applications
Network Health Monitoring System
Here's how you might build a network monitoring system with Lanet:
require 'lanet'
require 'json'
class NetworkMonitor
def initialize(config_file = 'network_config.json')
@config = JSON.parse(File.read(config_file))
@scanner = Lanet.scanner
@pinger = Lanet.pinger(timeout: 1, count: 3)
@last_status = {}
end
def run
loop do
monitor_devices
sleep @config['check_interval']
end
end
def monitor_devices
puts "#{Time.now}: Checking #{@config['devices'].size} devices"
@config['devices'].each do |device|
result = @pinger.ping_host(device['ip'])
current_status = result[:status]
if @last_status[device['ip']] != current_status
status_changed(device, current_status, result[:response_time])
end
@last_status[device['ip']] = current_status
end
end
def status_changed(device, online, response_time)
message = if online
"RECOVERED: #{device['name']} is back online. Response time: #{response_time}ms"
else
"ALERT: #{device['name']} (#{device['ip']}) is DOWN!"
end
puts message
notify_admin(message) if device['critical']
end
def notify_admin(message)
# Send via broadcast to any listening admin tools
sender = Lanet.sender
sender.broadcast("ALERT: #{message}")
# Could also send email, SMS, etc.
end
end
# Usage:
monitor = NetworkMonitor.new
monitor.run
Secure Chat Application
Building a secure chat system with Lanet is straightforward:
require 'lanet'
class SecureChat
def initialize(nickname, encryption_key, port = 5000)
@nickname = nickname
@encryption_key = encryption_key
@port = port
@sender = Lanet.sender(@port)
# Generate RSA key pair for signing messages
@key_pair = Lanet::Signer.generate_key_pair
puts "Chat initialized for #{@nickname}"
puts "Your public key fingerprint: #{key_fingerprint(@key_pair[:public_key])}"
end
def start_listening
puts "Listening for messages on port #{@port}..."
# Start receiver in a separate thread
@receiver_thread = Thread.new do
receiver = Lanet.receiver(@port)
receiver.listen do |data, sender_ip|
process_message(data, sender_ip)
end
end
# Read user input in main thread
read_user_input
rescue Interrupt
puts "\nExiting chat..."
ensure
@receiver_thread.kill if @receiver_thread
end
private
def process_message(data, sender_ip)
result = Lanet::Encryptor.process_message(
data,
@encryption_key,
nil # We don't have others' public keys yet
)
# Parse the message format: "NICKNAME: message"
if result[:content] =~ /^([^:]+): (.+)$/
nick = $1
message = $2
puts "\n#{nick} (#{sender_ip}): #{message}"
else
puts "\nMessage from #{sender_ip}: #{result[:content]}"
end
print "> " # Restore prompt
end
def read_user_input
loop do
print "> "
input = gets.chomp
break if input == '/quit'
# Format message with nickname
full_message = "#{@nickname}: #{input}"
# Encrypt and sign the message
prepared = Lanet::Encryptor.prepare_message(
full_message,
@encryption_key,
@key_pair[:private_key]
)
if input.start_with?('/msg ')
# Private message
_, target, message = input.split(' ', 3)
@sender.send_to(target, prepared)
puts "Private message sent to #{target}"
else
# Broadcast to everyone
@sender.broadcast(prepared)
end
end
end
def key_fingerprint(public_key)
require 'digest/sha1'
Digest::SHA1.hexdigest(public_key)[0,8]
end
end
# Usage:
puts "Enter your nickname:"
nickname = gets.chomp
puts "Enter encryption key (shared secret):"
key = gets.chomp
chat = SecureChat.new(nickname, key)
chat.start_listening
This implements a simple yet secure chat system with:
- End-to-End Encryption: Messages are encrypted with AES
- Message Signing: Each message is digitally signed
- Private Messaging: Direct messages to specific IPs
- Broadcasting: Public messages to all participants
Conclusion
Building a robust networking gem like Lanet requires combining various disciplines: socket programming, cryptography, concurrency, and command-line interface design. By breaking the problem into manageable components and applying solid design principles, you can create powerful tools that solve real-world networking challenges.
The techniques we've explored—from efficient thread pooling to secure message signing—demonstrate how Ruby can be used for sophisticated network applications. Whether you're building a LAN communication tool like Lanet, a network monitoring system, or a completely different gem, these principles can guide you toward creating high-quality, maintainable code.
Remember that great developer tools strike a balance between power and simplicity. By providing both a programmer-friendly API and an intuitive CLI, your gem can serve a wide audience of users with different needs and skill levels.
I hope this walkthrough helps you create your own amazing Ruby gems. If you'd like to explore Lanet further, check it out on GitHub or install it with gem install lanet
.
Top comments (0)