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)