DEV Community

Davide Santangelo
Davide Santangelo

Posted on

Building a Ruby Gem with CLI and Networking Capabilities: The Lanet Story

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
Enter fullscreen mode Exit fullscreen mode

For Lanet, I used:

bundle gem lanet
Enter fullscreen mode Exit fullscreen mode

This creates the basic directory structure:

lanet/
├── bin/
├── lib/
│   ├── lanet.rb
│   └── lanet/
│       └── version.rb
├── spec/
├── Gemfile
├── LICENSE.txt
├── README.md
└── lanet.gemspec
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Don't forget to make it executable:

chmod +x bin/lanet
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Encryption: AES-256-CBC for message confidentiality
  2. Digital signatures: RSA signatures for authenticity and integrity
  3. 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:

  1. Generating RSA key pairs
  2. Signing messages with the private key
  3. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This implementation follows several important concurrent programming principles:

  1. Resource Limiting: The thread count is capped to prevent system overload
  2. Work Stealing: Threads dynamically grab work from a shared queue
  3. Thread Safety: A mutex ensures thread-safe updates to shared data
  4. Progress Reporting: Atomic updates to progress indicators
  5. Background Processing: ARP cache updates happen in parallel
  6. 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
Enter fullscreen mode Exit fullscreen mode

This graduated approach ensures:

  1. Minimal network traffic by trying the least intrusive methods first
  2. Maximum discovery rate by falling back to alternative techniques
  3. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Key security aspects include:

  1. Unique IV: A new IV is generated for each encryption operation
  2. Key Derivation: User passwords are processed to create cryptographically strong keys
  3. Exception Handling: All crypto operations are wrapped in error handling
  4. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This implementation adds resilience through:

  1. Retry Logic: Automatically retries transient network failures
  2. Graduated Delay: Waits between retries to avoid overwhelming the network
  3. Specific Exception Handling: Catches only the exceptions it can meaningfully handle
  4. 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
Enter fullscreen mode Exit fullscreen mode

This thread pool offers several advantages:

  1. Dynamic Scaling: Grows and shrinks based on workload
  2. Controlled Queuing: Prevents memory issues from unbounded queues
  3. Clean Shutdown: Workers terminate gracefully
  4. 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
Enter fullscreen mode Exit fullscreen mode

The Actor pattern isolates message processing, providing:

  1. Message Queuing: Messages are processed in order
  2. State Encapsulation: Actor maintains internal state safely
  3. Decoupling: Receiver and processor are separated
  4. 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
Enter fullscreen mode Exit fullscreen mode

This approach:

  1. Limits Memory Usage: By processing in batches instead of loading all IPs at once
  2. Maintains Responsiveness: Allows progress updates between batches
  3. 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)
Enter fullscreen mode Exit fullscreen mode

This dynamic approach ensures your application:

  1. Scales with Hardware: Uses more threads on systems with more cores
  2. Avoids Oversubscription: Doesn't create more threads than necessary
  3. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This implements a simple yet secure chat system with:

  1. End-to-End Encryption: Messages are encrypted with AES
  2. Message Signing: Each message is digitally signed
  3. Private Messaging: Direct messages to specific IPs
  4. 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.


Resources

Top comments (0)