Added all of the existing code

This commit is contained in:
2025-09-27 14:43:52 +01:00
commit 6772bfd842
58 changed files with 19587 additions and 0 deletions

330
FREEZING_ISSUE_FIXES.md Normal file
View File

@@ -0,0 +1,330 @@
# TechIRCd Freezing Issue - Fixes Applied
## Issue Description
TechIRCd was experiencing freezing/hanging issues where the server would become unresponsive after running for a while, preventing new user connections and causing existing users to disconnect.
## Root Causes Identified
### 1. Resource Leaks in Client Handling
- **Problem**: Disconnected clients weren't being properly cleaned up, leading to memory leaks and resource exhaustion
- **Symptoms**: Server gradually becoming slower and eventually unresponsive
### 2. Goroutine Leaks
- **Problem**: Client handler goroutines weren't exiting properly when connections were broken
- **Symptoms**: Increasing number of goroutines over time, eventually exhausting system resources
### 3. Lock Contention in Ping/Health Checks
- **Problem**: Server-wide ping and health checks were holding locks too long and running synchronously
- **Symptoms**: Server becoming unresponsive during health checks, client operations timing out
### 4. Inefficient Connection State Management
- **Problem**: Inconsistent tracking of client connection state, leading to operations on dead connections
- **Symptoms**: Hanging writes, blocked goroutines, server freezing
### 5. Deadlocked Shutdown Process
- **Problem**: Shutdown process could deadlock when trying to notify clients while holding locks
- **Symptoms**: Ctrl+C not working, server becoming completely unresponsive, requiring SIGKILL
## Fixes Applied
### 1. Enhanced Client Cleanup (`client.go`)
#### Before:
```go
func (c *Client) cleanup() {
// Basic cleanup with potential blocking operations
if c.conn != nil {
c.conn.Close() // Could hang
}
// Synchronous channel cleanup
// Synchronous server removal
}
```
#### After:
```go
func (c *Client) cleanup() {
// Non-blocking cleanup with timeouts
if c.conn != nil {
c.conn.SetDeadline(time.Now().Add(5 * time.Second))
c.conn.Close()
}
// Asynchronous channel cleanup in goroutine
// Asynchronous server removal in goroutine
}
```
**Benefits:**
- Prevents cleanup operations from blocking other clients
- Uses timeouts to force close hanging connections
- Prevents deadlocks during shutdown
### 2. Improved Connection Handling
#### Connection Timeout Management:
```go
func (c *Client) handleMessageRead(...) bool {
// Set read deadline with timeout to prevent hanging
readTimeout := 30 * time.Second
if c.IsRegistered() {
readTimeout = 5 * time.Minute
}
// Use goroutine with timeout for non-blocking reads
scanChan := make(chan bool, 1)
go func() {
// Scanner in separate goroutine
scanChan <- scanner.Scan()
}()
select {
case result := <-scanChan:
// Process result
case <-time.After(readTimeout):
// Handle timeout
return false
}
}
```
**Benefits:**
- Prevents infinite blocking on read operations
- Detects and handles dead connections quickly
- Provides graceful timeout handling
### 3. Non-blocking Ping and Health Checks (`server.go`)
#### Before:
```go
func (s *Server) pingRoutine() {
ticker := time.NewTicker(30 * time.Second)
for {
select {
case <-ticker.C:
s.performPingCheck() // Blocking operation
}
}
}
```
#### After:
```go
func (s *Server) pingRoutine() {
ticker := time.NewTicker(60 * time.Second) // Less frequent
for {
select {
case <-ticker.C:
go s.performPingCheck() // Non-blocking
}
}
}
```
#### Enhanced Ping Check:
```go
func (s *Server) performPingCheck() {
// Get snapshot of client IDs without holding lock
s.mu.RLock()
clientIDs := make([]string, 0, len(s.clients))
for clientID := range s.clients {
clientIDs = append(clientIDs, clientID)
}
s.mu.RUnlock()
// Process clients individually to prevent blocking
for _, clientID := range clientIDs {
// Non-blocking ping sending
go func(id string) {
// Send ping in separate goroutine
}(clientID)
}
}
```
**Benefits:**
- Eliminates blocking during server-wide operations
- Reduces lock contention
- Prevents cascade failures
### 4. Batched Health Checks
#### Before:
```go
func (s *Server) performHealthCheck() {
// Hold lock for entire operation
s.mu.RLock()
clients := make([]*Client, 0, len(s.clients))
// ... process all clients synchronously
s.mu.RUnlock()
}
```
#### After:
```go
func (s *Server) performHealthCheck() {
// Process clients in batches of 50
batchSize := 50
for i := 0; i < len(clientIDs); i += batchSize {
batch := clientIDs[i:end]
for _, clientID := range batch {
// Process each client individually
}
// Small delay between batches
time.Sleep(10 * time.Millisecond)
}
}
```
**Benefits:**
- Prevents overwhelming the system during health checks
- Allows other operations to proceed between batches
- Reduces memory usage during large client counts
### 5. Enhanced Error Recovery
#### Panic Recovery:
```go
defer func() {
if r := recover(); r != nil {
log.Printf("Panic in client handler for %s: %v", c.getClientInfo(), r)
}
c.cleanup()
}()
```
#### Graceful Disconnection:
```go
func (c *Client) ForceDisconnect(reason string) {
log.Printf("Force disconnecting client %s: %s", c.getClientInfo(), reason)
c.mu.Lock()
c.disconnected = true
c.mu.Unlock()
if c.conn != nil {
c.SendMessage(fmt.Sprintf("ERROR :%s", reason))
}
}
```
### 5. Robust Shutdown Process (`server.go` & `main.go`)
#### Before:
```go
func (s *Server) Shutdown() {
// Could deadlock holding locks
s.mu.RLock()
for _, client := range s.clients {
client.SendMessage("ERROR :Server shutting down")
}
s.mu.RUnlock()
close(s.shutdown)
}
```
#### After:
```go
func (s *Server) Shutdown() {
// Non-blocking shutdown with timeout protection
// Close listeners immediately
go func() { /* close listeners */ }()
// Signal shutdown first
close(s.shutdown)
// Disconnect clients in batches asynchronously
go func() {
// Process clients in batches of 10
// Each client disconnection in separate goroutine
// Timeout protection for each operation
}()
// Force shutdown after reasonable timeout
time.Sleep(2 * time.Second)
}
```
#### Signal Handling with Force Option:
```go
// Double Ctrl+C for immediate shutdown
if shutdownInProgress {
log.Println("Forcing immediate shutdown...")
os.Exit(1)
}
// Timeout protection for graceful shutdown
select {
case <-shutdownComplete:
log.Println("Graceful shutdown completed")
case <-time.After(10 * time.Second):
log.Println("Shutdown timeout, forcing exit...")
}
```
**Benefits:**
- Prevents deadlocks during shutdown
- Allows double Ctrl+C for immediate force shutdown
- Timeout protection prevents hanging shutdown
- Asynchronous operations prevent blocking
## Configuration Optimizations
### Timing Adjustments:
- **Ping Interval**: Increased from 30s to 60s to reduce overhead
- **Health Check Interval**: Increased from 60s to 5 minutes
- **Read Timeouts**: More conservative timeouts for better stability
- **Registration Timeout**: Better enforcement to prevent hanging registrations
### Resource Limits:
- **Batch Processing**: Health checks limited to 50 clients per batch
- **Connection Limits**: Better enforcement of max client limits
- **Memory Management**: Proactive cleanup of disconnected clients
## Expected Results
1. **Stability**: Server should remain responsive under normal load
2. **Resource Usage**: More predictable memory and goroutine usage
3. **Connection Handling**: Faster detection and cleanup of dead connections
4. **Performance**: Reduced lock contention and blocking operations
5. **Monitoring**: Better logging and health monitoring
## Monitoring
The server now provides better logging for:
- Client connection/disconnection events
- Health check results and statistics
- Resource usage patterns
- Error conditions and recovery actions
## Testing
I've created test scripts to verify the fixes:
### 1. Shutdown Test (`test_shutdown.sh`)
- Tests graceful shutdown behavior
- Verifies server responds to SIGTERM
- Confirms shutdown completes within reasonable time
### 2. Stress Test (`test_stress.sh`)
- Simulates conditions that previously caused freezing
- Creates multiple stable and unstable connections
- Tests rapid connection/disconnection patterns
- Monitors server responsiveness during stress
- Verifies shutdown works after stress conditions
### Usage:
```bash
# Test shutdown behavior
./test_shutdown.sh
# Test stability under stress
./test_stress.sh
```
## Future Improvements
1. **Metrics Endpoint**: Add HTTP endpoint for real-time metrics
2. **Connection Pooling**: Implement connection pooling for better resource management
3. **Circuit Breakers**: Add circuit breakers for failing operations
4. **Rate Limiting**: Enhanced rate limiting per IP/user

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 ComputerTech312
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

117
Makefile Normal file
View File

@@ -0,0 +1,117 @@
.PHONY: build run clean test lint fmt help
# Default target
.DEFAULT_GOAL := help
# Variables
BINARY_NAME=techircd
CMD_PATH=.
VERSION?=$(shell git describe --tags --always --dirty)
LDFLAGS=-ldflags "-X main.version=$(VERSION)"
## Build Commands
# Build the IRC server
build: ## Build the binary
go build $(LDFLAGS) -o $(BINARY_NAME) $(CMD_PATH)
# Build for different platforms
build-all: ## Build for multiple platforms
GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o $(BINARY_NAME)-linux-amd64 $(CMD_PATH)
GOOS=windows GOARCH=amd64 go build $(LDFLAGS) -o $(BINARY_NAME)-windows-amd64.exe $(CMD_PATH)
GOOS=darwin GOARCH=amd64 go build $(LDFLAGS) -o $(BINARY_NAME)-darwin-amd64 $(CMD_PATH)
GOOS=darwin GOARCH=arm64 go build $(LDFLAGS) -o $(BINARY_NAME)-darwin-arm64 $(CMD_PATH)
## Development Commands
# Run the server
run: build ## Build and run the server
./$(BINARY_NAME)
# Run with custom config
run-config: build ## Run with custom config file
./$(BINARY_NAME) -config configs/config.dev.json
# Install dependencies
deps: ## Download and install dependencies
go mod download
go mod tidy
# Format code
fmt: ## Format Go code
go fmt ./...
goimports -w -local github.com/ComputerTech312/TechIRCd .
# Lint code
lint: ## Run linters
golangci-lint run
# Fix linting issues
lint-fix: ## Fix auto-fixable linting issues
golangci-lint run --fix
## Testing Commands
# Run all tests
test: ## Run all tests
go test -v -race ./...
# Run tests with coverage
test-coverage: ## Run tests with coverage report
go test -v -race -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
# Run benchmarks
benchmark: ## Run benchmark tests
go test -bench=. -benchmem ./...
# Test with the test client
test-client: build ## Test with the simple client
go run tests/test_client.go
## Release Commands
# Create a release build
release: clean ## Create optimized release build
CGO_ENABLED=0 go build $(LDFLAGS) -a -installsuffix cgo -o $(BINARY_NAME) $(CMD_PATH)
# Tag a new version
tag: ## Tag a new version (usage: make tag VERSION=v1.0.1)
git tag -a $(VERSION) -m "Release $(VERSION)"
git push origin $(VERSION)
## Utility Commands
# Clean build artifacts
clean: ## Clean build artifacts
rm -f $(BINARY_NAME)*
rm -f coverage.out coverage.html
# Show git status and recent commits
status: ## Show git status and recent commits
@echo "Git Status:"
@git status --short
@echo "\nRecent Commits:"
@git log --oneline -10
# Install development tools
install-tools: ## Install development tools
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
go install golang.org/x/tools/cmd/goimports@latest
# Generate documentation
docs: ## Generate documentation
go doc -all > docs/API.md
# Show help
help: ## Show this help message
@echo 'Usage:'
@echo ' make <target>'
@echo ''
@echo 'Targets:'
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " %-15s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
go fmt ./...
# Run with race detection
run-race: build
go run -race *.go

217
README.md Normal file
View File

@@ -0,0 +1,217 @@
# TechIRCd
A modern, high-performance IRC server written in Go with comprehensive RFC compliance and advanced features.
## Features
### 🚀 **Core IRC Protocol**
- Full RFC 2812 compliance (Internet Relay Chat: Client Protocol)
- User registration and authentication
- Channel management with comprehensive modes
- Private messaging and notices
- **Ultra-flexible WHOIS system** with granular privacy controls
- WHO and NAMES commands
- Ping/Pong keepalive mechanism
### 🔍 **Revolutionary WHOIS System**
- **Granular Privacy Controls**: Configure exactly what information is visible to everyone, operators, or only the user themselves
- **User Modes Visibility**: Control who can see user modes (+i, +w, +s, etc.)
- **SSL Status Display**: Show secure connection status
- **Idle Time & Signon Time**: Configurable time information
- **Real Host vs Masked Host**: Smart hostname display based on permissions
- **Channel Privacy**: Hide secret/private channels with fine-grained control
- **Operator Information**: Show operator class and privileges
- **Services Integration**: Account name display for services
- **Client Information**: Show IRC client details
- **Custom Fields**: Add your own WHOIS fields
- See [WHOIS Configuration Guide](docs/WHOIS_CONFIGURATION.md) for details
### 👑 **Advanced Channel Management**
- **Operator Hierarchy**: Owners (~), Operators (@), Half-ops (%), Voice (+)
- **Channel Modes**:
- `+m` (moderated) - Only voiced users can speak
- `+n` (no external messages)
- `+t` (topic protection)
- `+i` (invite only)
- `+s` (secret channel)
- `+p` (private channel)
- `+k` (channel key/password)
- `+l` (user limit)
- `+b` (ban list)
- **Extended Ban System**: Support for quiet mode (`~q:mask`) and other extended ban types
### 🔐 **IRC Operator Features**
- Comprehensive operator authentication system
- **Hierarchical Operator Classes** with rank-based permissions and inheritance
- **Completely Customizable Rank Names** (Gaming, Corporate, Fantasy themes, etc.)
- **Server Notice Masks (SNOmasks)**:
- `+c` (connection notices)
- `+k` (kill notices)
- +o (oper notices)
- +x (ban/quiet notices)
- +f (flood notices)
- +n (nick change notices)
- +s (server notices)
- +d (debug notices)
- Operator commands:
- KILL - Disconnect users
- GLOBALNOTICE - Send notices to all users
- OPERWALL - Send messages to all operators
- WALLOPS - Send wallops messages
- REHASH - Reload configuration
- TRACE - Network trace information
- GODMODE - Toggle ultimate channel override powers
- STEALTH - Toggle invisibility to regular users
- Services/admin commands:
- CHGHOST - Change user's hostname
- SVSNICK - Services force nickname change
- SVSMODE - Services force mode change
- SAMODE - Services admin mode change
- SANICK - Services admin nickname change
- SAKICK - Services admin kick
- SAPART - Services admin part
- SAJOIN - Services admin join
- GLINE - Global network ban
- ZLINE - IP address ban
- KLINE - Local server ban
- SHUN - Services shun (silent ignore)
### 👤 **User Modes**
- `+i` (invisible) - Hide from WHO listings
- `+w` (wallops) - Receive wallops messages
- `+s` (server notices) - Receive server notices (opers only)
- `+o` (operator) - IRC operator status
- `+x` (host masking) - Hide real hostname
- `+B` (bot) - Mark as a bot
- `+z` (SSL) - Connected via SSL/TLS
- `+r` (registered) - Registered with services
### 🛡️ **Security & Stability**
- Advanced flood protection with operator exemption
- Connection timeout management
- Input validation and sanitization
- Panic recovery and error handling
- Resource monitoring and health checks
- Graceful shutdown capabilities
### ⚡ **Unique Features (Not Found in Other IRCds)**
#### 🌟 **God Mode (+G)**
- **Ultimate Channel Override**: Join any channel regardless of bans, limits, keys, or invite-only mode
- **Kick Immunity**: Cannot be kicked by any user
- **Mode Override**: Set any channel mode without operator privileges
- **Complete Bypass**: Ignore all channel restrictions and limitations
#### 👻 **Stealth Mode (+S)**
- **User Invisibility**: Completely hidden from regular users in WHO, NAMES, and WHOIS
- **Operator Visibility**: Other operators can still see stealth users
- **Covert Monitoring**: Watch channels without being detected
- **Security Operations**: Investigate issues invisibly
#### 🎨 **Customizable Rank Names**
- **Gaming Themes**: Cadet → Sergeant → Lieutenant → Captain → General
- **Corporate Themes**: Intern → Associate → Manager → Director → CEO
- **Fantasy Themes**: Apprentice → Guardian → Knight → Lord → King
- **Unlimited Creativity**: Create any rank system you can imagine
#### 🔍 **Revolutionary WHOIS**
- **Granular Privacy**: 15+ configurable information types
- **Three-Tier Permissions**: Everyone/Operators/Self visibility controls
- **Custom Fields**: Add your own WHOIS information
- **Complete Flexibility**: Control every aspect of user information display
### 📊 **Monitoring & Logging**
- Real-time health monitoring
- Memory usage tracking
- Goroutine count monitoring
- Performance metrics logging
- Configurable log levels and rotation
- Private messaging
- WHO/WHOIS commands
- Configurable server settings
- Extensible architecture
- Ping/Pong keep-alive mechanism
- Graceful shutdown handling
## Requirements
- Go 1.21 or higher
## Building
```bash
# Install dependencies
go mod tidy
# Build the server
go run tools/build.go -build
# Or use the traditional method
go build -o techircd
```
## Build Options
The `tools/build.go` script provides several build options:
```bash
# Build and run
go run tools/build.go -run
# Run tests
go run tools/build.go -test
# Format code
go run tools/build.go -fmt
# Cross-platform builds
go run tools/build.go -build-all
# Optimized release build
go run tools/build.go -release
# Clean build artifacts
go run tools/build.go -clean
# Show all options
go run tools/build.go -help
```
## Usage
```bash
./techircd
```
The server will start on localhost:6667 by default. You can configure the host and port in `config.go`.
## Configuration
Edit `config.go` to customize:
- Server host and port
- Server name and description
- Maximum connections
- Ping timeout
- Message of the Day (MOTD)
## Connecting
You can connect using any IRC client:
```
/server localhost 6667
```
Or use the included test client:
```bash
go run test_client.go
```
## Architecture
- `main.go` - Entry point and server initialization
- `server.go` - Main IRC server implementation with concurrent connection handling
- `client.go` - Client connection handling and state management
- `channel.go` - Channel management with thread-safe operations
- `commands.go` - IRC command implementations (NICK, USER, JOIN, PART, etc.)
- `config.go` - Server configuration structure
- `test_client.go` - Simple test client for development

127
STRESS_TEST_README.md Normal file
View File

@@ -0,0 +1,127 @@
# TechIRCd Stress Testing Tool
A comprehensive Python-based stress testing tool for TechIRCd IRC server.
## Features
- **Configurable Test Scenarios**: JSON-based configuration
- **Multiple Test Types**: Connection tests, message flooding, command spam
- **Gradual or Mass Operations**: Connect/disconnect clients gradually or all at once
- **Real-time Statistics**: Connection counts, message statistics, error tracking
- **Logging**: Detailed logging to file and console
- **SSL Support**: Test both plain and SSL connections
## Requirements
```bash
pip3 install asyncio # Usually included with Python 3.7+
```
## Usage
### Run All Test Scenarios
```bash
python3 stress_test.py
```
### Run Specific Scenario
```bash
python3 stress_test.py --scenario "Basic Connection Test"
```
### Use Custom Configuration
```bash
python3 stress_test.py --config my_config.json
```
## Configuration
Edit `stress_config.json` to customize tests:
### Server Settings
```json
{
"server": {
"host": "localhost", // IRC server hostname
"port": 6667, // IRC server port
"ssl": false, // Use SSL connection
"ssl_port": 6697, // SSL port
"nick_prefix": "StressBot", // Prefix for bot nicknames
"auto_join_channels": ["#test"] // Channels to auto-join
}
}
```
### Test Scenarios
Each scenario supports:
- `client_count`: Number of concurrent clients
- `duration`: How long to run the test (seconds)
- `connect_gradually`: Connect clients one by one vs all at once
- `connect_delay`: Delay between gradual connections
- `disconnect_gradually`: Disconnect gradually vs all at once
- `activities`: List of activities to perform during test
### Activity Types
1. **channel_flood**: Flood channels with messages
2. **private_flood**: Send private messages between clients
3. **join_part_spam**: Rapid JOIN/PART operations
4. **nick_change_spam**: Rapid nickname changes
5. **random_commands**: Send random IRC commands
## Pre-configured Test Scenarios
1. **Basic Connection Test** (10 clients, 30s)
- Simple connection and registration test
2. **Mass Connection Stress** (50 clients, 60s)
- Many simultaneous connections with channel flooding
3. **Gradual Connection Test** (30 clients, 45s)
- Gradual connection buildup with private messaging
4. **Heavy Activity Test** (25 clients, 90s)
- Multiple activity types running simultaneously
5. **Nick Change Spam** (15 clients, 30s)
- Rapid nickname change testing
6. **Channel Chaos Test** (20 clients, 60s)
- JOIN/PART spam with message flooding
7. **Maximum Load Test** (100 clients, 120s)
- Push server to maximum capacity
## Monitoring
- **Real-time logs**: Monitor progress in console
- **Log file**: Detailed logs saved to `stress_test.log`
- **Statistics**: Connection counts, message statistics, error tracking
## Example Output
```
2025-09-07 12:00:00 - INFO - Starting scenario: Basic Connection Test
2025-09-07 12:00:00 - INFO - Clients: 10, Duration: 30s
2025-09-07 12:00:00 - INFO - Mass connecting 10 clients...
2025-09-07 12:00:01 - INFO - Connected 10/10 clients
2025-09-07 12:00:03 - INFO - Registered 10/10 clients
2025-09-07 12:00:33 - INFO - Stats: 10 connected, 10 registered, 0 messages sent
2025-09-07 12:00:35 - INFO - Scenario Basic Connection Test completed
```
## Tips for Testing
1. **Start Small**: Begin with basic connection test
2. **Monitor Resources**: Watch TechIRCd memory/CPU usage
3. **Check Logs**: Review both stress test and TechIRCd logs
4. **Gradual Increase**: Increase client counts gradually
5. **Test Different Scenarios**: Each scenario tests different aspects
## Troubleshooting
- **Connection Refused**: Make sure TechIRCd is running
- **SSL Errors**: Check SSL configuration in both TechIRCd and test config
- **Memory Issues**: Reduce client count or test duration
- **Python Errors**: Ensure Python 3.7+ with asyncio support

BIN
TechIRCd Executable file

Binary file not shown.

836
channel.go Normal file
View File

@@ -0,0 +1,836 @@
package main
import (
"fmt"
"path/filepath"
"strings"
"sync"
"time"
)
type Channel struct {
name string
topic string
topicBy string
topicTime time.Time
clients map[string]*Client
operators map[string]*Client
halfops map[string]*Client
voices map[string]*Client
owners map[string]*Client
admins map[string]*Client
modes map[rune]bool
key string
limit int
banList []string
quietList []string // Users who can join but not speak (+q)
exceptList []string // Users exempt from bans (+e)
inviteList []string // Users who can join invite-only channels (+I)
created time.Time
// Advanced mode settings
floodSettings string // Flood protection settings (e.g., "10:5")
joinThrottle string // Join throttling settings (e.g., "3:10")
mu sync.RWMutex
}
func NewChannel(name string) *Channel {
return &Channel{
name: name,
clients: make(map[string]*Client),
operators: make(map[string]*Client),
halfops: make(map[string]*Client),
voices: make(map[string]*Client),
owners: make(map[string]*Client),
admins: make(map[string]*Client),
modes: make(map[rune]bool),
banList: make([]string, 0),
quietList: make([]string, 0),
exceptList: make([]string, 0),
inviteList: make([]string, 0),
created: time.Now(),
}
}
func (ch *Channel) Name() string {
ch.mu.RLock()
defer ch.mu.RUnlock()
return ch.name
}
func (ch *Channel) Topic() string {
ch.mu.RLock()
defer ch.mu.RUnlock()
return ch.topic
}
func (ch *Channel) TopicBy() string {
ch.mu.RLock()
defer ch.mu.RUnlock()
return ch.topicBy
}
func (ch *Channel) TopicTime() time.Time {
ch.mu.RLock()
defer ch.mu.RUnlock()
return ch.topicTime
}
func (ch *Channel) SetTopic(topic, by string) {
ch.mu.Lock()
defer ch.mu.Unlock()
ch.topic = topic
ch.topicBy = by
ch.topicTime = time.Now()
}
func (ch *Channel) AddClient(client *Client) {
ch.mu.Lock()
defer ch.mu.Unlock()
nick := strings.ToLower(client.Nick())
// Check if client is already in the channel
if _, exists := ch.clients[nick]; exists {
// Client already in channel, don't add again
return
}
ch.clients[nick] = client
client.AddChannel(ch)
// First user gets configured founder mode (default: operator)
if len(ch.clients) == 1 {
// Get the configured founder mode from server config
founderMode := "o" // Default fallback
if client.server != nil && client.server.config != nil {
founderMode = client.server.config.Channels.FounderMode
}
// Apply the appropriate mode based on configuration
switch founderMode {
case "q":
ch.owners[nick] = client
case "a":
ch.admins[nick] = client
case "o":
ch.operators[nick] = client
case "h":
ch.halfops[nick] = client
case "v":
ch.voices[nick] = client
default:
// Invalid config, fallback to operator
ch.operators[nick] = client
}
}
}
func (ch *Channel) RemoveClient(client *Client) {
ch.mu.Lock()
defer ch.mu.Unlock()
nick := strings.ToLower(client.Nick())
delete(ch.clients, nick)
delete(ch.operators, nick)
delete(ch.halfops, nick)
delete(ch.voices, nick)
delete(ch.owners, nick)
delete(ch.admins, nick)
client.RemoveChannel(ch.name)
}
func (ch *Channel) HasClient(client *Client) bool {
ch.mu.RLock()
defer ch.mu.RUnlock()
_, exists := ch.clients[strings.ToLower(client.Nick())]
return exists
}
func (ch *Channel) IsOperator(client *Client) bool {
ch.mu.RLock()
defer ch.mu.RUnlock()
_, exists := ch.operators[strings.ToLower(client.Nick())]
return exists
}
func (ch *Channel) IsVoice(client *Client) bool {
ch.mu.RLock()
defer ch.mu.RUnlock()
_, exists := ch.voices[strings.ToLower(client.Nick())]
return exists
}
func (ch *Channel) IsHalfop(client *Client) bool {
ch.mu.RLock()
defer ch.mu.RUnlock()
_, exists := ch.halfops[strings.ToLower(client.Nick())]
return exists
}
func (ch *Channel) IsOwner(client *Client) bool {
ch.mu.RLock()
defer ch.mu.RUnlock()
_, exists := ch.owners[strings.ToLower(client.Nick())]
return exists
}
func (ch *Channel) IsQuieted(client *Client) bool {
ch.mu.RLock()
defer ch.mu.RUnlock()
return ch.isQuietedUnsafe(client)
}
func (ch *Channel) isQuietedUnsafe(client *Client) bool {
hostmask := fmt.Sprintf("%s!%s@%s", client.Nick(), client.User(), client.Host())
for _, quiet := range ch.quietList {
if ch.matchesBanMask(client, quiet, hostmask) {
return true
}
}
return false
}
func (ch *Channel) SetOperator(client *Client, isOp bool) {
ch.mu.Lock()
defer ch.mu.Unlock()
nick := strings.ToLower(client.Nick())
if isOp {
ch.operators[nick] = client
} else {
delete(ch.operators, nick)
}
}
func (ch *Channel) SetVoice(client *Client, hasVoice bool) {
ch.mu.Lock()
defer ch.mu.Unlock()
nick := strings.ToLower(client.Nick())
if hasVoice {
ch.voices[nick] = client
} else {
delete(ch.voices, nick)
}
}
func (ch *Channel) SetHalfop(client *Client, isHalfop bool) {
ch.mu.Lock()
defer ch.mu.Unlock()
nick := strings.ToLower(client.Nick())
if isHalfop {
ch.halfops[nick] = client
} else {
delete(ch.halfops, nick)
}
}
func (ch *Channel) SetOwner(client *Client, isOwner bool) {
ch.mu.Lock()
defer ch.mu.Unlock()
nick := strings.ToLower(client.Nick())
if isOwner {
ch.owners[nick] = client
} else {
delete(ch.owners, nick)
}
}
func (ch *Channel) IsAdmin(client *Client) bool {
ch.mu.RLock()
defer ch.mu.RUnlock()
_, exists := ch.admins[strings.ToLower(client.Nick())]
return exists
}
func (ch *Channel) SetAdmin(client *Client, isAdmin bool) {
ch.mu.Lock()
defer ch.mu.Unlock()
nick := strings.ToLower(client.Nick())
if isAdmin {
ch.admins[nick] = client
} else {
delete(ch.admins, nick)
}
}
func (ch *Channel) GetClients() []*Client {
ch.mu.RLock()
defer ch.mu.RUnlock()
clients := make([]*Client, 0, len(ch.clients))
for _, client := range ch.clients {
clients = append(clients, client)
}
return clients
}
func (ch *Channel) GetClientCount() int {
ch.mu.RLock()
defer ch.mu.RUnlock()
return len(ch.clients)
}
func (ch *Channel) UserCount() int {
return ch.GetClientCount()
}
func (ch *Channel) Broadcast(message string, exclude *Client) {
ch.mu.RLock()
defer ch.mu.RUnlock()
for _, client := range ch.clients {
if exclude != nil && client.Nick() == exclude.Nick() {
continue
}
client.SendMessage(message)
}
}
func (ch *Channel) BroadcastFrom(source, message string, exclude *Client) {
ch.mu.RLock()
defer ch.mu.RUnlock()
for _, client := range ch.clients {
if exclude != nil && client.Nick() == exclude.Nick() {
continue
}
client.SendFrom(source, message)
}
}
func (ch *Channel) HasMode(mode rune) bool {
ch.mu.RLock()
defer ch.mu.RUnlock()
return ch.modes[mode]
}
func (ch *Channel) SetMode(mode rune, set bool) {
ch.mu.Lock()
defer ch.mu.Unlock()
if set {
ch.modes[mode] = true
} else {
delete(ch.modes, mode)
}
}
func (ch *Channel) GetModes() string {
ch.mu.RLock()
defer ch.mu.RUnlock()
var modes []rune
for mode := range ch.modes {
modes = append(modes, mode)
}
if len(modes) == 0 {
return ""
}
return "+" + string(modes)
}
func (ch *Channel) CanSendMessage(client *Client) bool {
ch.mu.RLock()
defer ch.mu.RUnlock()
// Check if user is quieted first
if ch.isQuietedUnsafe(client) {
// Only owners, operators, and halfops can speak when quieted
nick := strings.ToLower(client.Nick())
_, isOwner := ch.owners[nick]
_, isOp := ch.operators[nick]
_, isHalfop := ch.halfops[nick]
if !isOwner && !isOp && !isHalfop {
return false
}
}
// If channel is not moderated, anyone in the channel can send
if !ch.modes['m'] {
return true
}
// In moderated channels, only owners, operators, halfops and voiced users can send messages
nick := strings.ToLower(client.Nick())
_, isOwner := ch.owners[nick]
_, isOp := ch.operators[nick]
_, isHalfop := ch.halfops[nick]
_, hasVoice := ch.voices[nick]
return isOwner || isOp || isHalfop || hasVoice
}
func (ch *Channel) Key() string {
ch.mu.RLock()
defer ch.mu.RUnlock()
return ch.key
}
func (ch *Channel) SetKey(key string) {
ch.mu.Lock()
defer ch.mu.Unlock()
ch.key = key
}
func (ch *Channel) Limit() int {
ch.mu.RLock()
defer ch.mu.RUnlock()
return ch.limit
}
func (ch *Channel) SetLimit(limit int) {
ch.mu.Lock()
defer ch.mu.Unlock()
ch.limit = limit
}
func (ch *Channel) GetNamesReply() string {
ch.mu.RLock()
defer ch.mu.RUnlock()
var names []string
for _, client := range ch.clients {
prefix := ""
if ch.IsOwner(client) {
prefix = "~"
} else if ch.IsOperator(client) {
prefix = "@"
} else if ch.IsHalfop(client) {
prefix = "%"
} else if ch.IsVoice(client) {
prefix = "+"
}
names = append(names, prefix+client.Nick())
}
return strings.Join(names, " ")
}
func (ch *Channel) CanSpeak(client *Client) bool {
ch.mu.RLock()
defer ch.mu.RUnlock()
// If channel is not moderated, anyone can speak
if !ch.modes['m'] {
return true
}
// Operators and voiced users can always speak
return ch.IsOperator(client) || ch.IsVoice(client)
}
func (ch *Channel) CanJoin(client *Client, key string) bool {
ch.mu.RLock()
defer ch.mu.RUnlock()
// Check if invite-only
if ch.modes['i'] {
// Check invite list
for _, mask := range ch.inviteList {
if ch.matchesMask(client.Prefix(), mask) {
return true
}
}
return false
}
// Check key
if ch.modes['k'] && ch.key != key {
return false
}
// Check limit
if ch.modes['l'] && len(ch.clients) >= ch.limit {
return false
}
// Check ban list
for _, mask := range ch.banList {
if ch.matchesMask(client.Prefix(), mask) {
// Check exception list
for _, exceptMask := range ch.exceptList {
if ch.matchesMask(client.Prefix(), exceptMask) {
return true
}
}
return false
}
}
return true
}
func (ch *Channel) matchesMask(target, mask string) bool {
// Simple mask matching - should be enhanced for production
return strings.Contains(strings.ToLower(target), strings.ToLower(mask))
}
func (ch *Channel) AddBan(mask string) {
ch.mu.Lock()
defer ch.mu.Unlock()
ch.banList = append(ch.banList, mask)
}
func (ch *Channel) RemoveBan(mask string) {
ch.mu.Lock()
defer ch.mu.Unlock()
for i, ban := range ch.banList {
if ban == mask {
ch.banList = append(ch.banList[:i], ch.banList[i+1:]...)
break
}
}
}
func (ch *Channel) GetBans() []string {
ch.mu.RLock()
defer ch.mu.RUnlock()
bans := make([]string, len(ch.banList))
copy(bans, ch.banList)
return bans
}
// Extended ban list management
func (ch *Channel) AddQuiet(mask string) {
ch.mu.Lock()
defer ch.mu.Unlock()
ch.quietList = append(ch.quietList, mask)
}
func (ch *Channel) RemoveQuiet(mask string) {
ch.mu.Lock()
defer ch.mu.Unlock()
for i, quiet := range ch.quietList {
if quiet == mask {
ch.quietList = append(ch.quietList[:i], ch.quietList[i+1:]...)
break
}
}
}
func (ch *Channel) GetQuiets() []string {
ch.mu.RLock()
defer ch.mu.RUnlock()
quiets := make([]string, len(ch.quietList))
copy(quiets, ch.quietList)
return quiets
}
func (ch *Channel) AddExcept(mask string) {
ch.mu.Lock()
defer ch.mu.Unlock()
ch.exceptList = append(ch.exceptList, mask)
}
func (ch *Channel) RemoveExcept(mask string) {
ch.mu.Lock()
defer ch.mu.Unlock()
for i, except := range ch.exceptList {
if except == mask {
ch.exceptList = append(ch.exceptList[:i], ch.exceptList[i+1:]...)
break
}
}
}
func (ch *Channel) GetExcepts() []string {
ch.mu.RLock()
defer ch.mu.RUnlock()
excepts := make([]string, len(ch.exceptList))
copy(excepts, ch.exceptList)
return excepts
}
func (ch *Channel) AddInviteException(mask string) {
ch.mu.Lock()
defer ch.mu.Unlock()
ch.inviteList = append(ch.inviteList, mask)
}
func (ch *Channel) RemoveInviteException(mask string) {
ch.mu.Lock()
defer ch.mu.Unlock()
for i, invite := range ch.inviteList {
if invite == mask {
ch.inviteList = append(ch.inviteList[:i], ch.inviteList[i+1:]...)
break
}
}
}
func (ch *Channel) GetInviteExceptions() []string {
ch.mu.RLock()
defer ch.mu.RUnlock()
invites := make([]string, len(ch.inviteList))
copy(invites, ch.inviteList)
return invites
}
// IsExempt checks if a client is exempt from bans
func (ch *Channel) IsExempt(client *Client) bool {
ch.mu.RLock()
defer ch.mu.RUnlock()
hostmask := fmt.Sprintf("%s!%s@%s", client.Nick(), client.User(), client.Host())
for _, except := range ch.exceptList {
if ch.matchesBanMask(client, except, hostmask) {
return true
}
}
return false
}
// CanJoinInviteOnly checks if a client can join an invite-only channel
func (ch *Channel) CanJoinInviteOnly(client *Client) bool {
ch.mu.RLock()
defer ch.mu.RUnlock()
hostmask := fmt.Sprintf("%s!%s@%s", client.Nick(), client.User(), client.Host())
for _, invite := range ch.inviteList {
if ch.matchesBanMask(client, invite, hostmask) {
return true
}
}
return false
}
func (ch *Channel) Created() time.Time {
ch.mu.RLock()
defer ch.mu.RUnlock()
return ch.created
}
// IsBanned checks if a client matches any ban mask in the channel
func (ch *Channel) IsBanned(client *Client) bool {
ch.mu.RLock()
defer ch.mu.RUnlock()
// Check exemptions first - if exempt, not banned
if ch.isExemptUnsafe(client) {
return false
}
hostmask := fmt.Sprintf("%s!%s@%s", client.Nick(), client.User(), client.Host())
for _, ban := range ch.banList {
if ch.matchesBanMask(client, ban, hostmask) {
return true
}
}
return false
}
// isExemptUnsafe checks exemptions without locking (internal use)
func (ch *Channel) isExemptUnsafe(client *Client) bool {
hostmask := fmt.Sprintf("%s!%s@%s", client.Nick(), client.User(), client.Host())
for _, except := range ch.exceptList {
if ch.matchesBanMask(client, except, hostmask) {
return true
}
}
return false
}
// matchesBanMask checks if a client matches a ban mask (supports extended bans)
func (ch *Channel) matchesBanMask(client *Client, banMask, hostmask string) bool {
// Check for extended ban format: ~type:parameter or ~type parameter
if strings.HasPrefix(banMask, "~") {
return ch.matchesExtendedBan(client, banMask)
}
// Traditional hostmask ban
return matchWildcard(banMask, hostmask)
}
// matchesExtendedBan handles extended ban types
func (ch *Channel) matchesExtendedBan(client *Client, extban string) bool {
if len(extban) < 2 || extban[0] != '~' {
return false
}
// Parse ~type:parameter or ~type parameter format
var banType string
var parameter string
if strings.Contains(extban, ":") {
// Format: ~type:parameter
parts := strings.SplitN(extban[1:], ":", 2)
banType = parts[0]
if len(parts) > 1 {
parameter = parts[1]
}
} else {
// Format: ~type parameter (space separated)
parts := strings.Fields(extban[1:])
if len(parts) > 0 {
banType = parts[0]
if len(parts) > 1 {
parameter = strings.Join(parts[1:], " ")
}
}
}
switch banType {
case "a": // Account ban: ~a:accountname or ~a accountname
if parameter == "" {
// ~a with no parameter bans unregistered users
return client.account == ""
}
// ~a:account bans specific account
return client.account == parameter
case "c": // Channel ban: ~c:#channel - ban users in another channel
if parameter == "" || !strings.HasPrefix(parameter, "#") {
return false
}
targetChannel := client.server.GetChannel(parameter)
return targetChannel != nil && targetChannel.HasClient(client)
case "j": // Join prevent: ~j:#channel - prevent joining if in another channel
if parameter == "" || !strings.HasPrefix(parameter, "#") {
return false
}
targetChannel := client.server.GetChannel(parameter)
return targetChannel != nil && targetChannel.HasClient(client)
case "n": // Nick pattern ban: ~n:pattern
if parameter == "" {
return false
}
return matchWildcard(parameter, client.Nick())
case "q": // Quiet ban: ~q:mask - this is a special case for quiet functionality
// When used in regular ban list, ~q acts as a quiet
if parameter == "" {
// ~q with no parameter quiets everyone
return true
}
// ~q:mask quiets matching users - check against hostmask pattern
hostmask := fmt.Sprintf("%s!%s@%s", client.Nick(), client.User(), client.Host())
return matchWildcard(parameter, hostmask)
case "r": // Real name ban: ~r:pattern
if parameter == "" {
return false
}
return matchWildcard(parameter, client.realname)
case "s": // Server ban: ~s:servername
if parameter == "" {
return false
}
return matchWildcard(parameter, client.server.config.Server.Name)
case "o": // Operator ban: ~o (bans all opers)
return client.IsOper()
case "z": // Non-SSL ban: ~z (bans non-SSL users)
return !client.ssl
case "Z": // SSL-only ban: ~Z (bans SSL users)
return client.ssl
case "u": // Username pattern ban: ~u:pattern
if parameter == "" {
return false
}
return matchWildcard(parameter, client.User())
case "h": // Hostname pattern ban: ~h:pattern
if parameter == "" {
return false
}
return matchWildcard(parameter, client.Host())
case "i": // IP ban: ~i:ip/cidr
if parameter == "" {
return false
}
// Simple IP matching for now (could be enhanced with CIDR)
return matchWildcard(parameter, client.Host())
case "R": // Registered only: ~R (bans unregistered users)
return client.account == ""
case "m": // Mute ban: ~m:mask - similar to quiet
if parameter == "" {
return true
}
hostmask := fmt.Sprintf("%s!%s@%s", client.Nick(), client.User(), client.Host())
return matchWildcard(parameter, hostmask)
default:
// Unknown extended ban type
return false
}
}
// IsInvited checks if a client is on the invite list for the channel
func (ch *Channel) IsInvited(client *Client) bool {
ch.mu.RLock()
defer ch.mu.RUnlock()
hostmask := fmt.Sprintf("%s!%s@%s", client.Nick(), client.User(), client.Host())
for _, invite := range ch.inviteList {
if matchWildcard(invite, hostmask) {
return true
}
}
return false
}
// matchWildcard checks if a pattern with wildcards (* and ?) matches a string
func matchWildcard(pattern, str string) bool {
matched, _ := filepath.Match(strings.ToLower(pattern), strings.ToLower(str))
return matched
}
// SetFloodSettings sets the flood protection settings
func (ch *Channel) SetFloodSettings(settings string) {
ch.mu.Lock()
defer ch.mu.Unlock()
ch.floodSettings = settings
}
// GetFloodSettings returns the flood protection settings
func (ch *Channel) GetFloodSettings() string {
ch.mu.RLock()
defer ch.mu.RUnlock()
return ch.floodSettings
}
// SetJoinThrottle sets the join throttling settings
func (ch *Channel) SetJoinThrottle(settings string) {
ch.mu.Lock()
defer ch.mu.Unlock()
ch.joinThrottle = settings
}
// GetJoinThrottle returns the join throttling settings
func (ch *Channel) GetJoinThrottle() string {
ch.mu.RLock()
defer ch.mu.RUnlock()
return ch.joinThrottle
}

1223
client.go Normal file

File diff suppressed because it is too large Load Diff

106
cmd/techircd/main.go Normal file
View File

@@ -0,0 +1,106 @@
package main
import (
"flag"
"log"
"os"
"os/signal"
"syscall"
)
// Config represents the server configuration.
// You should replace the fields below with your actual configuration structure.
type Config struct {
Server struct {
Version string
Listen struct {
Host string
Port int
EnableSSL bool
}
SSL struct {
CertFile string
KeyFile string
}
}
}
// Dummy implementations for missing functions.
// Replace these with your actual implementations.
func ParseConfig(file *os.File) (*Config, error) {
return &Config{}, nil
}
func DefaultConfig() *Config {
return &Config{}
}
func SaveConfig(cfg *Config, path string) error {
return nil
}
func (c *Config) Validate() error {
return nil
}
func (c *Config) SanitizeConfig() {}
func NewServer(cfg *Config) *Server {
return &Server{}
}
type Server struct{}
func (s *Server) Shutdown() {}
func (s *Server) Start() error { return nil }
func LoadConfig(path string) (*Config, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
return ParseConfig(file)
}
func main() {
// Parse command line flags
configFile := flag.String("config", "config.json", "Path to configuration file")
flag.Parse()
// Load configuration
config, err := LoadConfig(*configFile)
if err != nil {
log.Printf("Failed to load config from %s, using defaults: %v", *configFile, err)
// Create default config if file doesn't exist
config = DefaultConfig()
if err := SaveConfig(config, *configFile); err != nil {
log.Printf("Failed to save default config: %v", err)
}
}
// Validate and sanitize configuration
if err := config.Validate(); err != nil {
log.Fatalf("Configuration validation failed: %v", err)
}
config.SanitizeConfig()
log.Println("Configuration validated successfully")
// Create and start the server
server := NewServer(config)
// Handle graceful shutdown
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
log.Println("Shutting down server...")
server.Shutdown()
os.Exit(0)
}()
// Start the server
log.Printf("Starting TechIRCd %s on %s:%d", config.Server.Version, config.Server.Listen.Host, config.Server.Listen.Port)
if config.Server.Listen.EnableSSL {
log.Printf("SSL enabled with cert: %s, key: %s", config.Server.SSL.CertFile, config.Server.SSL.KeyFile)
}
if err := server.Start(); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}

4098
commands.go Normal file

File diff suppressed because it is too large Load Diff

407
config.go Normal file
View File

@@ -0,0 +1,407 @@
package main
import (
"encoding/json"
"fmt"
"os"
"time"
)
type Config struct {
Server struct {
Name string `json:"name"`
Network string `json:"network"`
Description string `json:"description"`
Version string `json:"version"`
AdminInfo string `json:"admin_info"`
Listen struct {
Host string `json:"host"`
Port int `json:"port"`
SSLPort int `json:"ssl_port"`
EnableSSL bool `json:"enable_ssl"`
} `json:"listen"`
SSL struct {
CertFile string `json:"cert_file"`
KeyFile string `json:"key_file"`
RequireSSL bool `json:"require_ssl"`
} `json:"ssl"`
} `json:"server"`
Limits struct {
MaxClients int `json:"max_clients"`
MaxChannels int `json:"max_channels"`
MaxChannelUsers int `json:"max_channel_users"`
MaxNickLength int `json:"max_nick_length"`
MaxChannelLength int `json:"max_channel_length"`
MaxTopicLength int `json:"max_topic_length"`
MaxKickLength int `json:"max_kick_length"`
MaxAwayLength int `json:"max_away_length"`
PingTimeout int `json:"ping_timeout"`
RegistrationTimeout int `json:"registration_timeout"`
FloodLines int `json:"flood_lines"`
FloodSeconds int `json:"flood_seconds"`
} `json:"limits"`
Features struct {
EnableOper bool `json:"enable_oper"`
EnableServices bool `json:"enable_services"`
EnableModes bool `json:"enable_modes"`
EnableCTCP bool `json:"enable_ctcp"`
EnableDCC bool `json:"enable_dcc"`
CaseMapping string `json:"case_mapping"`
} `json:"features"`
NickChangeNotification struct {
ShowToEveryone bool `json:"to_everyone"`
ShowToOpers bool `json:"to_opers"`
ShowToSelf bool `json:"to_self"`
} `json:"nick_change_notification"`
Privacy struct {
HideHostsFromUsers bool `json:"hide_hosts_from_users"`
OperBypassHostHide bool `json:"oper_bypass_host_hide"`
MaskedHostSuffix string `json:"masked_host_suffix"`
} `json:"privacy"`
WhoisFeatures struct {
// Basic information visibility
ShowUserModes struct {
ToEveryone bool `json:"to_everyone"`
ToOpers bool `json:"to_opers"`
ToSelf bool `json:"to_self"`
} `json:"show_user_modes"`
ShowSSLStatus struct {
ToEveryone bool `json:"to_everyone"`
ToOpers bool `json:"to_opers"`
ToSelf bool `json:"to_self"`
} `json:"show_ssl_status"`
ShowIdleTime struct {
ToEveryone bool `json:"to_everyone"`
ToOpers bool `json:"to_opers"`
ToSelf bool `json:"to_self"`
} `json:"show_idle_time"`
ShowSignonTime struct {
ToEveryone bool `json:"to_everyone"`
ToOpers bool `json:"to_opers"`
ToSelf bool `json:"to_self"`
} `json:"show_signon_time"`
ShowRealHost struct {
ToEveryone bool `json:"to_everyone"`
ToOpers bool `json:"to_opers"`
ToSelf bool `json:"to_self"`
} `json:"show_real_host"`
ShowChannels struct {
ToEveryone bool `json:"to_everyone"`
ToOpers bool `json:"to_opers"`
ToSelf bool `json:"to_self"`
HideSecret bool `json:"hide_secret_channels"`
HidePrivate bool `json:"hide_private_channels"`
ShowMembership bool `json:"show_membership_levels"`
} `json:"show_channels"`
ShowOperClass struct {
ToEveryone bool `json:"to_everyone"`
ToOpers bool `json:"to_opers"`
ToSelf bool `json:"to_self"`
} `json:"show_oper_class"`
ShowClientInfo struct {
ToEveryone bool `json:"to_everyone"`
ToOpers bool `json:"to_opers"`
ToSelf bool `json:"to_self"`
} `json:"show_client_info"`
ShowAccountName struct {
ToEveryone bool `json:"to_everyone"`
ToOpers bool `json:"to_opers"`
ToSelf bool `json:"to_self"`
} `json:"show_account_name"`
// Advanced/unique features
ShowActivityStats bool `json:"show_activity_stats"`
ShowGitHubIntegration bool `json:"show_github_integration"`
ShowGeolocation bool `json:"show_geolocation"`
ShowPerformanceStats bool `json:"show_performance_stats"`
ShowDeviceInfo bool `json:"show_device_info"`
ShowSocialGraph bool `json:"show_social_graph"`
ShowSecurityScore bool `json:"show_security_score"`
// Custom fields
CustomFields []struct {
Name string `json:"name"`
ToEveryone bool `json:"to_everyone"`
ToOpers bool `json:"to_opers"`
ToSelf bool `json:"to_self"`
Format string `json:"format"`
Description string `json:"description"`
} `json:"custom_fields"`
} `json:"whois_features"`
Channels struct {
DefaultModes string `json:"default_modes"`
AutoJoin []string `json:"auto_join"`
AdminChannels []string `json:"admin_channels"`
FounderMode string `json:"founder_mode"` // Mode given to first user joining a channel: "o", "a", "q"
AllowedModes struct {
Voice bool `json:"voice"` // +v
Halfop bool `json:"halfop"` // +h
Operator bool `json:"operator"` // +o
Admin bool `json:"admin"` // +a
Owner bool `json:"owner"` // +q
} `json:"allowed_modes"`
Modes struct {
BanListSize int `json:"ban_list_size"`
ExceptListSize int `json:"except_list_size"`
InviteListSize int `json:"invite_list_size"`
} `json:"modes"`
} `json:"channels"`
Opers []struct {
Name string `json:"name"`
Password string `json:"password"`
Host string `json:"host"`
Class string `json:"class"`
Flags []string `json:"flags"`
} `json:"opers"`
OperConfig struct {
ConfigFile string `json:"config_file"`
Enable bool `json:"enable"`
} `json:"oper_config"`
MOTD []string `json:"motd"`
Linking struct {
Enable bool `json:"enable"`
ServerPort int `json:"server_port"`
Password string `json:"password"`
Hub bool `json:"hub"`
AutoConnect bool `json:"auto_connect"`
Links []struct {
Name string `json:"name"`
Host string `json:"host"`
Port int `json:"port"`
Password string `json:"password"`
AutoConnect bool `json:"auto_connect"`
Hub bool `json:"hub"`
Description string `json:"description"`
} `json:"links"`
} `json:"linking"`
Logging struct {
Level string `json:"level"`
File string `json:"file"`
MaxSize int `json:"max_size"`
MaxBackups int `json:"max_backups"`
MaxAge int `json:"max_age"`
} `json:"logging"`
}
func LoadConfig(filename string) (*Config, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %v", err)
}
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse config file: %v", err)
}
return &config, nil
}
func (c *Config) PingTimeoutDuration() time.Duration {
return time.Duration(c.Limits.PingTimeout) * time.Second
}
func (c *Config) RegistrationTimeoutDuration() time.Duration {
return time.Duration(c.Limits.RegistrationTimeout) * time.Second
}
func DefaultConfig() *Config {
return &Config{
Server: struct {
Name string `json:"name"`
Network string `json:"network"`
Description string `json:"description"`
Version string `json:"version"`
AdminInfo string `json:"admin_info"`
Listen struct {
Host string `json:"host"`
Port int `json:"port"`
SSLPort int `json:"ssl_port"`
EnableSSL bool `json:"enable_ssl"`
} `json:"listen"`
SSL struct {
CertFile string `json:"cert_file"`
KeyFile string `json:"key_file"`
RequireSSL bool `json:"require_ssl"`
} `json:"ssl"`
}{
Name: "TechIRCd",
Network: "TechNet",
Description: "A modern IRC server written in Go",
Version: "1.0.0",
AdminInfo: "admin@example.com",
Listen: struct {
Host string `json:"host"`
Port int `json:"port"`
SSLPort int `json:"ssl_port"`
EnableSSL bool `json:"enable_ssl"`
}{
Host: "localhost",
Port: 6667,
SSLPort: 6697,
EnableSSL: false,
},
SSL: struct {
CertFile string `json:"cert_file"`
KeyFile string `json:"key_file"`
RequireSSL bool `json:"require_ssl"`
}{
CertFile: "server.crt",
KeyFile: "server.key",
RequireSSL: false,
},
},
Limits: struct {
MaxClients int `json:"max_clients"`
MaxChannels int `json:"max_channels"`
MaxChannelUsers int `json:"max_channel_users"`
MaxNickLength int `json:"max_nick_length"`
MaxChannelLength int `json:"max_channel_length"`
MaxTopicLength int `json:"max_topic_length"`
MaxKickLength int `json:"max_kick_length"`
MaxAwayLength int `json:"max_away_length"`
PingTimeout int `json:"ping_timeout"`
RegistrationTimeout int `json:"registration_timeout"`
FloodLines int `json:"flood_lines"`
FloodSeconds int `json:"flood_seconds"`
}{
MaxClients: 1000,
MaxChannels: 100,
MaxChannelUsers: 500,
MaxNickLength: 30,
MaxChannelLength: 50,
MaxTopicLength: 307,
MaxKickLength: 307,
MaxAwayLength: 307,
PingTimeout: 300,
RegistrationTimeout: 60,
FloodLines: 20,
FloodSeconds: 10,
},
Features: struct {
EnableOper bool `json:"enable_oper"`
EnableServices bool `json:"enable_services"`
EnableModes bool `json:"enable_modes"`
EnableCTCP bool `json:"enable_ctcp"`
EnableDCC bool `json:"enable_dcc"`
CaseMapping string `json:"case_mapping"`
}{
EnableOper: true,
EnableServices: false,
EnableModes: true,
EnableCTCP: true,
EnableDCC: false,
CaseMapping: "rfc1459",
},
Channels: struct {
DefaultModes string `json:"default_modes"`
AutoJoin []string `json:"auto_join"`
AdminChannels []string `json:"admin_channels"`
FounderMode string `json:"founder_mode"` // Mode given to first user joining a channel: "o", "a", "q"
AllowedModes struct {
Voice bool `json:"voice"` // +v
Halfop bool `json:"halfop"` // +h
Operator bool `json:"operator"` // +o
Admin bool `json:"admin"` // +a
Owner bool `json:"owner"` // +q
} `json:"allowed_modes"`
Modes struct {
BanListSize int `json:"ban_list_size"`
ExceptListSize int `json:"except_list_size"`
InviteListSize int `json:"invite_list_size"`
} `json:"modes"`
}{
DefaultModes: "+nt",
AutoJoin: []string{"#general"},
AdminChannels: []string{"#admin"},
FounderMode: "o", // Default to operator mode for channel founders
AllowedModes: struct {
Voice bool `json:"voice"`
Halfop bool `json:"halfop"`
Operator bool `json:"operator"`
Admin bool `json:"admin"`
Owner bool `json:"owner"`
}{
Voice: true,
Halfop: true,
Operator: true,
Admin: true,
Owner: true,
},
Modes: struct {
BanListSize int `json:"ban_list_size"`
ExceptListSize int `json:"except_list_size"`
InviteListSize int `json:"invite_list_size"`
}{
BanListSize: 100,
ExceptListSize: 100,
InviteListSize: 100,
},
},
Opers: []struct {
Name string `json:"name"`
Password string `json:"password"`
Host string `json:"host"`
Class string `json:"class"`
Flags []string `json:"flags"`
}{
{
Name: "admin",
Password: "changeme",
Host: "*@localhost",
Class: "admin",
Flags: []string{"global_kill", "remote", "connect", "squit"},
},
},
MOTD: []string{
"Welcome to TechIRCd!",
"A modern IRC server written in Go",
"Enjoy your stay on TechNet!",
},
Logging: struct {
Level string `json:"level"`
File string `json:"file"`
MaxSize int `json:"max_size"`
MaxBackups int `json:"max_backups"`
MaxAge int `json:"max_age"`
}{
Level: "info",
File: "techircd.log",
MaxSize: 100,
MaxBackups: 3,
MaxAge: 28,
},
}
}
func SaveConfig(config *Config, filename string) error {
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal config: %v", err)
}
if err := os.WriteFile(filename, data, 0644); err != nil {
return fmt.Errorf("failed to write config file: %v", err)
}
return nil
}

163
config.json Normal file
View File

@@ -0,0 +1,163 @@
{
"server": {
"name": "TechIRCd",
"network": "TechNet",
"description": "A modern IRC server written in Go",
"version": "1.0.0",
"admin_info": "admin@example.com",
"listen": {
"host": "localhost",
"port": 6667,
"ssl_port": 6697,
"enable_ssl": false
},
"ssl": {
"cert_file": "server.crt",
"key_file": "server.key",
"require_ssl": false
}
},
"limits": {
"max_clients": 1000,
"max_channels": 100,
"max_channel_users": 500,
"max_nick_length": 30,
"max_channel_length": 50,
"max_topic_length": 307,
"max_kick_length": 307,
"max_away_length": 307,
"ping_timeout": 300,
"registration_timeout": 60,
"flood_lines": 20,
"flood_seconds": 10
},
"features": {
"enable_oper": true,
"enable_services": false,
"enable_modes": true,
"enable_ctcp": true,
"enable_dcc": false,
"case_mapping": "rfc1459"
},
"nick_change_notification": {
"to_everyone": true,
"to_opers": true,
"to_self": true
},
"channels": {
"default_modes": "+nt",
"auto_join": [
"#general"
],
"admin_channels": [
"#admin"
],
"founder_mode": "o",
"allowed_modes": {
"voice": true,
"halfop": true,
"operator": true,
"admin": true,
"owner": true
},
"modes": {
"ban_list_size": 100,
"except_list_size": 100,
"invite_list_size": 100
}
},
"opers": [
{
"name": "admin",
"password": "changeme",
"host": "*@localhost",
"class": "admin",
"flags": [
"global_kill",
"remote",
"connect",
"squit"
]
}
],
"linking": {
"enable": true,
"server_port": 6697,
"password": "linkpassword123",
"hub": false,
"auto_connect": false,
"links": [
{
"name": "hub.technet.org",
"host": "127.0.0.1",
"port": 6697,
"password": "linkpassword123",
"auto_connect": false,
"hub": true,
"description": "TechNet Hub Server"
}
]
},
"whois_features": {
"show_user_modes": {
"to_everyone": false,
"to_opers": true,
"to_self": true
},
"show_ssl_status": {
"to_everyone": true,
"to_opers": true,
"to_self": true
},
"show_idle_time": {
"to_everyone": false,
"to_opers": true,
"to_self": true
},
"show_signon_time": {
"to_everyone": false,
"to_opers": true,
"to_self": true
},
"show_real_host": {
"to_everyone": false,
"to_opers": true,
"to_self": true
},
"show_channels": {
"to_everyone": true,
"to_opers": true,
"to_self": true,
"hide_secret_channels": true,
"hide_private_channels": false,
"show_membership_levels": true
},
"show_oper_class": {
"to_everyone": false,
"to_opers": true,
"to_self": true
},
"show_client_info": {
"to_everyone": false,
"to_opers": true,
"to_self": false
},
"show_account_name": {
"to_everyone": true,
"to_opers": true,
"to_self": true
}
},
"motd": [
"Welcome to TechIRCd!",
"A modern IRC server written in Go",
"Enjoy your stay on TechNet!"
],
"logging": {
"level": "info",
"file": "techircd.log",
"max_size": 100,
"max_backups": 3,
"max_age": 28
}
}

150
configs/config.json Normal file
View File

@@ -0,0 +1,150 @@
{
"server": {
"name": "TechIRCd",
"network": "TechNet",
"description": "A modern IRC server written in Go",
"version": "1.0.0",
"admin_info": "admin@example.com",
"listen": {
"host": "localhost",
"port": 6667,
"ssl_port": 6697,
"enable_ssl": false
},
"ssl": {
"cert_file": "server.crt",
"key_file": "server.key",
"require_ssl": false
}
},
"limits": {
"max_clients": 1000,
"max_channels": 100,
"max_channel_users": 500,
"max_nick_length": 30,
"max_channel_length": 50,
"max_topic_length": 307,
"max_kick_length": 307,
"max_away_length": 307,
"ping_timeout": 300,
"registration_timeout": 60,
"flood_lines": 10,
"flood_seconds": 60
},
"features": {
"enable_oper": true,
"enable_services": false,
"enable_modes": true,
"enable_ctcp": true,
"enable_dcc": false,
"case_mapping": "rfc1459"
},
"privacy": {
"hide_hosts_from_users": true,
"oper_bypass_host_hide": true,
"masked_host_suffix": "users.technet"
},
"whois_features": {
"show_user_modes": {
"to_everyone": false,
"to_opers": true,
"to_self": true
},
"show_ssl_status": {
"to_everyone": true,
"to_opers": true,
"to_self": true
},
"show_idle_time": {
"to_everyone": false,
"to_opers": true,
"to_self": true
},
"show_signon_time": {
"to_everyone": false,
"to_opers": true,
"to_self": true
},
"show_real_host": {
"to_everyone": false,
"to_opers": true,
"to_self": true
},
"show_channels": {
"to_everyone": true,
"to_opers": true,
"to_self": true,
"hide_secret_channels": true,
"hide_private_channels": false,
"show_membership_levels": true
},
"show_oper_class": {
"to_everyone": false,
"to_opers": true,
"to_self": true
},
"show_client_info": {
"to_everyone": false,
"to_opers": true,
"to_self": false
},
"show_account_name": {
"to_everyone": true,
"to_opers": true,
"to_self": true
},
"show_activity_stats": false,
"show_github_integration": false,
"show_geolocation": false,
"show_performance_stats": false,
"show_device_info": false,
"show_social_graph": false,
"show_security_score": false,
"custom_fields": []
},
"channels": {
"default_modes": "+nt",
"auto_join": [
"#general"
],
"admin_channels": [
"#admin"
],
"founder_mode": "o",
"modes": {
"ban_list_size": 100,
"except_list_size": 100,
"invite_list_size": 100
}
},
"opers": [
{
"name": "admin",
"password": "your_secure_password_here",
"host": "*@localhost",
"class": "admin",
"flags": [
"global_kill",
"remote",
"connect",
"squit"
]
}
],
"oper_config": {
"config_file": "configs/opers.conf",
"enable": true
},
"motd": [
"Welcome to TechIRCd!",
"A modern IRC server written in Go",
"Enjoy your stay on TechNet!"
],
"logging": {
"level": "info",
"file": "techircd.log",
"max_size": 100,
"max_backups": 3,
"max_age": 28
}
}

117
configs/opers.conf Normal file
View File

@@ -0,0 +1,117 @@
{
"rank_names": {
"rank_1": "Helper",
"rank_2": "Moderator",
"rank_3": "Operator",
"rank_4": "Administrator",
"rank_5": "Owner"
},
"classes": [
{
"name": "helper",
"rank": 1,
"description": "Helper - Basic moderation commands",
"permissions": [
"kick",
"topic",
"mode_channel"
],
"color": "green",
"symbol": "%"
},
{
"name": "moderator",
"rank": 2,
"description": "Moderator - Channel and user management",
"permissions": [
"ban",
"unban",
"kick",
"mute",
"topic",
"mode_channel",
"mode_user",
"who_override"
],
"inherits": "helper",
"color": "blue",
"symbol": "@"
},
{
"name": "operator",
"rank": 3,
"description": "Operator - Server management commands",
"permissions": [
"kill",
"rehash",
"connect",
"squit",
"wallops",
"operwall"
],
"inherits": "moderator",
"color": "red",
"symbol": "*"
},
{
"name": "admin",
"rank": 4,
"description": "Administrator - Full server control",
"permissions": ["*"],
"color": "purple",
"symbol": "&"
},
{
"name": "owner",
"rank": 5,
"description": "Server Owner - Ultimate authority",
"permissions": [
"*",
"shutdown",
"restart"
],
"color": "gold",
"symbol": "~"
}
],
"opers": [
{
"name": "admin",
"password": "changeme_please",
"host": "*@localhost",
"class": "admin",
"flags": [],
"max_clients": 1000,
"contact": "admin@example.com",
"last_seen": ""
},
{
"name": "helper1",
"password": "helper_password",
"host": "*@192.168.1.*",
"class": "helper",
"flags": [],
"max_clients": 100,
"contact": "helper@example.com",
"last_seen": ""
}
],
"settings": {
"require_ssl": false,
"max_failed_attempts": 3,
"lockout_duration_minutes": 30,
"allowed_commands": [
"OPER",
"KILL",
"REHASH",
"WALLOPS",
"OPERWALL",
"CONNECT",
"SQUIT"
],
"log_oper_actions": true,
"notify_on_oper_up": true,
"auto_expire_inactive_days": 365,
"require_two_factor": false
}
}

53
configs/services.json Normal file
View File

@@ -0,0 +1,53 @@
{
"enable": true,
"nickserv": {
"enable": true,
"nick": "NickServ",
"user": "services",
"host": "services.techircd.net",
"realname": "Nickname Registration Service",
"expire_time": 30,
"identify_timeout": 60,
"email_verify": false,
"restrict_registration": false
},
"chanserv": {
"enable": true,
"nick": "ChanServ",
"user": "services",
"host": "services.techircd.net",
"realname": "Channel Registration Service",
"expire_time": 30,
"max_channels": 20,
"auto_deop": true
},
"operserv": {
"enable": true,
"nick": "OperServ",
"user": "services",
"host": "services.techircd.net",
"realname": "Operator Service"
},
"memoserv": {
"enable": true,
"nick": "MemoServ",
"user": "services",
"host": "services.techircd.net",
"realname": "Memo Service",
"max_memos": 50,
"memo_expire": 365
},
"database": {
"enable": true,
"type": "mysql",
"database": "techircd_services",
"host": "localhost",
"port": 3306,
"username": "techircd",
"password": "changeme",
"options": {
"charset": "utf8mb4",
"parseTime": "true"
}
}
}

100
database.go Normal file
View File

@@ -0,0 +1,100 @@
package main
import (
"database/sql"
"time"
)
// Database layer for persistent storage
type DatabaseConfig struct {
Type string `json:"type"` // sqlite, mysql, postgres
Host string `json:"host"`
Port int `json:"port"`
Database string `json:"database"`
Username string `json:"username"`
Password string `json:"password"`
Options map[string]string `json:"options"`
}
type Database struct {
db *sql.DB
config DatabaseConfig
}
// User account storage
type UserAccount struct {
ID int `db:"id"`
Nick string `db:"nick"`
PasswordHash string `db:"password_hash"`
Email string `db:"email"`
RegisterTime time.Time `db:"register_time"`
LastSeen time.Time `db:"last_seen"`
Flags []string `db:"flags"`
}
// Channel registration
type RegisteredChannel struct {
ID int `db:"id"`
Name string `db:"name"`
Founder string `db:"founder"`
RegisterTime time.Time `db:"register_time"`
Topic string `db:"topic"`
Modes string `db:"modes"`
AccessList []ChannelAccess `db:"-"`
}
type ChannelAccess struct {
Nick string `db:"nick"`
Level int `db:"level"` // 1=voice, 10=halfop, 50=op, 100=admin, 500=founder
}
// Persistent bans/quiets
type NetworkBan struct {
ID int `db:"id"`
Mask string `db:"mask"`
Reason string `db:"reason"`
SetBy string `db:"set_by"`
SetTime time.Time `db:"set_time"`
Duration int `db:"duration"` // 0 = permanent
Type string `db:"type"` // gline, kline, zline, qline
}
// Statistics tracking
type ServerStats struct {
Date time.Time `db:"date"`
MaxUsers int `db:"max_users"`
MaxChannels int `db:"max_channels"`
TotalConnections int64 `db:"total_connections"`
MessageCount int64 `db:"message_count"`
OperCount int `db:"oper_count"`
}
func NewDatabase(config DatabaseConfig) (*Database, error) {
// Initialize database connection
return &Database{config: config}, nil
}
func (db *Database) CreateTables() error {
// Create all necessary tables
return nil
}
func (db *Database) GetUserAccount(nick string) (*UserAccount, error) {
// Retrieve user account
return nil, nil
}
func (db *Database) SaveUserAccount(account *UserAccount) error {
// Save user account
return nil
}
func (db *Database) GetChannelAccess(channel string) ([]ChannelAccess, error) {
// Get channel access list
return nil, nil
}
func (db *Database) LogStatistics(stats ServerStats) error {
// Log daily statistics
return nil
}

32
docs/CHANGELOG.md Normal file
View File

@@ -0,0 +1,32 @@
# Changelog
All notable changes to TechIRCd will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0] - 2025-07-30
### Added
- Initial release of TechIRCd
- Full IRC protocol implementation (RFC 2812 compliant)
- Advanced channel management with operator hierarchy
- Extended ban system with quiet mode support (~q:mask)
- Comprehensive operator features and SNOmasks
- User modes system with SSL detection
- Enhanced stability with panic recovery
- Real-time health monitoring and metrics
- Configuration validation and sanitization
- Graceful shutdown capabilities
- Flood protection with operator exemption
### Features
- Channel modes: +m +n +t +i +s +p +k +l +b
- User modes: +i +w +s +o +x +B +z +r
- Operator commands: KILL, GLOBALNOTICE, OPERWALL, WALLOPS, REHASH, TRACE
- SNOmasks: +c +k +o +x +f +n +s +d
- Extended ban types with quiet mode support
- Health monitoring with memory and goroutine tracking
- Comprehensive error handling and recovery systems
[1.0.0]: https://github.com/ComputerTech312/TechIRCd/releases/tag/v1.0.0

165
docs/CONNECTION_HANDLING.md Normal file
View File

@@ -0,0 +1,165 @@
# TechIRCd Connection Handling Improvements
## Overview
TechIRCd now features **robust connection handling** designed to handle real-world network conditions, client behaviors, and edge cases gracefully.
## Key Improvements
### 🛡️ **Robust Client Connection Handling**
#### **1. Comprehensive Error Recovery**
- **Panic Recovery**: All client handlers have panic recovery with detailed logging
- **Graceful Cleanup**: Proper resource cleanup even when connections fail
- **Connection Validation**: Validates connections and server state before processing
#### **2. Smart Timeout Management**
- **Registration Timeout**: 60 seconds for initial registration (configurable)
- **Read Timeouts**: 5 minutes for registered clients, 30 seconds during registration
- **Progressive Timeouts**: More lenient timeouts for new connections
#### **3. Enhanced Message Processing**
- **Binary Data Detection**: Detects and rejects non-text data
- **Size Limits**: Truncates oversized messages (512 bytes max)
- **Empty Line Handling**: Gracefully ignores empty lines
- **Encoding Validation**: Checks for valid IRC characters
#### **4. Intelligent Flood Protection**
- **Registration Leniency**: More generous limits during connection setup
- **Sliding Window**: Proper flood detection with time windows
- **Configurable Limits**: Respects server configuration settings
### 🔧 **Server Connection Management**
#### **1. Connection Limits**
- **Max Client Enforcement**: Rejects connections when server is full
- **Resource Protection**: Prevents server overload
- **Graceful Rejection**: Sends proper error message before closing
#### **2. Accept Error Handling**
- **Temporary Error Recovery**: Handles temporary network errors
- **Retry Logic**: Continues accepting after temporary failures
- **Error Classification**: Different handling for different error types
#### **3. Client State Tracking**
- **Connection Counting**: Real-time client count tracking
- **State Validation**: Ensures clients are properly initialized
- **Resource Monitoring**: Tracks and logs connection statistics
### 📊 **Enhanced Logging and Debugging**
#### **1. Detailed Connection Logging**
```
2025/09/03 19:02:44 New client connected from 46.24.193.29:60237 (total: 1)
2025/09/03 19:02:44 Starting robust client handler for 46.24.193.29:60237
2025/09/03 19:02:44 Client 46.24.193.29:60237: CAP LS 302
2025/09/03 19:02:44 Client 46.24.193.29:60237: NICK TestUser
2025/09/03 19:02:44 Client registration completed: TestUser (testuser) from 46.24.193.29:60237
```
#### **2. Error Context**
- **Connection Source**: Always logs client IP/port
- **Error Details**: Specific error messages for troubleshooting
- **State Information**: Logs registration status and timing
#### **3. Performance Metrics**
- **Connection Duration**: Tracks how long connections last
- **Message Counts**: Monitors message frequency
- **Registration Time**: Measures time to complete registration
### 🔒 **Security Enhancements**
#### **1. Protocol Validation**
- **Command Validation**: Ensures only valid IRC commands are processed
- **Data Sanitization**: Removes dangerous or malformed input
- **Buffer Management**: Prevents buffer overflow attacks
#### **2. Connection Security**
- **Rate Limiting**: Prevents rapid connection attempts
- **Resource Limits**: Protects against resource exhaustion
- **Input Validation**: Validates all client input before processing
### 🚀 **Real-World Compatibility**
#### **1. IRC Client Support**
- **HexChat Compatible**: Tested with popular IRC clients
- **IRCv3 Support**: Handles capability negotiation properly
- **Standard Compliance**: Follows RFC 2812 standards
#### **2. Network Conditions**
- **Slow Connections**: Handles high-latency connections
- **Unstable Networks**: Recovers from temporary network issues
- **Mobile Clients**: Optimized for mobile network conditions
#### **3. Edge Cases**
- **Partial Messages**: Handles incomplete message transmission
- **Connection Drops**: Graceful handling of sudden disconnections
- **Server Restarts**: Proper cleanup during server shutdown
## Configuration Options
### **Timeout Settings**
```json
{
"limits": {
"registration_timeout": 60,
"ping_timeout": 300,
"flood_lines": 10,
"flood_seconds": 60
}
}
```
### **Connection Limits**
```json
{
"limits": {
"max_clients": 1000,
"max_channels": 100
}
}
```
## Benefits
### **For Users**
-**Reliable Connections**: Less likely to drop due to network issues
-**Faster Registration**: Optimized registration process
-**Better Error Messages**: Clear feedback when issues occur
-**Mobile Friendly**: Works well on mobile networks
### **For Administrators**
-**Detailed Logging**: Easy troubleshooting with comprehensive logs
-**Resource Protection**: Server stays stable under load
-**Security**: Protection against malformed data and attacks
-**Monitoring**: Real-time connection statistics
### **For Developers**
-**Code Clarity**: Well-structured connection handling code
-**Error Recovery**: Robust error handling prevents crashes
-**Debugging**: Extensive logging for issue resolution
-**Maintainability**: Clean separation of concerns
## Testing Recommendations
### **Basic Connection Test**
```bash
# Test with telnet
telnet your-server.com 6667
# Send IRC commands
NICK TestUser
USER testuser 0 * :Test User
```
### **HexChat Configuration**
- **Server**: your-server.com
- **Port**: 6667 (or 6697 for SSL)
- **SSL**: Disabled (unless you have SSL configured)
- **Character Set**: UTF-8
### **Load Testing**
- Test with multiple simultaneous connections
- Try rapid connect/disconnect cycles
- Test with slow network conditions
TechIRCd now provides **enterprise-grade connection handling** suitable for production IRC networks!

65
docs/CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,65 @@
# Contributing to TechIRCd
We love your input! We want to make contributing to TechIRCd as easy and transparent as possible.
## Development Process
We use GitHub to host code, to track issues and feature requests, as well as accept pull requests.
## Pull Requests
1. Fork the repo and create your branch from `main`.
2. If you've added code that should be tested, add tests.
3. If you've changed APIs, update the documentation.
4. Ensure the test suite passes.
5. Make sure your code lints.
6. Issue that pull request!
## Code Style
- Follow standard Go formatting (`gofmt`)
- Use meaningful variable and function names
- Add comments for exported functions and complex logic
- Keep functions focused and single-purpose
## Testing
```bash
# Run all tests
go test ./...
# Run tests with coverage
go test -cover ./...
# Run tests with race detection
go test -race ./...
```
## Commit Messages
- Use the present tense ("Add feature" not "Added feature")
- Use the imperative mood ("Move cursor to..." not "Moves cursor to...")
- Limit the first line to 72 characters or less
- Reference issues and pull requests liberally after the first line
## Bug Reports
**Great Bug Reports** tend to have:
- A quick summary and/or background
- Steps to reproduce
- What you expected would happen
- What actually happens
- Notes (possibly including why you think this might be happening)
## Feature Requests
We welcome feature requests! Please provide:
- Clear description of the feature
- Use case or motivation
- Possible implementation approach (if you have ideas)
## License
By contributing, you agree that your contributions will be licensed under the MIT License.

174
docs/ENHANCEMENT_ROADMAP.md Normal file
View File

@@ -0,0 +1,174 @@
# TechIRCd Enhancement Roadmap
## 🚀 Phase 1: Core Improvements (High Priority)
### 1.1 Missing IRC Commands (2-3 weeks)
- [ ] USERHOST command
- [ ] ISON command
- [ ] STATS command with detailed server statistics
- [ ] TIME command
- [ ] VERSION command
- [ ] ADMIN command
- [ ] INFO command
- [ ] LUSERS command
- [ ] Enhanced LIST with pattern matching
- [ ] SILENCE command for user-level blocking
### 1.2 Channel Mode Enhancements (1-2 weeks)
- [ ] Ban exceptions (+e mode)
- [ ] Invite exceptions (+I mode)
- [ ] Permanent channels (+P mode)
- [ ] Registered-only channels (+r mode)
- [ ] SSL-only channels (+z mode)
- [ ] Channel forwarding (+f mode)
- [ ] Founder protection mode (~q)
- [ ] Admin protection mode (&a)
### 1.3 Security & Authentication (2-3 weeks)
- [ ] SASL authentication support
- [ ] Certificate-based authentication
- [ ] Rate limiting per IP/user
- [ ] GeoIP blocking capabilities
- [ ] DDoS protection mechanisms
- [ ] Enhanced flood protection
- [ ] Connection throttling
## 🌐 Phase 2: Network & Scaling (Medium Priority)
### 2.1 Enhanced Server Linking (3-4 weeks)
- [ ] Burst optimization for large networks
- [ ] Network topology visualization
- [ ] Automatic failover and recovery
- [ ] Load balancing across servers
- [ ] Network-wide channel/user sync
- [ ] Cluster management interface
- [ ] Health monitoring between servers
### 2.2 Database Integration (2-3 weeks)
- [ ] SQLite/MySQL/PostgreSQL support
- [ ] User account persistence
- [ ] Channel registration system
- [ ] Network-wide ban storage
- [ ] Statistics logging
- [ ] Audit trail logging
- [ ] Data migration tools
### 2.3 Services Framework (4-5 weeks)
- [ ] NickServ implementation
- [ ] ChanServ implementation
- [ ] OperServ implementation
- [ ] MemoServ implementation
- [ ] BotServ implementation
- [ ] Services API framework
- [ ] Plugin system for custom services
## 📊 Phase 3: Advanced Features (Lower Priority)
### 3.1 Monitoring & Analytics (3-4 weeks)
- [ ] Prometheus metrics integration
- [ ] Grafana dashboard templates
- [ ] Real-time performance monitoring
- [ ] User activity analytics
- [ ] Channel analytics
- [ ] Alert system with notifications
- [ ] Performance profiling tools
- [ ] Capacity planning metrics
### 3.2 Web Interface (4-5 weeks)
- [ ] Administrative web panel
- [ ] REST API endpoints
- [ ] GraphQL query interface
- [ ] Real-time dashboard with WebSockets
- [ ] Mobile-responsive design
- [ ] User management interface
- [ ] Channel management interface
- [ ] Network topology viewer
- [ ] Log viewer and search
### 3.3 Modern Protocol Support (3-4 weeks)
- [ ] IRCv3 capabilities
- [ ] Message tags support
- [ ] Account tracking
- [ ] MONITOR command
- [ ] Extended JOIN
- [ ] CHGHOST support
- [ ] Batch processing
- [ ] Server-time capability
## 🎨 Phase 4: Quality of Life (Ongoing)
### 4.1 Developer Experience
- [ ] Comprehensive test suite (>80% coverage)
- [ ] CI/CD pipeline setup
- [ ] Automated security scanning
- [ ] Performance benchmarking
- [ ] Documentation improvements
- [ ] Code quality tools integration
- [ ] Dependency management
### 4.2 Deployment & Operations
- [ ] Docker containerization
- [ ] Kubernetes manifests
- [ ] Helm charts
- [ ] Configuration management
- [ ] Backup and restore tools
- [ ] Migration utilities
- [ ] Health check endpoints
### 4.3 Community Features
- [ ] Plugin architecture
- [ ] Extension API
- [ ] Custom command framework
- [ ] Event system
- [ ] Webhook integrations
- [ ] Third-party service connectors
## 🔧 Technical Debt & Improvements
### Code Structure
- [ ] Proper package structure (remove `package main` everywhere)
- [ ] Interface definitions for better testing
- [ ] Dependency injection framework
- [ ] Configuration validation improvements
- [ ] Error handling standardization
- [ ] Logging framework upgrade
### Performance Optimizations
- [ ] Connection pooling
- [ ] Message batching
- [ ] Memory optimization
- [ ] CPU profiling and optimization
- [ ] Network I/O improvements
- [ ] Concurrent processing enhancements
### Security Hardening
- [ ] Input validation improvements
- [ ] Buffer overflow protection
- [ ] Memory safety audits
- [ ] Cryptographic improvements
- [ ] Secure defaults
- [ ] Vulnerability scanning
## 📈 Implementation Timeline
**Total Estimated Time: 6-8 months**
- **Month 1-2**: Phase 1 (Core Improvements)
- **Month 3-4**: Phase 2 (Network & Scaling)
- **Month 5-6**: Phase 3 (Advanced Features)
- **Month 7-8**: Phase 4 (Quality of Life) + Polish
## 🎯 Quick Wins (Can implement immediately)
1. **USERHOST command** (1 day)
2. **ISON command** (1 day)
3. **TIME command** (1 day)
4. **VERSION command** (1 day)
5. **Enhanced LIST filtering** (2-3 days)
6. **Ban exceptions (+e mode)** (2-3 days)
7. **Invite exceptions (+I mode)** (2-3 days)
8. **Basic SASL PLAIN** (3-4 days)
9. **Prometheus metrics** (3-4 days)
10. **Basic web stats API** (2-3 days)
These improvements would make TechIRCd one of the most feature-complete and modern IRC servers available!

176
docs/GOD_MODE_STEALTH.md Normal file
View File

@@ -0,0 +1,176 @@
# God Mode and Stealth Mode
TechIRCd supports two advanced operator features via user modes:
## God Mode (+G)
God Mode gives operators ultimate channel override capabilities.
### Usage
```
/mode nickname +G # Enable God Mode
/mode nickname -G # Disable God Mode
```
### Capabilities
- **Channel Access**: Can join any channel regardless of bans, limits, keys, or invite-only status
- **Kick Immunity**: Cannot be kicked from channels by anyone, including other operators
- **Ban Immunity**: Can join and remain in channels even when banned
- **Invite Override**: Can join invite-only channels without being invited
- **Limit Override**: Can join channels that have reached their user limit
- **Key Override**: Can join channels with passwords/keys without providing them
### Requirements
- Must be an IRC operator (`/oper`)
- Must have `god_mode` permission in operator class configuration
- Only the user themselves can set/unset their God Mode
### Security Notes
- God Mode actions are logged to operator snomasks (`+o`)
- Use responsibly - this bypasses all normal channel protections
- Intended for emergency situations and network administration
## Stealth Mode (+S)
Stealth Mode makes operators invisible to regular users while remaining visible to other operators.
### Usage
```
/mode nickname +S # Enable Stealth Mode
/mode nickname -S # Disable Stealth Mode
```
### Effects
- **WHO Command**: Stealth users don't appear in `/who` results for regular users
- **NAMES Command**: Stealth users don't appear in channel user lists for regular users
- **Channel Lists**: Regular users can't see stealth operators in channels
- **Operator Visibility**: Other operators can always see stealth users
### Requirements
- Must be an IRC operator (`/oper`)
- Must have `stealth_mode` permission in operator class configuration
- Only the user themselves can set/unset their Stealth Mode
### Use Cases
- Undercover moderation and monitoring
- Reduced operator visibility during investigations
- Network administration without user awareness
## Configuration
Add permissions to operator classes in `configs/opers.conf`:
```json
{
"classes": [
{
"name": "admin",
"rank": 4,
"description": "Administrator with special powers",
"permissions": [
"*",
"god_mode",
"stealth_mode"
]
}
]
}
```
## Examples
### Basic Usage
```
# As an operator with god_mode permission:
/mode mynick +G
# *** GOD MODE enabled - You have ultimate power!
# Join a banned/invite-only channel:
/join #private-channel
# Successfully joins despite restrictions
# Disable God Mode:
/mode mynick -G
# *** GOD MODE disabled
# Enable Stealth Mode:
/mode mynick +S
# *** STEALTH MODE enabled - You are now invisible to users
# Regular users won't see you in:
/who #channel
/names #channel
# Other operators will still see you
```
### Combined Usage
```
# Enable both modes simultaneously:
/mode mynick +GS
# *** GOD MODE enabled - You have ultimate power!
# *** STEALTH MODE enabled - You are now invisible to users
# Now you can:
# - Join any channel (God Mode)
# - Remain invisible to regular users (Stealth Mode)
# - Be visible to other operators
```
### Permission Checking
```
# Attempting without proper permissions:
/mode mynick +G
# :server 481 mynick :Permission Denied - You need god_mode permission
# Must be oper first:
/mode mynick +S
# :server 481 mynick :Permission Denied- You're not an IRC operator
```
## Technical Implementation
### God Mode
- Stored as user mode `+G`
- Checked via `HasGodMode()` method
- Bypasses channel restrictions in:
- `JOIN` command (bans, limits, keys, invite-only)
- `KICK` command (cannot be kicked)
- Channel access validation
### Stealth Mode
- Stored as user mode `+S`
- Checked via `HasStealthMode()` method
- Filtered in:
- `WHO` command responses
- `NAMES` command responses
- Channel member visibility
### Mode Persistence
- User modes are stored per-client session
- Lost on disconnect/reconnect
- Must be re-enabled after each connection
## Security Considerations
1. **Audit Trail**: All God Mode and Stealth Mode activations are logged
2. **Permission Based**: Requires explicit operator class permissions
3. **Self-Only**: Users can only set modes on themselves
4. **Operator Level**: Requires existing operator privileges
5. **Reversible**: Can be disabled at any time
## Troubleshooting
### Mode Not Setting
- Verify you are opered (`/oper`)
- Check operator class has required permissions
- Ensure using correct syntax (`/mode nickname +G`)
### Not Working as Expected
- God Mode only affects channel restrictions, not other commands
- Stealth Mode only hides from regular users, not operators
- Modes are case-sensitive (`+G` not `+g`)
### Permission Denied
- Contact network administrator to add permissions to your operator class
- Verify operator class configuration in `configs/opers.conf`

122
docs/IRCV3_FEATURES.md Normal file
View File

@@ -0,0 +1,122 @@
# TechIRCd IRCv3 Features
## ✅ Implemented IRCv3 Features
### Core Capability Negotiation
- **CAP LS** - List available capabilities
- **CAP LIST** - List enabled capabilities
- **CAP REQ** - Request capabilities
- **CAP ACK/NAK** - Acknowledge/reject capability requests
- **CAP END** - End capability negotiation
### Supported Capabilities
#### IRCv3.1 Base
-**sasl** - SASL authentication (PLAIN mechanism)
-**multi-prefix** - Multiple channel prefixes (~@%+)
-**away-notify** - Away state change notifications
-**account-notify** - Account login/logout notifications
-**extended-join** - JOIN with account and real name
#### IRCv3.2 Extensions
-**server-time** - Timestamp tags on messages
-**userhost-in-names** - Full hostmasks in NAMES
-**monitor-notify** - MONITOR command for presence
-**account-tag** - Account tags on messages
-**message-tags** - IRCv3 message tag framework
#### IRCv3.3 Features
-**echo-message** - Echo sent messages back to sender
-**chghost** - Host change notifications
-**invite-notify** - Channel invite notifications
-**batch** - Message batching support
## 🚀 Usage Examples
### Basic Capability Negotiation
```
Client: CAP LS 302
Server: :server CAP * LS :account-notify away-notify extended-join multi-prefix sasl server-time userhost-in-names account-tag message-tags echo-message batch chghost invite-notify monitor-notify
Client: CAP REQ :server-time message-tags sasl
Server: :server CAP * ACK :server-time message-tags sasl
Client: CAP END
```
### SASL Authentication
```
Client: CAP REQ :sasl
Server: :server CAP * ACK :sasl
Client: AUTHENTICATE PLAIN
Server: AUTHENTICATE +
Client: AUTHENTICATE dXNlcm5hbWU6cGFzc3dvcmQ=
Server: :server 903 * :SASL authentication successful
Client: CAP END
```
### Server-Time Tags
```
@time=2025-08-28T17:01:49.123Z :nick!user@host PRIVMSG #channel :Hello!
```
### Account Tags
```
@account=alice;time=2025-08-28T17:01:49.123Z :alice!user@host PRIVMSG #channel :Hello!
```
### Extended JOIN
```
:nick!user@host JOIN #channel alice :Real Name
```
### MONITOR Command
```
Client: MONITOR + alice,bob,charlie
Server: :server 730 nick :alice,bob
Server: :server 731 nick :charlie
Client: MONITOR L
Server: :server 732 nick :alice,bob,charlie
Server: :server 733 nick :End of MONITOR list
```
## 🔧 Configuration
IRCv3 features are automatically available when clients request them through CAP negotiation. No special server configuration required.
### SASL Accounts
Currently supports simple username/password authentication. Edit the `authenticateUser()` function in `commands.go` to integrate with your authentication system.
## 🎯 Advanced Features
### Message Tags
TechIRCd automatically adds appropriate tags based on client capabilities:
- `time` - Server timestamp (if server-time enabled)
- `account` - User account name (if logged in and account-tag enabled)
### Multi-Prefix Support
Shows all user channel modes: `~@%+nick` for Owner/Op/Halfop/Voice
### Echo Message
When enabled, clients receive copies of their own messages, useful for multi-device synchronization.
## 🔮 Future IRCv3 Extensions
Planned for future releases:
- **draft/chathistory** - Message history replay
- **draft/resume** - Connection state resumption
- **labeled-response** - Request/response correlation
- **standard-replies** - Standardized error responses
## 💡 Client Compatibility
TechIRCd's IRCv3 implementation is compatible with:
- **HexChat** - Full feature support
- **IRCCloud** - Full feature support
- **Textual** - Full feature support
- **WeeChat** - Full feature support
- **KiwiIRC** - Full feature support
- **IRCv3-compliant bots** - Full API support
Legacy IRC clients without IRCv3 support continue to work normally without any IRCv3 features.

290
docs/LINKING.md Normal file
View File

@@ -0,0 +1,290 @@
# TechIRCd Server Linking
TechIRCd now supports IRC server linking, allowing multiple IRC servers to form a network. This enables users on different servers to communicate with each other seamlessly.
## Features
- **Server-to-Server Communication**: Full IRC protocol compliance for server linking
- **Hub and Leaf Configuration**: Support for hub/leaf network topologies
- **Auto-Connect**: Automatic connection to configured servers on startup
- **Operator Commands**: Manual connection and disconnection controls
- **Security**: Password-based authentication for server connections
- **Network Transparency**: Users can see and communicate across the entire network
## Configuration
Server linking is configured in the `config.json` file under the `linking` section:
```json
{
"linking": {
"enable": true,
"server_port": 6697,
"password": "linkpassword123",
"hub": false,
"auto_connect": false,
"links": [
{
"name": "hub.technet.org",
"host": "127.0.0.1",
"port": 6697,
"password": "linkpassword123",
"auto_connect": false,
"hub": true,
"description": "TechNet Hub Server"
}
]
}
}
```
### Configuration Options
- **enable**: Enable or disable server linking functionality
- **server_port**: Port to listen on for incoming server connections
- **password**: Default password for server authentication
- **hub**: Whether this server acts as a hub (can connect to multiple servers)
- **auto_connect**: Automatically attempt to connect to configured links on startup
- **links**: Array of server configurations to link with
### Link Configuration
Each link in the `links` array supports:
- **name**: Unique server name (should match the remote server's configured name)
- **host**: Hostname or IP address of the remote server
- **port**: Port number the remote server is listening on for server connections
- **password**: Authentication password (must match on both servers)
- **auto_connect**: Whether to automatically connect to this server
- **hub**: Whether the remote server is a hub server
- **description**: Human-readable description of the server
## Operator Commands
Operators can manually control server links using these commands:
### CONNECT
Connect to a configured server:
```
/CONNECT <servername> <port> [host]
```
Examples:
```
/CONNECT hub.technet.org 6697
/CONNECT hub.technet.org 6697 192.168.1.100
```
### SQUIT
Disconnect from a linked server:
```
/SQUIT <servername> [reason]
```
Examples:
```
/SQUIT hub.technet.org
/SQUIT hub.technet.org :Maintenance required
```
### LINKS
Show all connected servers:
```
/LINKS
```
This displays the network topology showing all connected servers.
## Network Topology
TechIRCd supports both hub-and-leaf and mesh network topologies:
### Hub-and-Leaf
```
[Hub Server]
/ | \
[Leaf1] [Leaf2] [Leaf3]
```
In this configuration:
- The hub server (`hub: true`) connects to multiple leaf servers
- Leaf servers (`hub: false`) only connect to the hub
- All inter-server communication routes through the hub
### Mesh Network
```
[Server1] --- [Server2]
| \ / |
| \ / |
[Server3] --- [Server4]
```
In this configuration:
- All servers can connect to multiple other servers
- Provides redundancy and multiple communication paths
- More complex but more resilient
## Server-to-Server Protocol
TechIRCd implements standard IRC server-to-server protocol commands:
- **PASS**: Authentication password
- **SERVER**: Server introduction and information
- **PING/PONG**: Keep-alive and lag detection
- **SQUIT**: Server quit/disconnect
- **NICK**: User nickname propagation
- **USER**: User information propagation
- **JOIN/PART**: Channel membership changes
- **PRIVMSG/NOTICE**: Message routing between servers
- **QUIT**: User disconnection notifications
## Message Routing
When servers are linked, messages are automatically routed across the network:
1. **User Messages**: Private messages and notices are routed to the correct server
2. **Channel Messages**: Broadcast to all servers with users in the channel
3. **User Lists**: WHO, WHOIS, and NAMES commands work across the network
4. **Channel Lists**: LIST shows channels from all linked servers
## Security Considerations
- **Passwords**: Always use strong, unique passwords for server links
- **Network Access**: Restrict server linking ports to trusted networks
- **Operator Permissions**: Only trusted operators should have CONNECT/SQUIT privileges
- **Link Validation**: Server names and passwords are validated before connection
## Troubleshooting
### Connection Issues
1. **Check Configuration**: Ensure server names, hosts, ports, and passwords match
2. **Network Connectivity**: Verify network connectivity between servers
3. **Firewall**: Ensure server linking ports are open
4. **Logs**: Check server logs for connection errors and authentication failures
### Common Error Messages
- `No link configuration found`: Server not configured in links array
- `Server already connected`: Attempted to connect to already linked server
- `Authentication failed`: Password mismatch between servers
- `Connection refused`: Network connectivity or firewall issues
### Debugging
Enable debug logging to see detailed server linking information:
```json
{
"logging": {
"level": "debug"
}
}
```
## Example Network Setup
Here's an example of setting up a 3-server network:
### Hub Server (hub.technet.org)
```json
{
"server": {
"name": "hub.technet.org"
},
"linking": {
"enable": true,
"server_port": 6697,
"hub": true,
"links": [
{
"name": "leaf1.technet.org",
"host": "192.168.1.101",
"port": 6697,
"password": "linkpass123",
"auto_connect": true,
"hub": false
},
{
"name": "leaf2.technet.org",
"host": "192.168.1.102",
"port": 6697,
"password": "linkpass123",
"auto_connect": true,
"hub": false
}
]
}
}
```
### Leaf Server 1 (leaf1.technet.org)
```json
{
"server": {
"name": "leaf1.technet.org"
},
"linking": {
"enable": true,
"server_port": 6697,
"hub": false,
"links": [
{
"name": "hub.technet.org",
"host": "192.168.1.100",
"port": 6697,
"password": "linkpass123",
"auto_connect": true,
"hub": true
}
]
}
}
```
### Leaf Server 2 (leaf2.technet.org)
```json
{
"server": {
"name": "leaf2.technet.org"
},
"linking": {
"enable": true,
"server_port": 6697,
"hub": false,
"links": [
{
"name": "hub.technet.org",
"host": "192.168.1.100",
"port": 6697,
"password": "linkpass123",
"auto_connect": true,
"hub": true
}
]
}
}
```
This creates a hub-and-leaf network where:
- The hub automatically connects to both leaf servers
- Leaf servers connect only to the hub
- Users on any server can communicate with users on other servers
- Channels span across all servers in the network
## Performance Considerations
- **Network Latency**: High latency between servers may affect user experience
- **Bandwidth**: Server linking uses additional bandwidth for message propagation
- **CPU Usage**: Message routing and protocol handling requires CPU resources
- **Memory Usage**: Additional memory is needed to track remote users and channels
## Future Enhancements
Planned improvements for server linking include:
- **Services Integration**: Support for network services (NickServ, ChanServ, etc.)
- **Channel Bursting**: Optimized channel synchronization
- **Network Statistics**: Detailed network topology and performance metrics
- **Load Balancing**: Intelligent routing for optimal performance
- **Redundancy**: Automatic failover and connection recovery

355
docs/OPERATOR_SYSTEM.md Normal file
View File

@@ -0,0 +1,355 @@
# Operator Configuration Guide
TechIRCd features a sophisticated hierarchical operator system with separate configuration files and granular permission control.
## Overview
The operator system consists of:
- **Operator Classes**: Define ranks, permissions, and inheritance
- **Individual Operators**: Specific users with assigned classes
- **Permission System**: Granular control over what operators can do
- **Rank Hierarchy**: Higher ranks can operate on lower ranks
## Configuration Files
### Main Config (`config.json`)
```json
"oper_config": {
"config_file": "configs/opers.conf",
"enable": true
}
```
### Operator Config (`configs/opers.conf`)
Contains the complete operator hierarchy and individual operator definitions.
## Operator Classes
### Customizable Rank Names
TechIRCd allows you to completely customize the names of operator ranks. Instead of using the standard Helper/Moderator/Operator names, you can create themed naming schemes:
```json
"rank_names": {
"rank_1": "Cadet", // Instead of "Helper"
"rank_2": "Sergeant", // Instead of "Moderator"
"rank_3": "Lieutenant", // Instead of "Operator"
"rank_4": "Captain", // Instead of "Administrator"
"rank_5": "General", // Instead of "Owner"
"custom_ranks": {
"Field Marshal": 6, // Custom ranks beyond 5
"Supreme Commander": 10
}
}
```
### Popular Naming Themes
#### Gaming/Military Theme
- Rank 1: `Cadet`, `Recruit`, `Private`
- Rank 2: `Sergeant`, `Corporal`, `Specialist`
- Rank 3: `Lieutenant`, `Captain`, `Major`
- Rank 4: `Colonel`, `General`, `Commander`
- Rank 5: `Admiral`, `Marshal`, `Supreme Commander`
#### Corporate/Business Theme
- Rank 1: `Intern`, `Assistant`, `Associate`
- Rank 2: `Specialist`, `Senior Associate`, `Team Lead`
- Rank 3: `Manager`, `Senior Manager`, `Department Head`
- Rank 4: `Director`, `VP`, `Executive`
- Rank 5: `CEO`, `President`, `Chairman`
#### Fantasy/Medieval Theme
- Rank 1: `Apprentice`, `Squire`, `Page`
- Rank 2: `Guardian`, `Warrior`, `Mage`
- Rank 3: `Knight`, `Paladin`, `Wizard`
- Rank 4: `Lord`, `Baron`, `Archmage`
- Rank 5: `King`, `Emperor`, `Divine Ruler`
#### Sci-Fi Theme
- Rank 1: `Ensign`, `Technician`, `Cadet`
- Rank 2: `Engineer`, `Pilot`, `Specialist`
- Rank 3: `Commander`, `Captain`, `Leader`
- Rank 4: `Admiral`, `Fleet Commander`, `Director`
- Rank 5: `Supreme Admiral`, `Galactic Emperor`, `AI Overlord`
### Built-in Classes (Default Names)
#### Helper (Rank 1)
- **Symbol**: `%`
- **Color**: Green
- **Permissions**: Basic moderation
- `kick` - Kick users from channels
- `topic` - Change channel topics
- `mode_channel` - Change channel modes
#### Moderator (Rank 2)
- **Symbol**: `@`
- **Color**: Blue
- **Inherits**: Helper permissions
- **Additional Permissions**:
- `ban` / `unban` - Manage channel bans
- `mute` - Mute users
- `mode_user` - Change user modes
- `who_override` - See hidden users in WHO
#### Operator (Rank 3)
- **Symbol**: `*`
- **Color**: Red
- **Inherits**: Moderator permissions
- **Additional Permissions**:
- `kill` - Kill user connections
- `gline` - Global bans
- `rehash` - Reload configuration
- `connect` / `squit` - Server linking
- `wallops` / `operwall` - Send operator messages
#### Administrator (Rank 4)
- **Symbol**: `&`
- **Color**: Purple
- **Permissions**: `*` (All permissions)
#### Owner (Rank 5)
- **Symbol**: `~`
- **Color**: Gold
- **Special Permissions**:
- `*` - All permissions
- `override_rank` - Can operate on same/higher ranks
- `shutdown` / `restart` - Server control
### Custom Classes
Create your own operator classes:
```json
{
"name": "security",
"rank": 3,
"description": "Security Officer - Network protection",
"permissions": [
"kill",
"gline",
"scan_network",
"access_logs"
],
"inherits": "moderator",
"color": "orange",
"symbol": "!"
}
```
## Individual Operators
### Basic Operator Definition
```json
{
"name": "alice",
"password": "secure_password_here",
"host": "*@trusted.example.com",
"class": "moderator",
"flags": ["extra_channels"],
"max_clients": 500,
"contact": "alice@example.com"
}
```
### Advanced Features
```json
{
"name": "bob",
"password": "another_secure_password",
"host": "admin@192.168.1.*",
"class": "admin",
"flags": ["debug_access", "special_channels"],
"max_clients": 1000,
"expires": "2025-12-31",
"contact": "bob@company.com",
"last_seen": "2025-07-30T10:30:00Z"
}
```
## Permission System
### Standard Permissions
#### Channel Management
- `kick` - Kick users from channels
- `ban` / `unban` - Manage channel bans
- `topic` - Change channel topics
- `mode_channel` - Change channel modes
- `mode_user` - Change user modes
#### User Management
- `kill` - Disconnect users
- `gline` - Global network bans
- `mute` - Silence users
- `who_override` - See hidden information
#### Server Management
- `rehash` - Reload configuration
- `connect` / `squit` - Server linking
- `wallops` / `operwall` - Operator communications
- `shutdown` / `restart` - Server control
#### Special Permissions
- `*` - All permissions (wildcard)
- `override_rank` - Ignore rank restrictions
- `debug_access` - Debug commands
- `log_access` - View server logs
### Custom Permissions
Add your own permissions for custom commands:
```json
"permissions": [
"custom_report",
"special_database_access",
"network_monitoring"
]
```
## Security Features
### Settings Configuration
```json
"settings": {
"require_ssl": true,
"max_failed_attempts": 3,
"lockout_duration_minutes": 30,
"log_oper_actions": true,
"notify_on_oper_up": true,
"auto_expire_inactive_days": 365,
"require_two_factor": false
}
```
### Security Options
- **SSL Requirement**: Force SSL for operator authentication
- **Failed Attempt Tracking**: Lock accounts after failed attempts
- **Action Logging**: Log all operator actions
- **Auto-Expiration**: Remove inactive operators
- **Two-Factor Authentication**: (Future feature)
## Rank System
### How Ranks Work
- **Higher numbers = Higher authority**
- **Same rank cannot operate on each other**
- **Override permission bypasses rank restrictions**
### Example Hierarchy
```
Owner (5) -> Can do anything to anyone
Admin (4) -> Can operate on Operator (3) and below
Operator (3) -> Can operate on Moderator (2) and below
Moderator (2) -> Can operate on Helper (1) and below
Helper (1) -> Can only operate on regular users
```
## WHOIS Integration
The operator system integrates with WHOIS to show:
- Operator status with custom symbols
- Class names and descriptions
- **Custom rank names** based on your configuration
- Permission levels
Example WHOIS output with custom rank names:
```
[313] alice is an IRC operator (moderator - Channel and user management) [Sergeant]
[313] bob is an IRC operator (captain - Senior officer with full authority) [Captain]
```
With fantasy theme:
```
[313] merlin is an IRC operator (lord - Ruler of vast territories) [Lord]
```
## Configuration Examples
### Quick Setup - Gaming Theme
```json
{
"rank_names": {
"rank_1": "Cadet",
"rank_2": "Sergeant",
"rank_3": "Lieutenant",
"rank_4": "Captain",
"rank_5": "General"
},
"classes": [
{
"name": "cadet",
"rank": 1,
"description": "New recruit with basic training",
"permissions": ["kick", "topic"],
"symbol": "+",
"color": "green"
}
]
}
```
See `/configs/examples/` for complete themed configurations:
- `opers-gaming-theme.conf` - Military/Gaming ranks
- `opers-corporate-theme.conf` - Business hierarchy
- `opers-fantasy-theme.conf` - Medieval/Fantasy theme
## Migration from Legacy
TechIRCd automatically falls back to the legacy operator system if:
- `oper_config.enable` is `false`
- The opers.conf file cannot be loaded
- No matching operator is found in the new system
Legacy operators get basic permissions and rank 1.
## Commands for Operators
### Checking Permissions
```
/WHOIS operator_name # See operator class and rank
/OPERLIST # List all operators (future)
/OPERHELP # Show operator commands (future)
```
### Management Commands
```
/OPER name password # Become an operator
/OPERWALL message # Send message to all operators
/REHASH # Reload configuration
```
## Best Practices
### Security
1. Use strong passwords for all operators
2. Restrict host masks to trusted IPs
3. Enable SSL requirement for sensitive ranks
4. Regularly audit operator lists
5. Set expiration dates for temporary operators
### Organization
1. Create classes that match your network structure
2. Use inheritance to avoid permission duplication
3. Document custom permissions clearly
4. Use descriptive class names and descriptions
5. Assign appropriate symbols and colors
### Maintenance
1. Review operator activity regularly
2. Remove inactive operators
3. Update permissions as needed
4. Monitor operator actions through logs
5. Keep opers.conf in version control
This operator system makes TechIRCd extremely flexible for networks of any size, from small communities to large networks with complex hierarchies!

161
docs/PROJECT_STRUCTURE.md Normal file
View File

@@ -0,0 +1,161 @@
# TechIRCd Enhanced Project Structure
## Current Structure Issues
- All packages use `package main` instead of proper names
- Missing comprehensive test coverage
- No CI/CD pipeline
- Limited documentation
- No deployment configurations
## Proposed Enhanced Structure
```
```
TechIRCd/
├── .github/ # GitHub-specific files
│ ├── workflows/ # GitHub Actions CI/CD
│ │ ├── ci.yml # Continuous Integration
│ │ ├── release.yml # Automated releases
│ │ └── security.yml # Security scanning
│ ├── ISSUE_TEMPLATE/ # Issue templates
│ │ ├── bug_report.md
│ │ ├── feature_request.md
│ │ └── question.md
│ └── PULL_REQUEST_TEMPLATE.md
├── api/ # API definitions (if adding REST API)
│ └── v1/
│ └── openapi.yaml
├── build/ # Build configurations
│ └── ci/
│ └── scripts/
├── cmd/ # Main applications
│ ├── techircd/ # Main server
│ │ └── main.go
│ ├── techircd-admin/ # Admin tool
│ │ └── main.go
│ └── techircd-client/ # Test client
│ └── main.go
├── configs/ # Configuration files
│ ├── config.json # Default config
│ ├── config.prod.json # Production config
│ └── config.dev.json # Development config
├── deployments/ # Deployment configurations
│ ├── kubernetes/
│ │ ├── namespace.yaml
│ │ ├── deployment.yaml
│ │ └── service.yaml
│ └── systemd/
│ └── techircd.service
```
├── docs/ # Documentation
│ ├── api/ # API documentation
│ ├── admin/ # Administrator guide
│ ├── user/ # User guide
│ ├── development/ # Development guide
│ └── examples/ # Usage examples
├── examples/ # Example configurations
│ ├── simple/
│ ├── production/
│ └── cluster/
├── internal/ # Private application code
│ ├── channel/
│ │ ├── channel.go
│ │ ├── channel_test.go
│ │ ├── modes.go
│ │ └── permissions.go
│ ├── client/
│ │ ├── client.go
│ │ ├── client_test.go
│ │ ├── auth.go
│ │ └── connection.go
│ ├── commands/
│ │ ├── commands.go
│ │ ├── commands_test.go
│ │ ├── irc.go
│ │ └── operator.go
│ ├── config/
│ │ ├── config.go
│ │ ├── config_test.go
│ │ └── validation.go
│ ├── database/ # Database layer (future)
│ │ ├── models/
│ │ └── migrations/
│ ├── health/
│ │ ├── health.go
│ │ ├── health_test.go
│ │ └── metrics.go
│ ├── protocol/ # IRC protocol handling
│ │ ├── parser.go
│ │ ├── parser_test.go
│ │ └── numerics.go
│ ├── security/ # Security features
│ │ ├── auth.go
│ │ ├── ratelimit.go
│ │ └── validation.go
│ └── server/
│ ├── server.go
│ ├── server_test.go
│ └── handlers.go
├── pkg/ # Public library code
│ └── irc/ # IRC utilities for external use
│ ├── client/
│ │ └── client.go
│ └── protocol/
│ └── constants.go
├── scripts/ # Build and utility scripts
│ ├── build.sh
│ ├── test.sh
│ ├── lint.sh
│ └── release.sh
├── test/ # Test data and utilities
│ ├── fixtures/ # Test data
│ ├── integration/ # Integration tests
│ │ └── server_test.go
│ ├── e2e/ # End-to-end tests
│ └── performance/ # Performance tests
├── tools/ # Supporting tools
│ └── migrate/ # Database migration tool
├── web/ # Web interface (future)
│ ├── static/
│ └── templates/
├── .editorconfig
├── .golangci.yml # Linter configuration
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── go.mod
├── go.sum
├── LICENSE
├── Makefile
├── README.md
└── SECURITY.md
```
## Implementation Priority
### Phase 1: Core Structure
1. Fix package declarations
2. Add proper test files
3. Create CI/CD pipeline
4. Add linting configuration
### Phase 2: Enhanced Features
1. Create admin tools
2. Add API endpoints
3. Implement database layer
4. Add monitoring tools
### Phase 3: Production Ready
1. Add monitoring
2. Create deployment configs
3. Add security scanning
4. Performance optimization
## Benefits of This Structure
1. **Professional**: Follows Go and open-source best practices
2. **Scalable**: Easy to add new features and maintain
3. **Testable**: Comprehensive testing at all levels
4. **Deployable**: Ready for production environments
5. **Maintainable**: Clear separation of concerns
6. **Community-friendly**: Easy for contributors to understand

58
docs/SECURITY.md Normal file
View File

@@ -0,0 +1,58 @@
# Security Policy
## Supported Versions
We currently support the following versions with security updates:
| Version | Supported |
| ------- | ------------------ |
| 1.0.x | :white_check_mark: |
## Reporting a Vulnerability
We take the security of TechIRCd seriously. If you believe you have found a security vulnerability, please report it to us as described below.
**Please do not report security vulnerabilities through public GitHub issues.**
Instead, please report them via email to: security@techircd.org (or the maintainer's email)
Please include the following information (as much as you can provide) to help us better understand the nature and scope of the possible issue:
- Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
- Full paths of source file(s) related to the manifestation of the issue
- The location of the affected source code (tag/branch/commit or direct URL)
- Any special configuration required to reproduce the issue
- Step-by-step instructions to reproduce the issue
- Proof-of-concept or exploit code (if possible)
- Impact of the issue, including how an attacker might exploit the issue
This information will help us triage your report more quickly.
## Preferred Languages
We prefer all communications to be in English.
## Response Time
We will respond to your report within 48 hours and provide regular updates at least every 72 hours.
## Security Measures
TechIRCd implements several security measures:
- Input validation and sanitization
- Flood protection and rate limiting
- Connection timeout management
- Panic recovery mechanisms
- Memory usage monitoring
- Secure configuration validation
## Responsible Disclosure
We follow responsible disclosure practices:
1. **Report**: Submit vulnerability report privately
2. **Acknowledge**: We acknowledge receipt within 48 hours
3. **Investigate**: We investigate and develop a fix
4. **Coordinate**: We coordinate disclosure timeline with reporter
5. **Release**: We release security update and public disclosure

View File

@@ -0,0 +1,218 @@
# Services Integration Guide
TechIRCd is designed to work with external IRC services packages like Anope, Atheme, or IRCServices. This document explains how to set up services integration.
## Supported Services Packages
### Recommended: Anope Services
- **Website**: https://www.anope.org/
- **Features**: NickServ, ChanServ, OperServ, MemoServ, BotServ, HostServ
- **Database**: MySQL, PostgreSQL, SQLite
- **Protocol**: Full IRCd linking support
### Alternative: Atheme Services
- **Website**: https://atheme.github.io/
- **Features**: NickServ, ChanServ, OperServ, MemoServ
- **Database**: Multiple backend support
- **Protocol**: Standard IRC linking
## TechIRCd Services Integration Features
### 1. Standard IRC Protocol Support
TechIRCd implements the standard IRC protocol that services packages expect:
- **Server-to-Server linking** (`linking.json` configuration)
- **Standard IRC commands** (NICK, JOIN, PART, PRIVMSG, etc.)
- **Operator commands** (KILL, SQUIT, STATS, etc.)
- **User and channel modes**
- **IRCv3 capabilities** for enhanced features
### 2. Services Connection Methods
#### Method A: Services as Linked Server (Recommended)
Configure services to connect as a linked server using the linking configuration:
```json
{
"linking": {
"enable": true,
"links": [
{
"name": "services.yourdomain.com",
"host": "127.0.0.1",
"port": 6697,
"password": "services-link-password",
"auto_connect": true,
"hub": false,
"class": "services"
}
],
"classes": {
"services": {
"recvq": 16384,
"sendq": 16384,
"max_links": 1
}
}
}
}
```
#### Method B: Services as Local Client
Services can also connect as regular IRC clients with special privileges.
### 3. Services User Modes
TechIRCd supports standard services user modes:
- `+S` - Service mode (identifies services bots)
- `+o` - IRC Operator mode
- `+B` - Bot mode
- `+r` - Registered user mode
### 4. Channel Modes for Services
- `+r` - Registered channel
- `+R` - Registered users only
- `+M` - Registered/voiced users only speak
- `+S` - Services bots only
## Anope Configuration Example
### anope.conf snippet:
```
uplink
{
host = "127.0.0.1"
port = 6667
password = "link-password"
}
serverinfo
{
name = "services.yourdomain.com"
description = "Services for TechNet"
}
networkinfo
{
networkname = "TechNet"
ircd = "techircd"
}
```
### TechIRCd linking.json for Anope:
```json
{
"linking": {
"enable": true,
"server_port": 6697,
"password": "link-password",
"links": [
{
"name": "services.yourdomain.com",
"host": "127.0.0.1",
"port": 6697,
"password": "link-password",
"auto_connect": false,
"hub": false,
"class": "services"
}
]
}
}
```
## Setup Instructions
### 1. Configure TechIRCd
1. Edit `configs/linking.json` to add services link
2. Set up linking passwords
3. Configure operator access for services management
### 2. Install Anope Services
```bash
# Download and compile Anope
wget https://github.com/anope/anope/releases/latest
tar -xzf anope-*.tar.gz
cd anope-*
./configure --prefix=/opt/anope
make && make install
```
### 3. Configure Anope
1. Edit `/opt/anope/conf/services.conf`
2. Set TechIRCd as the uplink server
3. Configure database settings
4. Set up services nicknames and channels
### 4. Start Services
```bash
# Start TechIRCd first
./techircd start
# Then start Anope services
/opt/anope/bin/anoperc start
```
## Services Protocol Support
TechIRCd supports the standard IRC server protocol features needed by services:
### User Management
- `NICK` - Nickname changes
- `USER` - User registration
- `QUIT` - User disconnection
- `KILL` - Force disconnect users
### Channel Management
- `JOIN/PART` - Channel membership
- `MODE` - Channel and user modes
- `TOPIC` - Channel topics
- `KICK/BAN` - Channel moderation
### Network Management
- `SQUIT` - Server disconnection
- `SERVER` - Server introduction
- `BURST` - Network state synchronization
- `STATS` - Server statistics
### Services-Specific
- `SVSNICK` - Services nickname enforcement
- `SVSMODE` - Services mode changes
- `SVSJOIN` - Services-forced joins
- `SVSPART` - Services-forced parts
## Troubleshooting
### Services Won't Connect
1. Check linking password in both configs
2. Verify port configuration
3. Check TechIRCd logs for connection attempts
4. Ensure services hostname resolves
### Services Commands Not Working
1. Verify services have operator status
2. Check services user modes (+S +o)
3. Review channel access levels
4. Check services configuration
### Database Issues
Services handle their own database - TechIRCd doesn't need database access.
## Benefits of External Services
### For TechIRCd:
-**Simplified codebase** - Focus on IRC protocol
-**Better performance** - No database overhead
-**Higher reliability** - Services crashes don't affect IRC
-**Easier maintenance** - Separate concerns
### For Users:
-**Mature services** - Anope/Atheme are battle-tested
-**Rich features** - Full services functionality
-**Database choice** - MySQL, PostgreSQL, SQLite
-**Web interfaces** - Many services offer web panels
## Getting Help
- **TechIRCd Support**: GitHub Issues
- **Anope Support**: https://www.anope.org/
- **Atheme Support**: https://atheme.github.io/
- **IRC Help**: #anope or #atheme on irc.anope.org

208
docs/WHOIS_CONFIGURATION.md Normal file
View File

@@ -0,0 +1,208 @@
# WHOIS Configuration Guide
TechIRCd provides extremely flexible WHOIS configuration that allows administrators to control exactly what information is visible to different types of users.
## Overview
The WHOIS system in TechIRCd uses a three-tier permission system:
- **to_everyone**: Information visible to all users
- **to_opers**: Information visible only to IRC operators
- **to_self**: Information visible when users query themselves
## Configuration Options
### Basic Information Controls
#### User Modes (`show_user_modes`)
Controls who can see what user modes (+i, +w, +s, etc.) a user has set.
```json
"show_user_modes": {
"to_everyone": false,
"to_opers": true,
"to_self": true
}
```
#### SSL Status (`show_ssl_status`)
Shows whether a user is connected via SSL/TLS.
```json
"show_ssl_status": {
"to_everyone": true,
"to_opers": true,
"to_self": true
}
```
#### Idle Time (`show_idle_time`)
Shows how long a user has been idle and their signon time.
```json
"show_idle_time": {
"to_everyone": false,
"to_opers": true,
"to_self": true
}
```
#### Signon Time (`show_signon_time`)
Shows when a user connected to the server (alternative to idle time).
```json
"show_signon_time": {
"to_everyone": false,
"to_opers": true,
"to_self": true
}
```
#### Real Host (`show_real_host`)
Shows the user's real IP address/hostname (bypasses host masking).
```json
"show_real_host": {
"to_everyone": false,
"to_opers": true,
"to_self": true
}
```
### Channel Information (`show_channels`)
Controls channel visibility with additional granular options:
```json
"show_channels": {
"to_everyone": true,
"to_opers": true,
"to_self": true,
"hide_secret_channels": true,
"hide_private_channels": false,
"show_membership_levels": true
}
```
- `hide_secret_channels`: Hide channels with mode +s from non-members
- `hide_private_channels`: Hide channels with mode +p from non-members
- `show_membership_levels`: Show @/+/% prefixes for ops/voice/halfop
### Operator Information (`show_oper_class`)
Shows IRC operator class/type information.
```json
"show_oper_class": {
"to_everyone": false,
"to_opers": true,
"to_self": true
}
```
### Client Information (`show_client_info`)
Shows information about the IRC client software being used.
```json
"show_client_info": {
"to_everyone": false,
"to_opers": true,
"to_self": false
}
```
### Account Name (`show_account_name`)
Shows services account name (for networks with services integration).
```json
"show_account_name": {
"to_everyone": true,
"to_opers": true,
"to_self": true
}
```
## Advanced Features (Future)
These features are planned for future implementation:
- `show_activity_stats`: User activity analytics
- `show_github_integration`: GitHub profile integration
- `show_geolocation`: Approximate location information
- `show_performance_stats`: Connection performance metrics
- `show_device_info`: Device and OS information
- `show_social_graph`: Mutual channels and connections
- `show_security_score`: Account security rating
## Custom Fields
TechIRCd supports custom WHOIS fields for maximum flexibility:
```json
"custom_fields": [
{
"name": "website",
"to_everyone": true,
"to_opers": true,
"to_self": true,
"format": "Website: %s",
"description": "User's personal website"
}
]
```
## Example Configurations
### Maximum Privacy
Only show basic information to everyone, detailed info to opers:
```json
"whois_features": {
"show_user_modes": {"to_everyone": false, "to_opers": true, "to_self": true},
"show_ssl_status": {"to_everyone": false, "to_opers": true, "to_self": true},
"show_idle_time": {"to_everyone": false, "to_opers": true, "to_self": true},
"show_real_host": {"to_everyone": false, "to_opers": true, "to_self": true},
"show_channels": {
"to_everyone": true, "to_opers": true, "to_self": true,
"hide_secret_channels": true, "hide_private_channels": true,
"show_membership_levels": false
}
}
```
### Maximum Transparency
Show most information to everyone:
```json
"whois_features": {
"show_user_modes": {"to_everyone": true, "to_opers": true, "to_self": true},
"show_ssl_status": {"to_everyone": true, "to_opers": true, "to_self": true},
"show_idle_time": {"to_everyone": true, "to_opers": true, "to_self": true},
"show_real_host": {"to_everyone": false, "to_opers": true, "to_self": true},
"show_channels": {
"to_everyone": true, "to_opers": true, "to_self": true,
"hide_secret_channels": false, "hide_private_channels": false,
"show_membership_levels": true
}
}
```
### Development/Testing
Show everything to everyone for debugging:
```json
"whois_features": {
"show_user_modes": {"to_everyone": true, "to_opers": true, "to_self": true},
"show_ssl_status": {"to_everyone": true, "to_opers": true, "to_self": true},
"show_idle_time": {"to_everyone": true, "to_opers": true, "to_self": true},
"show_real_host": {"to_everyone": true, "to_opers": true, "to_self": true},
"show_client_info": {"to_everyone": true, "to_opers": true, "to_self": true}
}
```
## Notes
- The WHOIS system respects IRC operator privileges and host masking settings
- Secret and private channel hiding works in conjunction with channel membership
- All settings are hot-reloadable (restart server after config changes)
- The system is designed to be extremely flexible while maintaining IRC protocol compliance
This configuration system makes TechIRCd one of the most configurable IRC servers available!

263
extreme_stress.py Normal file
View File

@@ -0,0 +1,263 @@
#!/usr/bin/env python3
"""
EXTREME TechIRCd Stress Testing Tool
BRUTAL IRC server stress testing designed to break things
"""
import asyncio
import random
import string
import time
import logging
import socket
import sys
from typing import List
import threading
class ExtremeIRCClient:
def __init__(self, client_id: int):
self.client_id = client_id
self.nick = f"EXTREME{client_id:05d}"
self.reader = None
self.writer = None
self.connected = False
self.message_count = 0
async def connect(self):
try:
self.reader, self.writer = await asyncio.wait_for(
asyncio.open_connection('localhost', 6667),
timeout=5.0
)
self.connected = True
return True
except Exception as e:
return False
async def register(self):
if not self.connected or self.writer is None:
return False
try:
self.writer.write(f"NICK {self.nick}\r\n".encode())
self.writer.write(f"USER {self.nick} 0 * :Extreme Client {self.client_id}\r\n".encode())
await self.writer.drain()
# Don't wait for response - BRUTAL mode
return True
except:
return False
async def spam_messages(self, count: int, delay: float = 0.001):
"""Send messages as fast as possible"""
if not self.connected or self.writer is None:
return
messages = [
f"PRIVMSG #extreme :SPAM MESSAGE {i} FROM {self.nick} - " + "X" * 100
for i in range(count)
]
try:
for msg in messages:
self.writer.write(f"{msg}\r\n".encode())
self.message_count += 1
if delay > 0:
await asyncio.sleep(delay)
await self.writer.drain()
except:
pass
async def chaos_commands(self):
"""Send random commands rapidly"""
commands = [
"JOIN #extreme",
"PART #extreme",
"JOIN #chaos1,#chaos2,#chaos3,#chaos4,#chaos5",
"LIST",
"WHO #extreme",
"VERSION",
"TIME",
"ADMIN",
"INFO",
"LUSERS",
f"NICK {self.nick}_CHAOS_{random.randint(1000,9999)}",
"PING :test",
]
try:
if self.writer is not None:
for _ in range(50): # 50 rapid commands
cmd = random.choice(commands)
self.writer.write(f"{cmd}\r\n".encode())
await asyncio.sleep(0.01) # 10ms between commands
await self.writer.drain()
except:
pass
async def disconnect(self):
if self.writer:
try:
self.writer.write(b"QUIT :EXTREME TEST COMPLETE\r\n")
await self.writer.drain()
self.writer.close()
await self.writer.wait_closed()
except:
pass
self.connected = False
class ExtremeStressTester:
def __init__(self):
self.clients = []
self.total_connections = 0
self.successful_connections = 0
self.total_messages = 0
self.start_time = None
async def extreme_connection_bomb(self, count: int):
"""Connect as many clients as possible simultaneously"""
print(f"🔥 EXTREME: Launching {count} connections simultaneously...")
tasks = []
for i in range(count):
client = ExtremeIRCClient(i)
self.clients.append(client)
tasks.append(self.connect_and_register(client))
# Launch ALL connections at once - BRUTAL
results = await asyncio.gather(*tasks, return_exceptions=True)
self.successful_connections = sum(1 for r in results if r is True)
print(f"💀 Connection bomb result: {self.successful_connections}/{count} successful")
async def connect_and_register(self, client):
try:
if await client.connect():
await client.register()
return True
except:
pass
return False
async def extreme_message_hurricane(self, messages_per_client: int = 200):
"""Send massive amounts of messages from all clients"""
print(f"🌪️ EXTREME: Message hurricane - {messages_per_client} msgs per client...")
connected_clients = [c for c in self.clients if c.connected]
print(f"💀 Using {len(connected_clients)} connected clients")
tasks = []
for client in connected_clients:
tasks.append(client.spam_messages(messages_per_client, delay=0.001)) # 1ms delay
await asyncio.gather(*tasks, return_exceptions=True)
total_msgs = sum(c.message_count for c in connected_clients)
print(f"💀 Hurricane complete: {total_msgs} total messages sent")
async def extreme_chaos_mode(self):
"""Every client sends random commands rapidly"""
print(f"🔥 EXTREME: CHAOS MODE ACTIVATED...")
connected_clients = [c for c in self.clients if c.connected]
tasks = []
for client in connected_clients:
tasks.append(client.chaos_commands())
await asyncio.gather(*tasks, return_exceptions=True)
print(f"💀 Chaos mode complete")
async def rapid_fire_connections(self, total: int, batch_size: int = 50):
"""Connect clients in rapid batches"""
print(f"⚡ EXTREME: Rapid fire - {total} clients in batches of {batch_size}")
for batch_start in range(0, total, batch_size):
batch_end = min(batch_start + batch_size, total)
batch_clients = []
# Create batch
for i in range(batch_start, batch_end):
client = ExtremeIRCClient(i + 10000) # Offset IDs
batch_clients.append(client)
self.clients.append(client)
# Connect batch simultaneously
tasks = [self.connect_and_register(c) for c in batch_clients]
results = await asyncio.gather(*tasks, return_exceptions=True)
successful = sum(1 for r in results if r is True)
print(f"💀 Batch {batch_start//batch_size + 1}: {successful}/{len(batch_clients)} connected")
# Tiny delay between batches
await asyncio.sleep(0.1)
async def cleanup(self):
"""Disconnect all clients"""
print("🧹 Cleaning up connections...")
tasks = []
for client in self.clients:
if client.connected:
tasks.append(client.disconnect())
if tasks:
await asyncio.gather(*tasks, return_exceptions=True)
print(f"💀 Cleanup complete. Total clients created: {len(self.clients)}")
async def run_extreme_test():
"""Run the most brutal stress test possible"""
print("💀💀💀 EXTREME STRESS TEST STARTING 💀💀💀")
print("⚠️ WARNING: This test is designed to break things!")
print()
tester = ExtremeStressTester()
start_time = time.time()
try:
# Phase 1: Connection bomb
await tester.extreme_connection_bomb(150)
await asyncio.sleep(2)
# Phase 2: Message hurricane
await tester.extreme_message_hurricane(100)
await asyncio.sleep(1)
# Phase 3: Chaos mode
await tester.extreme_chaos_mode()
await asyncio.sleep(1)
# Phase 4: More connections while under load
await tester.rapid_fire_connections(100, 25)
await asyncio.sleep(2)
# Phase 5: Final message barrage
await tester.extreme_message_hurricane(50)
except KeyboardInterrupt:
print("\n🛑 Test interrupted by user")
except Exception as e:
print(f"\n💥 Test crashed: {e}")
finally:
# Always cleanup
await tester.cleanup()
end_time = time.time()
duration = end_time - start_time
print(f"\n💀 EXTREME STRESS TEST COMPLETE 💀")
print(f"Duration: {duration:.2f} seconds")
print(f"Total clients: {len(tester.clients)}")
print(f"Successful connections: {tester.successful_connections}")
total_messages = sum(c.message_count for c in tester.clients)
print(f"Total messages sent: {total_messages}")
if duration > 0:
print(f"Messages per second: {total_messages/duration:.2f}")
if __name__ == "__main__":
print("💀 EXTREME STRESS TESTER 💀")
print("This will attempt to overwhelm the IRC server")
print("Make sure TechIRCd is running!")
print()
try:
asyncio.run(run_extreme_test())
except KeyboardInterrupt:
print("\n🛑 Aborted by user")

105
extreme_stress.sh Normal file
View File

@@ -0,0 +1,105 @@
#!/bin/bash
# EXTREME TechIRCd Stress Test Runner
# This script runs progressively more brutal stress tests
echo "💀💀💀 EXTREME STRESS TEST SUITE 💀💀💀"
echo ""
# Check if TechIRCd is running
if ! pgrep -f "techircd" > /dev/null; then
echo "🚨 TechIRCd is not running! Starting it..."
./techircd start &
sleep 3
echo "✅ TechIRCd started"
fi
echo "🔥 Available EXTREME stress tests:"
echo ""
echo "1. extreme_bomb - 150 simultaneous connections + message flood"
echo "2. extreme_rapid - Rapid fire connection test (original + extreme scenarios)"
echo "3. extreme_nuclear - THE NUCLEAR OPTION (300 clients, everything at max)"
echo "4. extreme_python - Custom Python brutality test"
echo "5. extreme_all - ALL EXTREME TESTS IN SEQUENCE"
echo ""
if [ $# -eq 0 ]; then
echo "Usage: $0 <test_name>"
echo "Example: $0 extreme_bomb"
exit 1
fi
TEST_TYPE=$1
case $TEST_TYPE in
extreme_bomb)
echo "💣 Running EXTREME Mass Connection Bomb..."
python3 stress_test.py --scenario "EXTREME: Mass Connection Bomb"
;;
extreme_rapid)
echo "⚡ Running EXTREME Rapid Fire Connections..."
python3 stress_test.py --scenario "EXTREME: Rapid Fire Connections"
;;
extreme_nuclear)
echo "☢️ Running THE NUCLEAR OPTION..."
echo "⚠️ WARNING: This test uses 300 clients!"
python3 stress_test.py --scenario "EXTREME: The Nuclear Option"
;;
extreme_python)
echo "🐍 Running Custom Python Brutality Test..."
python3 extreme_stress.py
;;
extreme_hurricane)
echo "🌪️ Running EXTREME Message Hurricane..."
python3 stress_test.py --scenario "EXTREME: Message Hurricane"
;;
extreme_chaos)
echo "🔥 Running EXTREME Connection Chaos..."
python3 stress_test.py --scenario "EXTREME: Connection Chaos"
;;
extreme_all)
echo "💀 RUNNING ALL EXTREME TESTS IN SEQUENCE..."
echo "⚠️ This will be BRUTAL on your server!"
read -p "Are you sure? (yes/NO): " confirm
if [ "$confirm" = "yes" ]; then
echo ""
echo "🚀 Starting extreme test sequence..."
echo "1/6: Mass Connection Bomb"
python3 stress_test.py --scenario "EXTREME: Mass Connection Bomb"
sleep 5
echo "2/6: Rapid Fire Connections"
python3 stress_test.py --scenario "EXTREME: Rapid Fire Connections"
sleep 5
echo "3/6: Message Hurricane"
python3 stress_test.py --scenario "EXTREME: Message Hurricane"
sleep 5
echo "4/6: Connection Chaos"
python3 stress_test.py --scenario "EXTREME: Connection Chaos"
sleep 5
echo "5/6: Custom Python Brutality"
python3 extreme_stress.py
sleep 10
echo "6/6: THE NUCLEAR OPTION"
python3 stress_test.py --scenario "EXTREME: The Nuclear Option"
echo "💀 ALL EXTREME TESTS COMPLETE 💀"
else
echo "❌ Extreme test sequence cancelled"
fi
;;
*)
echo "❌ Unknown test type: $TEST_TYPE"
echo "Available: extreme_bomb, extreme_rapid, extreme_nuclear, extreme_python, extreme_hurricane, extreme_chaos, extreme_all"
exit 1
;;
esac
echo ""
echo "🏁 Stress test complete!"
echo "Check server logs for performance data"

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module github.com/ComputerTech312/TechIRCd
go 1.21

99
health.go Normal file
View File

@@ -0,0 +1,99 @@
package main
import (
"log"
"runtime"
"sync/atomic"
"time"
)
// HealthMonitor tracks server health metrics
type HealthMonitor struct {
server *Server
totalClients int64
totalMessages int64
startTime time.Time
ticker *time.Ticker
shutdown chan bool
}
func NewHealthMonitor(server *Server) *HealthMonitor {
return &HealthMonitor{
server: server,
startTime: time.Now(),
shutdown: make(chan bool),
}
}
func (h *HealthMonitor) Start() {
h.ticker = time.NewTicker(5 * time.Minute) // Log stats every 5 minutes
go h.monitor()
}
func (h *HealthMonitor) Stop() {
if h.ticker != nil {
h.ticker.Stop()
}
close(h.shutdown)
}
func (h *HealthMonitor) IncrementClients() {
atomic.AddInt64(&h.totalClients, 1)
}
func (h *HealthMonitor) DecrementClients() {
atomic.AddInt64(&h.totalClients, -1)
}
func (h *HealthMonitor) IncrementMessages() {
atomic.AddInt64(&h.totalMessages, 1)
}
func (h *HealthMonitor) monitor() {
for {
select {
case <-h.ticker.C:
h.logHealthStats()
case <-h.shutdown:
return
}
}
}
func (h *HealthMonitor) logHealthStats() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
h.server.mu.RLock()
clientCount := len(h.server.clients)
channelCount := len(h.server.channels)
h.server.mu.RUnlock()
totalClients := atomic.LoadInt64(&h.totalClients)
totalMessages := atomic.LoadInt64(&h.totalMessages)
uptime := time.Since(h.startTime)
log.Printf("Health Stats - Uptime: %v, Clients: %d, Channels: %d, Total Clients: %d, Total Messages: %d",
uptime.Round(time.Second), clientCount, channelCount, totalClients, totalMessages)
log.Printf("Memory Stats - Alloc: %d KB, Sys: %d KB, NumGC: %d, Goroutines: %d",
bToKb(m.Alloc), bToKb(m.Sys), m.NumGC, runtime.NumGoroutine())
// Alert if memory usage is high
if m.Alloc > 100*1024*1024 { // 100MB
log.Printf("WARNING: High memory usage detected: %d MB", bToMb(m.Alloc))
}
// Alert if too many goroutines
if runtime.NumGoroutine() > 1000 {
log.Printf("WARNING: High goroutine count: %d", runtime.NumGoroutine())
}
}
func bToKb(b uint64) uint64 {
return b / 1024
}
func bToMb(b uint64) uint64 {
return b / 1024 / 1024
}

File diff suppressed because it is too large Load Diff

397
linking.go Normal file
View File

@@ -0,0 +1,397 @@
package main
import (
"fmt"
"log"
"net"
"strings"
"sync"
"time"
)
// LinkedServer represents a connection to another IRC server
type LinkedServer struct {
name string
conn net.Conn
host string
port int
password string
hub bool
description string
connected bool
lastPing time.Time
server *Server
mu sync.RWMutex
}
// LinkConfig represents configuration for server linking
type LinkConfig struct {
Enable bool `json:"enable"`
ServerPort int `json:"server_port"`
Password string `json:"password"`
Hub bool `json:"hub"`
AutoConnect bool `json:"auto_connect"`
Links []struct {
Name string `json:"name"`
Host string `json:"host"`
Port int `json:"port"`
Password string `json:"password"`
AutoConnect bool `json:"auto_connect"`
Hub bool `json:"hub"`
Description string `json:"description"`
} `json:"links"`
}
// ServerMessage represents a server-to-server message
type ServerMessage struct {
Prefix string
Command string
Params []string
Source string
Target string
}
// NewLinkedServer creates a new linked server connection
func NewLinkedServer(name, host string, port int, password string, hub bool, description string, server *Server) *LinkedServer {
return &LinkedServer{
name: name,
host: host,
port: port,
password: password,
hub: hub,
description: description,
server: server,
connected: false,
lastPing: time.Now(),
}
}
// Connect establishes a connection to the remote server
func (ls *LinkedServer) Connect() error {
ls.mu.Lock()
defer ls.mu.Unlock()
if ls.connected {
return fmt.Errorf("server %s is already connected", ls.name)
}
// Connect to remote server
var addr string
if strings.Contains(ls.host, ":") {
// IPv6 address, enclose in brackets
addr = fmt.Sprintf("[%s]:%d", ls.host, ls.port)
} else {
addr = fmt.Sprintf("%s:%d", ls.host, ls.port)
}
conn, err := net.DialTimeout("tcp", addr, 30*time.Second)
if err != nil {
return fmt.Errorf("failed to connect to %s: %v", ls.name, err)
}
ls.conn = conn
ls.connected = true
log.Printf("Connected to server %s at %s", ls.name, addr)
// Start handling the connection
go ls.Handle()
// Send server introduction
ls.SendServerAuth()
return nil
}
// Disconnect closes the connection to the remote server
func (ls *LinkedServer) Disconnect() {
ls.mu.Lock()
defer ls.mu.Unlock()
if !ls.connected {
return
}
if ls.conn != nil {
ls.conn.Close()
ls.conn = nil
}
ls.connected = false
log.Printf("Disconnected from server %s", ls.name)
}
// SendServerAuth sends authentication to the remote server
func (ls *LinkedServer) SendServerAuth() {
// Send PASS command
ls.SendMessage(fmt.Sprintf("PASS %s", ls.password))
// Send SERVER command
ls.SendMessage(fmt.Sprintf("SERVER %s 1 :%s", ls.server.config.Server.Name, ls.server.config.Server.Description))
log.Printf("Sent authentication to server %s", ls.name)
}
// SendMessage sends a raw message to the linked server
func (ls *LinkedServer) SendMessage(message string) error {
ls.mu.RLock()
defer ls.mu.RUnlock()
if !ls.connected || ls.conn == nil {
return fmt.Errorf("server %s is not connected", ls.name)
}
_, err := fmt.Fprintf(ls.conn, "%s\r\n", message)
if err != nil {
log.Printf("Error sending message to server %s: %v", ls.name, err)
ls.connected = false
}
return err
}
// Handle processes messages from the linked server
func (ls *LinkedServer) Handle() {
defer func() {
if r := recover(); r != nil {
log.Printf("Panic in server link handler for %s: %v", ls.name, r)
}
ls.Disconnect()
}()
scanner := net.Conn(ls.conn)
buffer := make([]byte, 4096)
for ls.connected {
// Set read deadline
ls.conn.SetReadDeadline(time.Now().Add(5 * time.Minute))
n, err := scanner.Read(buffer)
if err != nil {
if ls.connected {
log.Printf("Read error from server %s: %v", ls.name, err)
}
break
}
data := string(buffer[:n])
lines := strings.Split(strings.TrimSpace(data), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
ls.handleServerMessage(line)
}
}
}
// handleServerMessage processes a single message from the linked server
func (ls *LinkedServer) handleServerMessage(line string) {
log.Printf("Received from server %s: %s", ls.name, line)
parts := strings.Fields(line)
if len(parts) == 0 {
return
}
var command string
var params []string
// Parse message prefix
if parts[0][0] == ':' {
parts = parts[1:]
}
if len(parts) == 0 {
return
}
command = strings.ToUpper(parts[0])
params = parts[1:]
// Handle server-to-server commands
switch command {
case "PASS":
ls.handleServerPass(params)
case "SERVER":
ls.handleServerIntro(params)
case "PING":
ls.handleServerPing(params)
case "PONG":
ls.handleServerPong()
case "SQUIT":
ls.handleServerQuit(params)
case "NICK":
ls.handleRemoteNick(params)
case "USER":
ls.handleRemoteUser(params)
case "JOIN":
ls.handleRemoteJoin(params)
case "PART":
ls.handleRemotePart(params)
case "QUIT":
ls.handleRemoteQuit(params)
case "PRIVMSG":
ls.handleRemotePrivmsg(params)
default:
log.Printf("Unknown server command from %s: %s", ls.name, command)
}
}
// handleServerPass handles PASS command from remote server
func (ls *LinkedServer) handleServerPass(params []string) {
if len(params) < 1 {
log.Printf("Invalid PASS from server %s", ls.name)
ls.Disconnect()
return
}
password := params[0]
if password != ls.password {
log.Printf("Invalid password from server %s", ls.name)
ls.Disconnect()
return
}
log.Printf("Server %s authenticated successfully", ls.name)
}
// handleServerIntro handles SERVER command from remote server
func (ls *LinkedServer) handleServerIntro(params []string) {
if len(params) < 3 {
log.Printf("Invalid SERVER from server %s", ls.name)
ls.Disconnect()
return
}
serverName := params[0]
hopCount := params[1]
description := strings.Join(params[2:], " ")
if description[0] == ':' {
description = description[1:]
}
log.Printf("Server introduced: %s (hops: %s) - %s", serverName, hopCount, description)
// Send our user list and channel list to the remote server
ls.sendBurst()
}
// handleServerPing handles PING from remote server
func (ls *LinkedServer) handleServerPing(params []string) {
if len(params) < 1 {
return
}
token := params[0]
ls.SendMessage(fmt.Sprintf("PONG %s %s", ls.server.config.Server.Name, token))
}
// handleServerPong handles PONG from remote server
func (ls *LinkedServer) handleServerPong() {
ls.mu.Lock()
ls.lastPing = time.Now()
ls.mu.Unlock()
}
// handleServerQuit handles SQUIT from remote server
func (ls *LinkedServer) handleServerQuit(params []string) {
if len(params) >= 1 {
reason := strings.Join(params, " ")
log.Printf("Server %s quit: %s", ls.name, reason)
}
ls.Disconnect()
}
// sendBurst sends initial data to newly connected server
func (ls *LinkedServer) sendBurst() {
// Send all local users
for _, client := range ls.server.GetClients() {
if client.IsRegistered() {
ls.SendMessage(fmt.Sprintf("NICK %s 1 %d %s %s %s %s :%s",
client.Nick(),
time.Now().Unix(),
client.User(),
client.Host(),
ls.server.config.Server.Name,
client.Nick(),
client.Realname()))
}
}
// Send all channels and their members
for _, channel := range ls.server.GetChannels() {
// Send channel creation
ls.SendMessage(fmt.Sprintf("NJOIN %s :", channel.name))
// Send channel members
var members []string
for _, client := range channel.GetClients() {
members = append(members, client.Nick())
}
if len(members) > 0 {
ls.SendMessage(fmt.Sprintf("NJOIN %s :%s", channel.name, strings.Join(members, " ")))
}
// Send channel topic if exists
if channel.topic != "" {
ls.SendMessage(fmt.Sprintf("TOPIC %s :%s", channel.name, channel.topic))
}
}
// End of burst
ls.SendMessage("EOB")
log.Printf("Sent burst to server %s", ls.name)
}
// Remote user handling functions
func (ls *LinkedServer) handleRemoteNick(params []string) {
// Handle remote NICK command
log.Printf("Remote NICK from %s: %s", ls.name, strings.Join(params, " "))
}
func (ls *LinkedServer) handleRemoteUser(params []string) {
// Handle remote USER command
log.Printf("Remote USER from %s: %s", ls.name, strings.Join(params, " "))
}
func (ls *LinkedServer) handleRemoteJoin(params []string) {
// Handle remote JOIN command
log.Printf("Remote JOIN from %s: %s", ls.name, strings.Join(params, " "))
}
func (ls *LinkedServer) handleRemotePart(params []string) {
// Handle remote PART command
log.Printf("Remote PART from %s: %s", ls.name, strings.Join(params, " "))
}
func (ls *LinkedServer) handleRemoteQuit(params []string) {
// Handle remote QUIT command
log.Printf("Remote QUIT from %s: %s", ls.name, strings.Join(params, " "))
}
func (ls *LinkedServer) handleRemotePrivmsg(params []string) {
// Handle remote PRIVMSG command
log.Printf("Remote PRIVMSG from %s: %s", ls.name, strings.Join(params, " "))
}
// IsConnected returns whether the server is currently connected
func (ls *LinkedServer) IsConnected() bool {
ls.mu.RLock()
defer ls.mu.RUnlock()
return ls.connected
}
// Name returns the server name
func (ls *LinkedServer) Name() string {
return ls.name
}
// Description returns the server description
func (ls *LinkedServer) Description() string {
return ls.description
}

334
main.go Normal file
View File

@@ -0,0 +1,334 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"os/exec"
"os/signal"
"strconv"
"strings"
"syscall"
"time"
)
var DebugMode bool
const (
VERSION = "1.0.0"
PIDFILE = "techircd.pid"
CONFIGFILE = "config.json"
)
func main() {
// Parse command line arguments
if len(os.Args) < 2 {
showUsage()
os.Exit(1)
}
command := strings.ToLower(os.Args[1])
// Parse flags for the remaining arguments
flagSet := flag.NewFlagSet("techircd", flag.ExitOnError)
configFile := flagSet.String("config", CONFIGFILE, "Path to configuration file")
daemon := flagSet.Bool("daemon", false, "Run as daemon (background)")
verbose := flagSet.Bool("verbose", false, "Enable verbose logging")
debug := flagSet.Bool("debug", false, "Enable extremely detailed debug logging (shows all IRC messages)")
port := flagSet.Int("port", 0, "Override port from config")
// Parse remaining arguments
flagSet.Parse(os.Args[2:])
switch command {
case "start":
startServer(*configFile, *daemon, *verbose, *debug, *port)
case "stop":
stopServer()
case "restart":
stopServer()
time.Sleep(2 * time.Second)
startServer(*configFile, *daemon, *verbose, *debug, *port)
case "status":
showStatus()
case "reload":
reloadConfig()
case "version", "-v", "--version":
fmt.Printf("TechIRCd version %s\n", VERSION)
case "help", "-h", "--help":
showUsage()
default:
fmt.Printf("Unknown command: %s\n", command)
showUsage()
os.Exit(1)
}
}
func showUsage() {
fmt.Printf(`TechIRCd %s - Modern IRC Server
Usage: %s <command> [options]
Commands:
start Start the IRC server
stop Stop the IRC server
restart Restart the IRC server
status Show server status
reload Reload configuration
version Show version information
help Show this help message
Options:
-config <file> Path to configuration file (default: config.json)
-daemon Run as daemon (background process)
-verbose Enable verbose logging
-debug Enable extremely detailed debug logging (shows all IRC messages)
-port <port> Override port from configuration
Examples:
%s start # Start with default config
%s start -config custom.json # Start with custom config
%s start -daemon # Start as background daemon
%s stop # Stop the server
%s status # Check if server is running
`, VERSION, os.Args[0], os.Args[0], os.Args[0], os.Args[0], os.Args[0], os.Args[0])
}
func startServer(configFile string, daemon, verbose, debug bool, port int) {
// Check if server is already running
if isRunning() {
fmt.Println("TechIRCd is already running")
os.Exit(1)
}
// Load configuration
config, err := LoadConfig(configFile)
if err != nil {
log.Printf("Failed to load config from %s, using defaults: %v", configFile, err)
config = DefaultConfig()
if err := SaveConfig(config, configFile); err != nil {
log.Printf("Failed to save default config: %v", err)
}
}
// Override port if specified
if port > 0 {
config.Server.Listen.Port = port
fmt.Printf("Port overridden to %d\n", port)
}
// Validate configuration
if err := config.Validate(); err != nil {
log.Fatalf("Configuration validation failed: %v", err)
}
config.SanitizeConfig()
if verbose {
log.Println("Configuration validated successfully")
}
// Set global debug mode
DebugMode = debug
if debug {
log.Println("Debug mode enabled - will log all IRC messages")
}
// Start as daemon if requested
if daemon {
startDaemon(configFile, verbose, debug, port)
return
}
// Write PID file
if err := writePidFile(); err != nil {
log.Fatalf("Failed to write PID file: %v", err)
}
defer removePidFile()
// Create server
server := NewServer(config)
// Setup signal handling for graceful shutdown with forced shutdown capability
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
shutdownInProgress := false
go func() {
for sig := range c {
switch sig {
case syscall.SIGHUP:
log.Println("Received SIGHUP, reloading configuration...")
// Reload config here if needed
case os.Interrupt, syscall.SIGTERM:
if shutdownInProgress {
log.Println("Received second interrupt signal, forcing immediate shutdown...")
removePidFile()
os.Exit(1) // Force exit
}
shutdownInProgress = true
log.Println("Shutting down server...")
// Start shutdown with timeout
shutdownComplete := make(chan bool, 1)
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("Panic during shutdown: %v", r)
}
shutdownComplete <- true
}()
server.Shutdown()
}()
// Wait for graceful shutdown or force after timeout
select {
case <-shutdownComplete:
log.Println("Graceful shutdown completed")
case <-time.After(10 * time.Second):
log.Println("Shutdown timeout reached, forcing exit...")
}
removePidFile()
os.Exit(0)
}
}
}()
// Start the server
fmt.Printf("Starting TechIRCd %s on %s:%d\n", VERSION, config.Server.Listen.Host, config.Server.Listen.Port)
if config.Server.Listen.EnableSSL {
fmt.Printf("SSL enabled on port %d\n", config.Server.Listen.SSLPort)
}
if err := server.Start(); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}
func startDaemon(configFile string, verbose, debug bool, port int) {
// Build command arguments
args := []string{os.Args[0], "start", "-config", configFile}
if verbose {
args = append(args, "-verbose")
}
if debug {
args = append(args, "-debug")
}
if port > 0 {
args = append(args, "-port", strconv.Itoa(port))
}
// Start as background process
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdout = nil
cmd.Stderr = nil
cmd.Stdin = nil
if err := cmd.Start(); err != nil {
log.Fatalf("Failed to start daemon: %v", err)
}
fmt.Printf("TechIRCd started as daemon (PID: %d)\n", cmd.Process.Pid)
}
func stopServer() {
pid, err := readPidFile()
if err != nil {
fmt.Println("TechIRCd is not running")
return
}
process, err := os.FindProcess(pid)
if err != nil {
fmt.Println("TechIRCd is not running")
removePidFile()
return
}
// Send SIGTERM for graceful shutdown
if err := process.Signal(syscall.SIGTERM); err != nil {
fmt.Println("TechIRCd is not running")
removePidFile()
return
}
// Wait for process to stop
for i := 0; i < 10; i++ {
if !isRunning() {
fmt.Println("TechIRCd stopped")
return
}
time.Sleep(500 * time.Millisecond)
}
// Force kill if still running
process.Signal(syscall.SIGKILL)
fmt.Println("TechIRCd force stopped")
removePidFile()
}
func showStatus() {
if isRunning() {
pid, _ := readPidFile()
fmt.Printf("TechIRCd is running (PID: %d)\n", pid)
} else {
fmt.Println("TechIRCd is not running")
}
}
func reloadConfig() {
pid, err := readPidFile()
if err != nil {
fmt.Println("TechIRCd is not running")
return
}
process, err := os.FindProcess(pid)
if err != nil {
fmt.Println("TechIRCd is not running")
return
}
if err := process.Signal(syscall.SIGHUP); err != nil {
fmt.Println("Failed to reload configuration")
return
}
fmt.Println("Configuration reload signal sent")
}
func isRunning() bool {
pid, err := readPidFile()
if err != nil {
return false
}
process, err := os.FindProcess(pid)
if err != nil {
return false
}
// Send signal 0 to check if process exists
err = process.Signal(syscall.Signal(0))
return err == nil
}
func writePidFile() error {
pid := os.Getpid()
return os.WriteFile(PIDFILE, []byte(strconv.Itoa(pid)), 0644)
}
func readPidFile() (int, error) {
data, err := os.ReadFile(PIDFILE)
if err != nil {
return 0, err
}
return strconv.Atoi(strings.TrimSpace(string(data)))
}
func removePidFile() {
os.Remove(PIDFILE)
}

138
monitoring_analytics.go Normal file
View File

@@ -0,0 +1,138 @@
package main
import (
"time"
)
// Advanced monitoring and analytics
type MonitoringConfig struct {
Prometheus struct {
Enable bool `json:"enable"`
Port int `json:"port"`
Path string `json:"path"`
} `json:"prometheus"`
Grafana struct {
Enable bool `json:"enable"`
Dashboard string `json:"dashboard_url"`
} `json:"grafana"`
Logging struct {
Level string `json:"level"`
Format string `json:"format"` // json, text
Output string `json:"output"` // file, stdout, syslog
Structured bool `json:"structured"`
} `json:"logging"`
}
// Metrics collection
type ServerMetrics struct {
// Connection metrics
TotalConnections int64 `metric:"total_connections"`
ActiveConnections int `metric:"active_connections"`
ConnectionsPerSecond float64 `metric:"connections_per_second"`
FailedConnections int64 `metric:"failed_connections"`
// Message metrics
MessagesPerSecond float64 `metric:"messages_per_second"`
TotalMessages int64 `metric:"total_messages"`
PrivateMessages int64 `metric:"private_messages"`
ChannelMessages int64 `metric:"channel_messages"`
// Channel metrics
TotalChannels int `metric:"total_channels"`
AverageChannelSize float64 `metric:"average_channel_size"`
LargestChannel int `metric:"largest_channel"`
// Performance metrics
CPUUsage float64 `metric:"cpu_usage"`
MemoryUsage int64 `metric:"memory_usage"`
GoroutineCount int `metric:"goroutine_count"`
ResponseTime time.Duration `metric:"response_time"`
// Network metrics
BytesSent int64 `metric:"bytes_sent"`
BytesReceived int64 `metric:"bytes_received"`
NetworkLatency time.Duration `metric:"network_latency"`
}
// Real-time analytics
type AnalyticsEngine struct {
UserActivity map[string]*UserActivityMetrics
ChannelAnalytics map[string]*ChannelAnalytics
NetworkHealth *NetworkHealthMetrics
}
type UserActivityMetrics struct {
MessagesPerHour []int `json:"messages_per_hour"`
ChannelsJoined []string `json:"channels_joined"`
CommandsUsed map[string]int `json:"commands_used"`
ConnectionTime time.Duration `json:"connection_time"`
LastActivity time.Time `json:"last_activity"`
}
type ChannelAnalytics struct {
MessageCount int64 `json:"message_count"`
UniqueUsers int `json:"unique_users"`
AverageUsers float64 `json:"average_users"`
PeakUsers int `json:"peak_users"`
MostActiveUsers []string `json:"most_active_users"`
TopicChanges int `json:"topic_changes"`
}
type NetworkHealthMetrics struct {
Uptime time.Duration `json:"uptime"`
ServerLoad float64 `json:"server_load"`
ErrorRate float64 `json:"error_rate"`
ResponseTime time.Duration `json:"response_time"`
LinkedServerCount int `json:"linked_servers"`
NetworkSplit bool `json:"network_split"`
}
// Alert system
type AlertManager struct {
Rules []AlertRule `json:"rules"`
Channels []AlertChannel `json:"channels"`
}
type AlertRule struct {
Name string `json:"name"`
Condition string `json:"condition"` // "cpu_usage > 80"
Threshold float64 `json:"threshold"`
Duration int `json:"duration"` // seconds
Severity string `json:"severity"` // info, warning, critical
Actions []string `json:"actions"`
}
type AlertChannel struct {
Type string `json:"type"` // email, slack, discord, webhook
Config map[string]string `json:"config"`
}
// Performance profiling
func (s *Server) StartProfiling() {
// Enable CPU and memory profiling
}
func (s *Server) GeneratePerformanceReport() *PerformanceReport {
// Generate detailed performance report
return &PerformanceReport{}
}
type PerformanceReport struct {
Timestamp time.Time `json:"timestamp"`
CPUProfile []CPUSample `json:"cpu_profile"`
MemoryProfile []MemorySample `json:"memory_profile"`
Bottlenecks []string `json:"bottlenecks"`
Recommendations []string `json:"recommendations"`
}
type CPUSample struct {
Function string `json:"function"`
Usage float64 `json:"usage"`
}
type MemorySample struct {
Object string `json:"object"`
Size int64 `json:"size"`
}

339
oper_config.go Normal file
View File

@@ -0,0 +1,339 @@
package main
import (
"encoding/json"
"fmt"
"os"
)
// OperClass defines an operator class with specific permissions
type OperClass struct {
Name string `json:"name"`
Rank int `json:"rank"` // Higher number = higher rank
Description string `json:"description"`
Permissions []string `json:"permissions"`
Inherits string `json:"inherits"` // Inherit permissions from another class
Color string `json:"color"` // Color for display purposes
Symbol string `json:"symbol"` // Symbol to display (*, @, &, etc.)
}
// Oper defines an individual operator
type Oper struct {
Name string `json:"name"`
Password string `json:"password"`
Host string `json:"host"`
Class string `json:"class"`
Flags []string `json:"flags"` // Additional per-user flags
MaxClients int `json:"max_clients"` // Max clients this oper can handle
Expires string `json:"expires"` // Expiration date (optional)
Contact string `json:"contact"` // Contact information
LastSeen string `json:"last_seen"` // Last time this oper was online
}
// RankNames defines custom names for rank levels
type RankNames struct {
Rank1 string `json:"rank_1"` // Default: Helper
Rank2 string `json:"rank_2"` // Default: Moderator
Rank3 string `json:"rank_3"` // Default: Operator
Rank4 string `json:"rank_4"` // Default: Administrator
Rank5 string `json:"rank_5"` // Default: Owner
// Support for custom ranks beyond 5
CustomRanks map[string]int `json:"custom_ranks"` // "CustomName": 6
}
// OperConfig holds the complete operator configuration
type OperConfig struct {
Classes []OperClass `json:"classes"`
Opers []Oper `json:"opers"`
RankNames RankNames `json:"rank_names"`
Settings struct {
RequireSSL bool `json:"require_ssl"`
MaxFailedAttempts int `json:"max_failed_attempts"`
LockoutDuration int `json:"lockout_duration_minutes"`
AllowedCommands []string `json:"allowed_commands"`
LogOperActions bool `json:"log_oper_actions"`
NotifyOnOperUp bool `json:"notify_on_oper_up"`
AutoExpireInactive int `json:"auto_expire_inactive_days"`
RequireTwoFactor bool `json:"require_two_factor"`
} `json:"settings"`
}
// LoadOperConfig loads operator configuration from file
func LoadOperConfig(filename string) (*OperConfig, error) {
data, err := os.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("failed to read oper config file: %v", err)
}
var config OperConfig
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse oper config file: %v", err)
}
return &config, nil
}
// SaveOperConfig saves operator configuration to file
func SaveOperConfig(config *OperConfig, filename string) error {
data, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal oper config: %v", err)
}
if err := os.WriteFile(filename, data, 0644); err != nil {
return fmt.Errorf("failed to write oper config file: %v", err)
}
return nil
}
// GetOperClass returns the operator class by name
func (oc *OperConfig) GetOperClass(className string) *OperClass {
for i := range oc.Classes {
if oc.Classes[i].Name == className {
return &oc.Classes[i]
}
}
return nil
}
// GetOper returns the operator by name
func (oc *OperConfig) GetOper(operName string) *Oper {
for i := range oc.Opers {
if oc.Opers[i].Name == operName {
return &oc.Opers[i]
}
}
return nil
}
// GetOperPermissions returns all permissions for an operator (including inherited)
func (oc *OperConfig) GetOperPermissions(operName string) []string {
oper := oc.GetOper(operName)
if oper == nil {
return nil
}
class := oc.GetOperClass(oper.Class)
if class == nil {
return oper.Flags
}
permissions := make(map[string]bool)
// Add class permissions
for _, perm := range class.Permissions {
permissions[perm] = true
}
// Add inherited permissions
if class.Inherits != "" {
inherited := oc.GetOperClass(class.Inherits)
if inherited != nil {
for _, perm := range inherited.Permissions {
permissions[perm] = true
}
}
}
// Add individual flags
for _, flag := range oper.Flags {
permissions[flag] = true
}
// Convert back to slice
result := make([]string, 0, len(permissions))
for perm := range permissions {
result = append(result, perm)
}
return result
}
// GetRankName returns the custom name for a rank level
func (oc *OperConfig) GetRankName(rank int) string {
switch rank {
case 1:
if oc.RankNames.Rank1 != "" {
return oc.RankNames.Rank1
}
return "Helper"
case 2:
if oc.RankNames.Rank2 != "" {
return oc.RankNames.Rank2
}
return "Moderator"
case 3:
if oc.RankNames.Rank3 != "" {
return oc.RankNames.Rank3
}
return "Operator"
case 4:
if oc.RankNames.Rank4 != "" {
return oc.RankNames.Rank4
}
return "Administrator"
case 5:
if oc.RankNames.Rank5 != "" {
return oc.RankNames.Rank5
}
return "Owner"
default:
// Check custom ranks
for name, rankNum := range oc.RankNames.CustomRanks {
if rankNum == rank {
return name
}
}
return fmt.Sprintf("Rank %d", rank)
}
}
// GetOperRankName returns the rank name for an operator
func (oc *OperConfig) GetOperRankName(operName string) string {
oper := oc.GetOper(operName)
if oper == nil {
return "Unknown"
}
class := oc.GetOperClass(oper.Class)
if class == nil {
return "Unknown"
}
return oc.GetRankName(class.Rank)
}
// SetCustomRankName allows adding custom rank names at runtime
func (oc *OperConfig) SetCustomRankName(name string, rank int) {
if oc.RankNames.CustomRanks == nil {
oc.RankNames.CustomRanks = make(map[string]int)
}
oc.RankNames.CustomRanks[name] = rank
}
// HasPermission checks if an operator has a specific permission
func (oc *OperConfig) HasPermission(operName, permission string) bool {
permissions := oc.GetOperPermissions(operName)
for _, perm := range permissions {
if perm == permission || perm == "*" { // * grants all permissions
return true
}
}
return false
}
// GetOperRank returns the rank of an operator
func (oc *OperConfig) GetOperRank(operName string) int {
oper := oc.GetOper(operName)
if oper == nil {
return 0
}
class := oc.GetOperClass(oper.Class)
if class == nil {
return 0
}
return class.Rank
}
// CanOperateOn checks if oper1 can perform actions on oper2 (based on rank)
func (oc *OperConfig) CanOperateOn(oper1Name, oper2Name string) bool {
rank1 := oc.GetOperRank(oper1Name)
rank2 := oc.GetOperRank(oper2Name)
// Higher rank can operate on lower rank
// Same rank cannot operate on each other (unless they have override permission)
return rank1 > rank2 || oc.HasPermission(oper1Name, "override_rank")
}
// DefaultOperConfig returns a default operator configuration
func DefaultOperConfig() *OperConfig {
return &OperConfig{
Classes: []OperClass{
{
Name: "helper",
Rank: 1,
Description: "Helper - Basic moderation commands",
Permissions: []string{"kick", "topic", "mode_channel"},
Color: "green",
Symbol: "%",
},
{
Name: "moderator",
Rank: 2,
Description: "Moderator - Channel and user management",
Permissions: []string{"ban", "unban", "kick", "mute", "topic", "mode_channel", "mode_user", "who_override"},
Inherits: "helper",
Color: "blue",
Symbol: "@",
},
{
Name: "operator",
Rank: 3,
Description: "Operator - Server management commands",
Permissions: []string{"kill", "gline", "rehash", "connect", "squit", "wallops", "operwall"},
Inherits: "moderator",
Color: "red",
Symbol: "*",
},
{
Name: "admin",
Rank: 4,
Description: "Administrator - Full server control",
Permissions: []string{"*"}, // All permissions
Color: "purple",
Symbol: "&",
},
{
Name: "owner",
Rank: 5,
Description: "Server Owner - Ultimate authority",
Permissions: []string{"*", "override_rank", "shutdown", "restart"},
Color: "gold",
Symbol: "~",
},
},
Opers: []Oper{
{
Name: "admin",
Password: "changeme",
Host: "*@localhost",
Class: "admin",
MaxClients: 1000,
Contact: "admin@example.com",
},
},
RankNames: RankNames{
Rank1: "Helper", // Customizable: "Support Staff", "Junior Mod", etc.
Rank2: "Moderator", // Customizable: "Channel Mod", "Guard", etc.
Rank3: "Operator", // Customizable: "IRC Operator", "Senior Staff", etc.
Rank4: "Administrator", // Customizable: "Admin", "Server Admin", etc.
Rank5: "Owner", // Customizable: "Root", "Founder", etc.
CustomRanks: map[string]int{
// Example: "Super Admin": 6,
// Example: "Network Founder": 10,
},
},
Settings: struct {
RequireSSL bool `json:"require_ssl"`
MaxFailedAttempts int `json:"max_failed_attempts"`
LockoutDuration int `json:"lockout_duration_minutes"`
AllowedCommands []string `json:"allowed_commands"`
LogOperActions bool `json:"log_oper_actions"`
NotifyOnOperUp bool `json:"notify_on_oper_up"`
AutoExpireInactive int `json:"auto_expire_inactive_days"`
RequireTwoFactor bool `json:"require_two_factor"`
}{
RequireSSL: false,
MaxFailedAttempts: 3,
LockoutDuration: 30,
AllowedCommands: []string{"OPER", "KILL", "GLINE", "REHASH", "WALLOPS"},
LogOperActions: true,
NotifyOnOperUp: true,
AutoExpireInactive: 365,
RequireTwoFactor: false,
},
}
}

192
rate_limiter.go Normal file
View File

@@ -0,0 +1,192 @@
package main
import (
"sync"
"time"
)
// AdvancedRateLimiter implements token bucket rate limiting for client connections
type AdvancedRateLimiter struct {
maxTokens int
refillRate time.Duration
tokens int
lastRefill time.Time
mu sync.Mutex
}
// ClientRateLimiter manages per-client rate limiting
type ClientRateLimiter struct {
limiters map[string]*AdvancedRateLimiter
mu sync.RWMutex
cleanup chan string
}
// NewAdvancedRateLimiter creates a new rate limiter
func NewAdvancedRateLimiter(maxTokens int, refillRate time.Duration) *AdvancedRateLimiter {
return &AdvancedRateLimiter{
maxTokens: maxTokens,
refillRate: refillRate,
tokens: maxTokens,
lastRefill: time.Now(),
}
}
// Allow checks if an action is allowed (consumes a token)
func (rl *AdvancedRateLimiter) Allow() bool {
rl.mu.Lock()
defer rl.mu.Unlock()
rl.refill()
if rl.tokens > 0 {
rl.tokens--
return true
}
return false
}
// refill adds tokens based on elapsed time
func (rl *AdvancedRateLimiter) refill() {
now := time.Now()
elapsed := now.Sub(rl.lastRefill)
tokensToAdd := int(elapsed / rl.refillRate)
if tokensToAdd > 0 {
rl.tokens += tokensToAdd
if rl.tokens > rl.maxTokens {
rl.tokens = rl.maxTokens
}
rl.lastRefill = now
}
}
// GetTokens returns current token count (for monitoring)
func (rl *AdvancedRateLimiter) GetTokens() int {
rl.mu.Lock()
defer rl.mu.Unlock()
rl.refill()
return rl.tokens
}
// NewClientRateLimiter creates a new client rate limiter
func NewClientRateLimiter() *ClientRateLimiter {
crl := &ClientRateLimiter{
limiters: make(map[string]*AdvancedRateLimiter),
cleanup: make(chan string, 1000),
}
// Start cleanup routine
go crl.cleanupRoutine()
return crl
}
// Allow checks if a client action is allowed
func (crl *ClientRateLimiter) Allow(clientID string, maxTokens int, refillRate time.Duration) bool {
crl.mu.RLock()
limiter, exists := crl.limiters[clientID]
crl.mu.RUnlock()
if !exists {
crl.mu.Lock()
// Double-check after acquiring write lock
limiter, exists = crl.limiters[clientID]
if !exists {
limiter = NewAdvancedRateLimiter(maxTokens, refillRate)
crl.limiters[clientID] = limiter
}
crl.mu.Unlock()
}
return limiter.Allow()
}
// RemoveClient removes a client's rate limiter
func (crl *ClientRateLimiter) RemoveClient(clientID string) {
select {
case crl.cleanup <- clientID:
// Queued for cleanup
default:
// Cleanup queue full, clean directly
crl.mu.Lock()
delete(crl.limiters, clientID)
crl.mu.Unlock()
}
}
// cleanupRoutine processes client cleanup requests
func (crl *ClientRateLimiter) cleanupRoutine() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for {
select {
case clientID := <-crl.cleanup:
crl.mu.Lock()
delete(crl.limiters, clientID)
crl.mu.Unlock()
case <-ticker.C:
// Periodic cleanup of old rate limiters
crl.mu.Lock()
for clientID, limiter := range crl.limiters {
// Remove limiters that haven't been used recently
if time.Since(limiter.lastRefill) > 10*time.Minute {
delete(crl.limiters, clientID)
}
}
crl.mu.Unlock()
}
}
}
// GetStats returns rate limiter statistics
func (crl *ClientRateLimiter) GetStats() map[string]interface{} {
crl.mu.RLock()
defer crl.mu.RUnlock()
stats := make(map[string]interface{})
stats["active_limiters"] = len(crl.limiters)
totalTokens := 0
for _, limiter := range crl.limiters {
totalTokens += limiter.GetTokens()
}
stats["total_tokens"] = totalTokens
return stats
}
// GlobalRateLimiter implements server-wide rate limiting
type GlobalRateLimiter struct {
connectionLimiter *AdvancedRateLimiter
messageLimiter *AdvancedRateLimiter
}
// NewGlobalRateLimiter creates a new global rate limiter
func NewGlobalRateLimiter(connPerSecond, msgPerSecond int) *GlobalRateLimiter {
return &GlobalRateLimiter{
connectionLimiter: NewAdvancedRateLimiter(connPerSecond*10, time.Second/time.Duration(connPerSecond)),
messageLimiter: NewAdvancedRateLimiter(msgPerSecond*10, time.Second/time.Duration(msgPerSecond)),
}
}
// AllowConnection checks if a new connection is allowed
func (grl *GlobalRateLimiter) AllowConnection() bool {
return grl.connectionLimiter.Allow()
}
// AllowMessage checks if a message is allowed
func (grl *GlobalRateLimiter) AllowMessage() bool {
return grl.messageLimiter.Allow()
}
// GetConnectionTokens returns available connection tokens
func (grl *GlobalRateLimiter) GetConnectionTokens() int {
return grl.connectionLimiter.GetTokens()
}
// GetMessageTokens returns available message tokens
func (grl *GlobalRateLimiter) GetMessageTokens() int {
return grl.messageLimiter.GetTokens()
}

60
scripts/demo_user_modes.sh Executable file
View File

@@ -0,0 +1,60 @@
#!/bin/bash
# God Mode and Stealth Mode User Mode Demo
# This script demonstrates the new user mode implementation
echo "=== God Mode and Stealth Mode User Mode Demo ==="
echo ""
echo "God Mode and Stealth Mode are now implemented as proper IRC user modes:"
echo ""
echo "🔱 GOD MODE (+G):"
echo " /mode mynick +G # Enable God Mode - Ultimate channel override powers"
echo " /mode mynick -G # Disable God Mode"
echo ""
echo " Capabilities:"
echo " • Join banned/invite-only/password/full channels"
echo " • Cannot be kicked by anyone"
echo " • Override all channel restrictions"
echo ""
echo "👻 STEALTH MODE (+S):"
echo " /mode mynick +S # Enable Stealth Mode - Invisible to regular users"
echo " /mode mynick -S # Disable Stealth Mode"
echo ""
echo " Effects:"
echo " • Hidden from /who and /names for regular users"
echo " • Still visible to other operators"
echo " • Invisible channel presence"
echo ""
echo "⚡ COMBINED USAGE:"
echo " /mode mynick +GS # Enable both modes simultaneously"
echo " /mode mynick -GS # Disable both modes"
echo ""
echo "📋 REQUIREMENTS:"
echo " • Must be an IRC operator (/oper)"
echo " • Operator class must have 'god_mode' and/or 'stealth_mode' permissions"
echo " • Can only set modes on yourself"
echo ""
echo "🛡️ PERMISSIONS:"
echo " Add to your operator class in configs/opers.conf:"
echo ' "permissions": ["*", "god_mode", "stealth_mode"]'
echo ""
echo "🔍 CHECKING CURRENT MODES:"
echo " /mode mynick # Show your current user modes"
echo ""
echo "Example operator class with both permissions:"
echo '{'
echo ' "name": "admin",'
echo ' "rank": 4,'
echo ' "permissions": ["*", "god_mode", "stealth_mode"]'
echo '}'
echo ""
echo "This follows proper IRC conventions - user modes instead of custom commands!"
echo "Use /mode +G and /mode +S just like any other IRC user mode."

59
scripts/test_irc_connection.sh Executable file
View File

@@ -0,0 +1,59 @@
#!/bin/bash
# Simple IRC client test to debug connection issues
# This script connects to the IRC server and sends basic registration commands
echo "=== TechIRCd Connection Test ==="
echo "Testing IRC registration sequence..."
echo ""
# Create a temporary file for the test
TEST_FILE="/tmp/irc_test.txt"
# Connect and send basic registration commands
{
echo "NICK testuser"
echo "USER testuser 0 * :Test User"
sleep 2
echo "QUIT :Test complete"
} | nc -q 3 127.0.0.1 6667 | tee "$TEST_FILE"
echo ""
echo "=== Raw IRC Server Response ==="
cat "$TEST_FILE"
echo ""
echo "=== Analysis ==="
if grep -q "001" "$TEST_FILE"; then
echo "✅ RPL_WELCOME (001) found - Server sent welcome message"
else
echo "❌ RPL_WELCOME (001) NOT found - Registration may have failed"
fi
if grep -q "002" "$TEST_FILE"; then
echo "✅ RPL_YOURHOST (002) found"
else
echo "❌ RPL_YOURHOST (002) NOT found"
fi
if grep -q "003" "$TEST_FILE"; then
echo "✅ RPL_CREATED (003) found"
else
echo "❌ RPL_CREATED (003) NOT found"
fi
if grep -q "004" "$TEST_FILE"; then
echo "✅ RPL_MYINFO (004) found"
else
echo "❌ RPL_MYINFO (004) NOT found"
fi
echo ""
echo "If the pyechat client connects but shows nothing, it might be:"
echo "1. Not sending NICK/USER commands automatically"
echo "2. Expecting different IRC numeric responses"
echo "3. Not parsing the IRC protocol properly"
echo "4. Waiting for specific server capabilities"
# Cleanup
rm -f "$TEST_FILE"

106
security_enhancements.go Normal file
View File

@@ -0,0 +1,106 @@
package main
import (
"crypto/tls"
"net"
"time"
)
// Advanced security and authentication
type SecurityConfig struct {
RateLimit struct {
Enable bool `json:"enable"`
MaxRequests int `json:"max_requests"`
Window int `json:"window_seconds"`
BanDuration int `json:"ban_duration"`
} `json:"rate_limit"`
GeoBlocking struct {
Enable bool `json:"enable"`
Whitelist []string `json:"whitelist_countries"`
Blacklist []string `json:"blacklist_countries"`
} `json:"geo_blocking"`
TwoFactor struct {
Enable bool `json:"enable"`
Methods []string `json:"methods"` // totp, sms, email
Required bool `json:"required_for_opers"`
} `json:"two_factor"`
SASL struct {
Enable bool `json:"enable"`
Mechanisms []string `json:"mechanisms"` // PLAIN, EXTERNAL, SCRAM-SHA-256
Required bool `json:"required"`
} `json:"sasl"`
}
// Rate limiting per IP/user
type RateLimiter struct {
connections map[string]*ConnectionLimit
messages map[string]*MessageLimit
}
type ConnectionLimit struct {
IP net.IP
Count int
LastSeen time.Time
Banned bool
BanUntil time.Time
}
type MessageLimit struct {
Count int
LastReset time.Time
Violations int
}
// Certificate-based authentication
type CertAuth struct {
Enable bool `json:"enable"`
RequiredCAs []string `json:"required_cas"`
UserMapping map[string]string `json:"user_mapping"` // cert fingerprint -> username
AutoOper bool `json:"auto_oper"`
}
// OAuth integration
type OAuthConfig struct {
Providers map[string]OAuthProvider `json:"providers"`
}
type OAuthProvider struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
AuthURL string `json:"auth_url"`
TokenURL string `json:"token_url"`
UserInfoURL string `json:"user_info_url"`
}
// DDoS protection
type DDoSProtection struct {
Enable bool `json:"enable"`
MaxConnections int `json:"max_connections_per_ip"`
ConnectionRate int `json:"max_connections_per_minute"`
SynFloodProtection bool `json:"syn_flood_protection"`
}
// Implement security features
func (s *Server) CheckRateLimit(ip net.IP) bool {
// Check if IP is rate limited
return true
}
func (s *Server) ValidateCertificate(cert *tls.Certificate) bool {
// Validate client certificate
return true
}
func (s *Server) AuthenticateOAuth(provider, token string) (*UserInfo, error) {
// OAuth authentication
return nil, nil
}
type UserInfo struct {
Username string
Email string
Verified bool
}

1016
server.go Normal file

File diff suppressed because it is too large Load Diff

509
services.go Normal file
View File

@@ -0,0 +1,509 @@
package main
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"log"
"strings"
"time"
)
// ServicesManager handles all network services
type ServicesManager struct {
server *Server
config *ServicesConfig
nickServ *NickServ
chanServ *ChanServ
operServ *OperServ
memoServ *MemoServ
enabled bool
}
// NickServ handles nickname registration and authentication
type NickServ struct {
manager *ServicesManager
nick string
accounts map[string]*NickAccount
}
// ChanServ handles channel registration and management
type ChanServ struct {
manager *ServicesManager
nick string
channels map[string]*RegisteredChannel
}
// OperServ handles network operator services
type OperServ struct {
manager *ServicesManager
nick string
}
// MemoServ handles user-to-user messages
type MemoServ struct {
manager *ServicesManager
nick string
memos map[string][]*Memo
}
// NickAccount represents a registered nickname
type NickAccount struct {
Nick string `json:"nick"`
PasswordHash string `json:"password_hash"`
Email string `json:"email"`
RegisterTime time.Time `json:"register_time"`
LastSeen time.Time `json:"last_seen"`
Settings map[string]string `json:"settings"`
AccessList []string `json:"access_list"`
Flags []string `json:"flags"`
}
// RegisteredChannel represents a registered channel
type RegisteredChannelServices struct {
Name string `json:"name"`
Founder string `json:"founder"`
RegisterTime time.Time `json:"register_time"`
Topic string `json:"topic"`
Modes string `json:"modes"`
AccessList map[string]*ChannelAccess `json:"access_list"`
Settings map[string]string `json:"settings"`
AutoModes map[string]string `json:"auto_modes"`
}
// ChannelAccess represents access levels for channels - using the one from database.go
// type ChannelAccess struct {
// Nick string `json:"nick"`
// Level int `json:"level"` // 0=banned, 1=voice, 2=halfop, 3=op, 4=sop, 5=founder
// SetBy string `json:"set_by"`
// SetTime time.Time `json:"set_time"`
// LastUsed time.Time `json:"last_used"`
// }
// Memo represents a user memo
type Memo struct {
From string `json:"from"`
To string `json:"to"`
Message string `json:"message"`
Time time.Time `json:"time"`
Read bool `json:"read"`
}
// ServicesConfig configuration for services
type ServicesConfig struct {
Enable bool `json:"enable"`
NickServ NickServConfig `json:"nickserv"`
ChanServ ChanServConfig `json:"chanserv"`
OperServ OperServConfig `json:"operserv"`
MemoServ MemoServConfig `json:"memoserv"`
Database DatabaseConfig `json:"database"`
}
type NickServConfig struct {
Enable bool `json:"enable"`
Nick string `json:"nick"`
User string `json:"user"`
Host string `json:"host"`
Realname string `json:"realname"`
ExpireTime int `json:"expire_time"` // Days until unused nicks expire
IdentifyTimeout int `json:"identify_timeout"` // Seconds to identify before kill
EmailVerify bool `json:"email_verify"`
RestrictReg bool `json:"restrict_registration"`
}
type ChanServConfig struct {
Enable bool `json:"enable"`
Nick string `json:"nick"`
User string `json:"user"`
Host string `json:"host"`
Realname string `json:"realname"`
ExpireTime int `json:"expire_time"` // Days until unused channels expire
MaxChannels int `json:"max_channels"` // Max channels per user
AutoDeop bool `json:"auto_deop"` // Auto-deop users without access
}
type OperServConfig struct {
Enable bool `json:"enable"`
Nick string `json:"nick"`
User string `json:"user"`
Host string `json:"host"`
Realname string `json:"realname"`
}
type MemoServConfig struct {
Enable bool `json:"enable"`
Nick string `json:"nick"`
User string `json:"user"`
Host string `json:"host"`
Realname string `json:"realname"`
MaxMemos int `json:"max_memos"`
MemoExpire int `json:"memo_expire"` // Days until memos expire
}
// NewServicesManager creates a new services manager
func NewServicesManager(server *Server, config *ServicesConfig) *ServicesManager {
sm := &ServicesManager{
server: server,
config: config,
enabled: config.Enable,
}
if config.NickServ.Enable {
sm.nickServ = &NickServ{
manager: sm,
nick: config.NickServ.Nick,
accounts: make(map[string]*NickAccount),
}
}
if config.ChanServ.Enable {
sm.chanServ = &ChanServ{
manager: sm,
nick: config.ChanServ.Nick,
channels: make(map[string]*RegisteredChannel),
}
}
if config.OperServ.Enable {
sm.operServ = &OperServ{
manager: sm,
nick: config.OperServ.Nick,
}
}
if config.MemoServ.Enable {
sm.memoServ = &MemoServ{
manager: sm,
nick: config.MemoServ.Nick,
memos: make(map[string][]*Memo),
}
}
return sm
}
// Start initializes and starts all enabled services
func (sm *ServicesManager) Start() error {
if !sm.enabled {
return nil
}
log.Println("Starting TechIRCd Services...")
// Create service clients
if sm.nickServ != nil {
sm.createServiceClient(sm.config.NickServ.Nick, sm.config.NickServ.User,
sm.config.NickServ.Host, sm.config.NickServ.Realname)
}
if sm.chanServ != nil {
sm.createServiceClient(sm.config.ChanServ.Nick, sm.config.ChanServ.User,
sm.config.ChanServ.Host, sm.config.ChanServ.Realname)
}
if sm.operServ != nil {
sm.createServiceClient(sm.config.OperServ.Nick, sm.config.OperServ.User,
sm.config.OperServ.Host, sm.config.OperServ.Realname)
}
if sm.memoServ != nil {
sm.createServiceClient(sm.config.MemoServ.Nick, sm.config.MemoServ.User,
sm.config.MemoServ.Host, sm.config.MemoServ.Realname)
}
log.Println("Services started successfully")
return nil
}
// createServiceClient creates a virtual client for a service
func (sm *ServicesManager) createServiceClient(nick, user, host, realname string) {
// Create a virtual service client that appears as a regular user
serviceClient := &Client{
nick: nick,
user: user,
host: host,
realname: realname,
server: sm.server,
modes: make(map[rune]bool),
channels: make(map[string]*Channel),
capabilities: make(map[string]bool),
}
// Set service modes
serviceClient.modes['S'] = true // Service mode
serviceClient.modes['o'] = true // Operator mode
// Add to server
sm.server.clients[strings.ToLower(nick)] = serviceClient
// Send introduction to network
sm.server.BroadcastToServers(fmt.Sprintf(":%s NICK %s", sm.server.config.Server.Name, nick))
}
// HandleMessage processes messages directed to services
func (sm *ServicesManager) HandleMessage(from *Client, target, message string) bool {
if !sm.enabled {
return false
}
lowerTarget := strings.ToLower(target)
parts := strings.Fields(message)
if len(parts) == 0 {
return false
}
command := strings.ToUpper(parts[0])
switch lowerTarget {
case strings.ToLower(sm.config.NickServ.Nick):
return sm.handleNickServCommand(from, command, parts[1:])
case strings.ToLower(sm.config.ChanServ.Nick):
return sm.handleChanServCommand(from)
case strings.ToLower(sm.config.OperServ.Nick):
return sm.handleOperServCommand(from)
case strings.ToLower(sm.config.MemoServ.Nick):
return sm.handleMemoServCommand(from)
}
return false
}
// NickServ command handlers
func (sm *ServicesManager) handleNickServCommand(from *Client, command string, args []string) bool {
switch command {
case "REGISTER":
return sm.handleNickServRegister(from, args)
case "IDENTIFY":
return sm.handleNickServIdentify(from, args)
case "INFO":
return sm.handleNickServInfo(from, args)
case "DROP":
return sm.handleNickServDrop(from, args)
case "SET":
return sm.handleNickServSet(from)
case "ACCESS":
return sm.handleNickServAccess(from)
case "HELP":
return sm.handleNickServHelp(from)
default:
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
fmt.Sprintf("Unknown command %s. Type /msg %s HELP for help.", command, sm.config.NickServ.Nick))
return true
}
}
func (sm *ServicesManager) handleNickServRegister(from *Client, args []string) bool {
if len(args) < 2 {
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
"Syntax: REGISTER <password> <email>")
return true
}
nick := strings.ToLower(from.nick)
password := args[0]
email := args[1]
// Check if nick is already registered
if _, exists := sm.nickServ.accounts[nick]; exists {
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
"This nickname is already registered.")
return true
}
// Hash password
hash := sha256.Sum256([]byte(password))
passwordHash := hex.EncodeToString(hash[:])
// Create account
account := &NickAccount{
Nick: from.nick,
PasswordHash: passwordHash,
Email: email,
RegisterTime: time.Now(),
LastSeen: time.Now(),
Settings: make(map[string]string),
AccessList: []string{},
Flags: []string{},
}
sm.nickServ.accounts[nick] = account
// Set user as identified
from.account = from.nick
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
fmt.Sprintf("Nickname %s has been registered successfully.", from.nick))
return true
}
func (sm *ServicesManager) handleNickServIdentify(from *Client, args []string) bool {
if len(args) < 1 {
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
"Syntax: IDENTIFY <password>")
return true
}
nick := strings.ToLower(from.nick)
password := args[0]
account, exists := sm.nickServ.accounts[nick]
if !exists {
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
"This nickname is not registered.")
return true
}
// Check password
hash := sha256.Sum256([]byte(password))
passwordHash := hex.EncodeToString(hash[:])
if account.PasswordHash != passwordHash {
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
"Invalid password.")
return true
}
// Set user as identified
from.account = from.nick
account.LastSeen = time.Now()
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
fmt.Sprintf("You are now identified for %s.", from.nick))
return true
}
func (sm *ServicesManager) handleNickServInfo(from *Client, args []string) bool {
var targetNick string
if len(args) > 0 {
targetNick = args[0]
} else {
targetNick = from.nick
}
nick := strings.ToLower(targetNick)
account, exists := sm.nickServ.accounts[nick]
if !exists {
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
fmt.Sprintf("The nickname %s is not registered.", targetNick))
return true
}
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
fmt.Sprintf("Information for %s:", account.Nick))
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
fmt.Sprintf("Registered: %s", account.RegisterTime.Format("Jan 02, 2006 15:04:05 MST")))
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
fmt.Sprintf("Last seen: %s", account.LastSeen.Format("Jan 02, 2006 15:04:05 MST")))
return true
}
func (sm *ServicesManager) handleNickServDrop(from *Client, _ []string) bool {
nick := strings.ToLower(from.nick)
if from.account != from.nick {
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
"You must be identified to drop your nickname.")
return true
}
delete(sm.nickServ.accounts, nick)
from.account = ""
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
fmt.Sprintf("Nickname %s has been dropped.", from.nick))
return true
}
func (sm *ServicesManager) handleNickServSet(from *Client) bool {
// Handle SET commands (EMAIL, PASSWORD, etc.)
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
"SET command not yet implemented.")
return true
}
func (sm *ServicesManager) handleNickServAccess(from *Client) bool {
// Handle ACCESS commands (ADD, DEL, LIST)
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick,
"ACCESS command not yet implemented.")
return true
}
func (sm *ServicesManager) handleNickServHelp(from *Client) bool {
help := []string{
"*** NickServ Help ***",
"REGISTER <password> <email> - Register your nickname",
"IDENTIFY <password> - Identify to your nickname",
"INFO [nick] - Show information about a nickname",
"DROP - Drop your nickname registration",
"SET - Change settings",
"ACCESS - Manage access list",
"HELP - Show this help",
}
for _, line := range help {
sm.sendServiceNotice(sm.config.NickServ.Nick, from.nick, line)
}
return true
}
// ChanServ command handlers (placeholder)
func (sm *ServicesManager) handleChanServCommand(from *Client) bool {
sm.sendServiceNotice(sm.config.ChanServ.Nick, from.nick,
"ChanServ commands not yet implemented.")
return true
}
// OperServ command handlers (placeholder)
func (sm *ServicesManager) handleOperServCommand(from *Client) bool {
sm.sendServiceNotice(sm.config.OperServ.Nick, from.nick,
"OperServ commands not yet implemented.")
return true
}
// MemoServ command handlers (placeholder)
func (sm *ServicesManager) handleMemoServCommand(from *Client) bool {
sm.sendServiceNotice(sm.config.MemoServ.Nick, from.nick,
"MemoServ commands not yet implemented.")
return true
}
// sendServiceNotice sends a notice from a service to a user
func (sm *ServicesManager) sendServiceNotice(from, to, message string) {
if client := sm.server.GetClient(to); client != nil {
client.SendMessage(fmt.Sprintf(":%s NOTICE %s :%s", from, to, message))
}
}
// IsServiceNick checks if a nick is a service
func (sm *ServicesManager) IsServiceNick(nick string) bool {
if !sm.enabled {
return false
}
lowerNick := strings.ToLower(nick)
return lowerNick == strings.ToLower(sm.config.NickServ.Nick) ||
lowerNick == strings.ToLower(sm.config.ChanServ.Nick) ||
lowerNick == strings.ToLower(sm.config.OperServ.Nick) ||
lowerNick == strings.ToLower(sm.config.MemoServ.Nick)
}
// GetUserAccount returns the account for a nick
func (sm *ServicesManager) GetUserAccount(nick string) *NickAccount {
if sm.nickServ == nil {
return nil
}
return sm.nickServ.accounts[strings.ToLower(nick)]
}
// IsIdentified checks if a user is identified
func (sm *ServicesManager) IsIdentified(nick string) bool {
if client := sm.server.GetClient(nick); client != nil {
return client.account != ""
}
return false
}

309
stress_config.json Normal file
View File

@@ -0,0 +1,309 @@
{
"server": {
"host": "localhost",
"port": 6667,
"ssl": false,
"ssl_port": 6697,
"nick_prefix": "StressBot",
"auto_join_channels": ["#test", "#general"]
},
"log_level": "INFO",
"scenarios": [
{
"name": "Basic Connection Test",
"description": "Test basic connection and registration",
"client_count": 10,
"duration": 30,
"connect_gradually": false,
"disconnect_gradually": true,
"disconnect_delay": 0.2,
"delay_after": 5,
"activities": []
},
{
"name": "Mass Connection Stress",
"description": "Stress test with many simultaneous connections",
"client_count": 50,
"duration": 60,
"connect_gradually": false,
"disconnect_gradually": false,
"delay_after": 5,
"activities": [
{
"type": "channel_flood",
"channel": "#test",
"message_count": 20,
"delay": 2.0
}
]
},
{
"name": "Gradual Connection Test",
"description": "Test gradual connection buildup",
"client_count": 30,
"duration": 45,
"connect_gradually": true,
"connect_delay": 0.1,
"disconnect_gradually": true,
"disconnect_delay": 0.1,
"delay_after": 3,
"activities": [
{
"type": "private_flood",
"message_count": 15,
"delay": 3.0
}
]
},
{
"name": "Heavy Activity Test",
"description": "Test server under heavy message load",
"client_count": 25,
"duration": 90,
"connect_gradually": false,
"disconnect_gradually": false,
"delay_after": 5,
"activities": [
{
"type": "channel_flood",
"channel": "#test",
"message_count": 30,
"delay": 1.0
},
{
"type": "private_flood",
"message_count": 20,
"delay": 1.5
},
{
"type": "join_part_spam",
"iterations": 10,
"channels": ["#spam1", "#spam2", "#spam3"],
"delay": 2.0
},
{
"type": "random_commands",
"command_count": 15,
"commands": ["WHO #test", "LIST", "VERSION", "WHOIS StressBot0001"],
"delay": 2.5
}
]
},
{
"name": "Nick Change Spam",
"description": "Test rapid nick changes",
"client_count": 15,
"duration": 30,
"connect_gradually": false,
"disconnect_gradually": false,
"delay_after": 3,
"activities": [
{
"type": "nick_change_spam",
"iterations": 20,
"delay": 1.0
}
]
},
{
"name": "Channel Chaos Test",
"description": "Rapid JOIN/PART with message flooding",
"client_count": 20,
"duration": 60,
"connect_gradually": true,
"connect_delay": 0.05,
"disconnect_gradually": true,
"disconnect_delay": 0.05,
"delay_after": 5,
"activities": [
{
"type": "join_part_spam",
"iterations": 25,
"channels": ["#chaos1", "#chaos2", "#chaos3", "#chaos4"],
"delay": 0.5
},
{
"type": "channel_flood",
"channel": "#chaos1",
"message_count": 40,
"delay": 1.0
}
]
},
{
"name": "Maximum Load Test",
"description": "Push server to maximum capacity",
"client_count": 100,
"duration": 120,
"connect_gradually": true,
"connect_delay": 0.02,
"disconnect_gradually": true,
"disconnect_delay": 0.01,
"delay_after": 10,
"activities": [
{
"type": "channel_flood",
"channel": "#maxload",
"message_count": 50,
"delay": 0.8
},
{
"type": "private_flood",
"message_count": 30,
"delay": 1.2
},
{
"type": "join_part_spam",
"iterations": 15,
"channels": ["#load1", "#load2", "#load3", "#load4", "#load5"],
"delay": 1.5
},
{
"type": "random_commands",
"command_count": 25,
"commands": ["WHO #maxload", "LIST", "VERSION", "WHOIS StressBot0050", "NAMES #load1"],
"delay": 2.0
}
]
},
{
"name": "EXTREME: Mass Connection Bomb",
"description": "BRUTAL: 200+ connections at once",
"client_count": 200,
"duration": 120,
"connect_gradually": false,
"disconnect_gradually": false,
"delay_after": 10,
"activities": [
{
"type": "channel_flood",
"channel": "#massacre",
"message_count": 50,
"delay": 0.1
},
{
"type": "private_flood",
"message_count": 30,
"delay": 0.2
}
]
},
{
"name": "EXTREME: Rapid Fire Connections",
"description": "BRUTAL: Connect 150 clients as fast as possible",
"client_count": 150,
"duration": 60,
"connect_gradually": true,
"connect_delay": 0.001,
"disconnect_gradually": true,
"disconnect_delay": 0.001,
"delay_after": 5,
"activities": [
{
"type": "channel_flood",
"channel": "#rapidfire",
"message_count": 100,
"delay": 0.05
}
]
},
{
"name": "EXTREME: Message Hurricane",
"description": "BRUTAL: Maximum message throughput test",
"client_count": 80,
"duration": 180,
"connect_gradually": false,
"disconnect_gradually": false,
"delay_after": 10,
"activities": [
{
"type": "channel_flood",
"channel": "#hurricane",
"message_count": 200,
"delay": 0.01
},
{
"type": "private_flood",
"message_count": 150,
"delay": 0.02
},
{
"type": "join_part_spam",
"iterations": 50,
"channels": ["#chaos1", "#chaos2", "#chaos3", "#chaos4", "#chaos5"],
"delay": 0.1
},
{
"type": "random_commands",
"command_count": 100,
"commands": ["WHO #hurricane", "LIST", "VERSION", "TIME", "ADMIN", "INFO"],
"delay": 0.05
}
]
},
{
"name": "EXTREME: Connection Chaos",
"description": "BRUTAL: Rapid connect/disconnect cycles",
"client_count": 100,
"duration": 90,
"connect_gradually": true,
"connect_delay": 0.01,
"disconnect_gradually": true,
"disconnect_delay": 0.01,
"delay_after": 5,
"activities": [
{
"type": "nick_spam",
"iterations": 20,
"delay": 0.1
},
{
"type": "channel_flood",
"channel": "#chaos",
"message_count": 75,
"delay": 0.1
}
]
},
{
"name": "EXTREME: The Nuclear Option",
"description": "ULTIMATE BRUTAL: Everything at maximum intensity",
"client_count": 300,
"duration": 300,
"connect_gradually": true,
"connect_delay": 0.002,
"disconnect_gradually": false,
"delay_after": 15,
"activities": [
{
"type": "channel_flood",
"channel": "#nuclear",
"message_count": 500,
"delay": 0.005
},
{
"type": "private_flood",
"message_count": 300,
"delay": 0.01
},
{
"type": "join_part_spam",
"iterations": 100,
"channels": ["#nuke1", "#nuke2", "#nuke3", "#nuke4", "#nuke5", "#nuke6", "#nuke7", "#nuke8"],
"delay": 0.02
},
{
"type": "nick_spam",
"iterations": 50,
"delay": 0.03
},
{
"type": "random_commands",
"command_count": 200,
"commands": ["WHO #nuclear", "LIST", "VERSION", "TIME", "ADMIN", "INFO", "LUSERS"],
"delay": 0.01
}
]
}
]
}

453
stress_test.py Normal file
View File

@@ -0,0 +1,453 @@
#!/usr/bin/env python3
"""
TechIRCd Stress Testing Tool
Advanced IRC server stress testing with configurable scenarios
"""
import asyncio
import json
import random
import string
import time
import logging
import argparse
from typing import List, Dict, Any
import socket
import ssl
class IRCClient:
def __init__(self, config: Dict[str, Any], client_id: int):
self.config = config
self.client_id = client_id
self.nick = f"{config['nick_prefix']}{client_id:04d}"
self.user = f"user{client_id}"
self.realname = f"Stress Test Client {client_id}"
self.reader = None
self.writer = None
self.connected = False
self.registered = False
self.channels = []
self.message_count = 0
self.start_time = None
async def connect(self):
"""Connect to IRC server"""
try:
if self.config.get('ssl', False):
context = ssl.create_default_context()
self.reader, self.writer = await asyncio.open_connection(
self.config['host'],
self.config.get('ssl_port', 6697),
ssl=context
)
else:
self.reader, self.writer = await asyncio.open_connection(
self.config['host'],
self.config['port']
)
self.connected = True
self.start_time = time.time()
# Start registration
await self.send(f"NICK {self.nick}")
await self.send(f"USER {self.user} 0 * :{self.realname}")
# Start message handler
asyncio.create_task(self.message_handler())
return True
except Exception as e:
logging.error(f"Client {self.client_id} connection failed: {e}")
return False
async def send(self, message: str):
"""Send message to server"""
if self.writer and not self.writer.is_closing():
try:
self.writer.write(f"{message}\r\n".encode())
await self.writer.drain()
logging.debug(f"Client {self.client_id} sent: {message}")
except Exception as e:
logging.error(f"Client {self.client_id} send error: {e}")
async def message_handler(self):
"""Handle incoming messages"""
try:
while self.connected and self.reader:
line = await self.reader.readline()
if not line:
break
message = line.decode().strip()
if not message:
continue
logging.debug(f"Client {self.client_id} received: {message}")
await self.handle_message(message)
except Exception as e:
logging.error(f"Client {self.client_id} message handler error: {e}")
finally:
self.connected = False
async def handle_message(self, message: str):
"""Process incoming IRC messages"""
parts = message.split()
if len(parts) < 2:
return
# Handle PING
if parts[0] == "PING":
await self.send(f"PONG {parts[1]}")
return
# Handle numeric responses
if len(parts) >= 2 and parts[1].isdigit():
numeric = int(parts[1])
# Welcome message - registration complete
if numeric == 1: # RPL_WELCOME
self.registered = True
logging.info(f"Client {self.client_id} ({self.nick}) registered successfully")
# Auto-join channels if configured
for channel in self.config.get('auto_join_channels', []):
await self.join_channel(channel)
async def join_channel(self, channel: str):
"""Join a channel"""
await self.send(f"JOIN {channel}")
if channel not in self.channels:
self.channels.append(channel)
async def send_privmsg(self, target: str, message: str):
"""Send private message or channel message"""
await self.send(f"PRIVMSG {target} :{message}")
self.message_count += 1
async def disconnect(self):
"""Disconnect from server"""
self.connected = False
if self.writer and not self.writer.is_closing():
await self.send("QUIT :Stress test complete")
self.writer.close()
await self.writer.wait_closed()
class StressTest:
def __init__(self, config_file: str):
with open(config_file, 'r') as f:
self.config = json.load(f)
self.clients: List[IRCClient] = []
self.start_time = None
self.stats = {
'connected': 0,
'registered': 0,
'messages_sent': 0,
'errors': 0
}
# Setup logging
log_level = getattr(logging, self.config.get('log_level', 'INFO').upper())
logging.basicConfig(
level=log_level,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('stress_test.log'),
logging.StreamHandler()
]
)
async def run_scenario(self, scenario: Dict[str, Any]):
"""Run a specific test scenario"""
scenario_name = scenario['name']
client_count = scenario['client_count']
duration = scenario.get('duration', 60)
logging.info(f"Starting scenario: {scenario_name}")
logging.info(f"Clients: {client_count}, Duration: {duration}s")
# Create and connect clients
if scenario.get('connect_gradually', False):
await self.gradual_connect(client_count, scenario.get('connect_delay', 0.1))
else:
await self.mass_connect(client_count)
# Run scenario activities
await self.run_activities(scenario, duration)
# Collect stats
await self.collect_stats()
# Disconnect clients
if scenario.get('disconnect_gradually', False):
await self.gradual_disconnect(scenario.get('disconnect_delay', 0.1))
else:
await self.mass_disconnect()
logging.info(f"Scenario {scenario_name} completed")
async def mass_connect(self, count: int):
"""Connect many clients simultaneously"""
logging.info(f"Mass connecting {count} clients...")
tasks = []
for i in range(count):
client = IRCClient(self.config['server'], i + 1)
self.clients.append(client)
tasks.append(client.connect())
results = await asyncio.gather(*tasks, return_exceptions=True)
connected = sum(1 for r in results if r is True)
self.stats['connected'] = connected
logging.info(f"Connected {connected}/{count} clients")
# Wait for registration
await asyncio.sleep(2)
registered = sum(1 for c in self.clients if c.registered)
self.stats['registered'] = registered
logging.info(f"Registered {registered}/{connected} clients")
async def gradual_connect(self, count: int, delay: float):
"""Connect clients gradually with delay"""
logging.info(f"Gradually connecting {count} clients with {delay}s delay...")
for i in range(count):
client = IRCClient(self.config['server'], i + 1)
self.clients.append(client)
success = await client.connect()
if success:
self.stats['connected'] += 1
if delay > 0:
await asyncio.sleep(delay)
# Wait for registration
await asyncio.sleep(2)
registered = sum(1 for c in self.clients if c.registered)
self.stats['registered'] = registered
logging.info(f"Registered {registered}/{self.stats['connected']} clients")
async def run_activities(self, scenario: Dict[str, Any], duration: int):
"""Run scenario activities for specified duration"""
activities = scenario.get('activities', [])
if not activities:
logging.info(f"No activities specified, waiting {duration} seconds...")
await asyncio.sleep(duration)
return
end_time = time.time() + duration
while time.time() < end_time:
for activity in activities:
if time.time() >= end_time:
break
await self.run_activity(activity)
# Delay between activities
delay = activity.get('delay', 1.0)
await asyncio.sleep(delay)
async def run_activity(self, activity: Dict[str, Any]):
"""Run a single activity"""
activity_type = activity['type']
if activity_type == 'channel_flood':
await self.channel_flood_activity(activity)
elif activity_type == 'private_flood':
await self.private_flood_activity(activity)
elif activity_type == 'join_part_spam':
await self.join_part_spam_activity(activity)
elif activity_type == 'nick_change_spam':
await self.nick_change_spam_activity(activity)
elif activity_type == 'random_commands':
await self.random_commands_activity(activity)
else:
logging.warning(f"Unknown activity type: {activity_type}")
async def channel_flood_activity(self, activity: Dict[str, Any]):
"""Flood channels with messages"""
message_count = activity.get('message_count', 10)
channel = activity.get('channel', '#test')
registered_clients = [c for c in self.clients if c.registered]
if not registered_clients:
return
tasks = []
for _ in range(message_count):
client = random.choice(registered_clients)
message = self.generate_random_message()
tasks.append(client.send_privmsg(channel, message))
await asyncio.gather(*tasks, return_exceptions=True)
self.stats['messages_sent'] += len(tasks)
async def private_flood_activity(self, activity: Dict[str, Any]):
"""Flood with private messages"""
message_count = activity.get('message_count', 10)
registered_clients = [c for c in self.clients if c.registered]
if len(registered_clients) < 2:
return
tasks = []
for _ in range(message_count):
sender = random.choice(registered_clients)
target = random.choice(registered_clients)
if sender != target:
message = self.generate_random_message()
tasks.append(sender.send_privmsg(target.nick, message))
await asyncio.gather(*tasks, return_exceptions=True)
self.stats['messages_sent'] += len(tasks)
async def join_part_spam_activity(self, activity: Dict[str, Any]):
"""Spam JOIN/PART commands"""
iterations = activity.get('iterations', 5)
channels = activity.get('channels', ['#spam1', '#spam2', '#spam3'])
registered_clients = [c for c in self.clients if c.registered]
if not registered_clients:
return
for _ in range(iterations):
client = random.choice(registered_clients)
channel = random.choice(channels)
await client.join_channel(channel)
await asyncio.sleep(0.1)
await client.send(f"PART {channel} :Spam test")
async def nick_change_spam_activity(self, activity: Dict[str, Any]):
"""Spam NICK changes"""
iterations = activity.get('iterations', 5)
registered_clients = [c for c in self.clients if c.registered]
if not registered_clients:
return
for _ in range(iterations):
client = random.choice(registered_clients)
new_nick = f"{client.nick}_{''.join(random.choices(string.ascii_lowercase, k=3))}"
await client.send(f"NICK {new_nick}")
await asyncio.sleep(0.1)
async def random_commands_activity(self, activity: Dict[str, Any]):
"""Send random IRC commands"""
command_count = activity.get('command_count', 10)
commands = activity.get('commands', ['WHO #test', 'WHOIS randomnick', 'LIST', 'VERSION'])
registered_clients = [c for c in self.clients if c.registered]
if not registered_clients:
return
for _ in range(command_count):
client = random.choice(registered_clients)
command = random.choice(commands)
await client.send(command)
await asyncio.sleep(0.1)
def generate_random_message(self) -> str:
"""Generate random message content"""
words = ['hello', 'world', 'test', 'stress', 'irc', 'server', 'message', 'random', 'data']
return ' '.join(random.choices(words, k=random.randint(1, 8)))
async def collect_stats(self):
"""Collect and display statistics"""
connected = sum(1 for c in self.clients if c.connected)
registered = sum(1 for c in self.clients if c.registered)
total_messages = sum(c.message_count for c in self.clients)
self.stats.update({
'connected': connected,
'registered': registered,
'messages_sent': total_messages
})
logging.info(f"Stats: {connected} connected, {registered} registered, {total_messages} messages sent")
async def mass_disconnect(self):
"""Disconnect all clients simultaneously"""
logging.info("Disconnecting all clients...")
tasks = [client.disconnect() for client in self.clients]
await asyncio.gather(*tasks, return_exceptions=True)
self.clients.clear()
async def gradual_disconnect(self, delay: float):
"""Disconnect clients gradually"""
logging.info(f"Gradually disconnecting clients with {delay}s delay...")
for client in self.clients:
await client.disconnect()
if delay > 0:
await asyncio.sleep(delay)
self.clients.clear()
async def run_all_scenarios(self):
"""Run all configured scenarios"""
self.start_time = time.time()
for scenario in self.config['scenarios']:
try:
await self.run_scenario(scenario)
# Delay between scenarios
scenario_delay = scenario.get('delay_after', 2)
if scenario_delay > 0:
logging.info(f"Waiting {scenario_delay}s before next scenario...")
await asyncio.sleep(scenario_delay)
except Exception as e:
logging.error(f"Scenario {scenario['name']} failed: {e}")
self.stats['errors'] += 1
total_time = time.time() - self.start_time
logging.info(f"All scenarios completed in {total_time:.2f} seconds")
logging.info(f"Final stats: {self.stats}")
def main():
parser = argparse.ArgumentParser(description='TechIRCd Stress Testing Tool')
parser.add_argument('--config', '-c', default='stress_config.json',
help='Configuration file (default: stress_config.json)')
parser.add_argument('--scenario', '-s', help='Run specific scenario only')
args = parser.parse_args()
try:
stress_test = StressTest(args.config)
if args.scenario:
# Run specific scenario
scenario = next((s for s in stress_test.config['scenarios'] if s['name'] == args.scenario), None)
if scenario:
asyncio.run(stress_test.run_scenario(scenario))
else:
print(f"Scenario '{args.scenario}' not found")
return 1
else:
# Run all scenarios
asyncio.run(stress_test.run_all_scenarios())
return 0
except FileNotFoundError:
print(f"Configuration file '{args.config}' not found")
return 1
except Exception as e:
print(f"Error: {e}")
return 1
if __name__ == "__main__":
exit(main())

154
test_ban_enforcement.py Normal file
View File

@@ -0,0 +1,154 @@
#!/usr/bin/env python3
import socket
import time
import threading
def connect_client(nickname):
"""Connect a client and return the socket"""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('localhost', 6667))
# Send initial IRC commands
sock.send(f"NICK {nickname}\r\n".encode())
sock.send(f"USER {nickname} 0 * :Test User\r\n".encode())
# Wait for welcome messages
time.sleep(0.5)
return sock
except Exception as e:
print(f"Error connecting {nickname}: {e}")
return None
def read_responses(sock, name, duration=2):
"""Read responses from server for a duration"""
try:
sock.settimeout(0.1)
responses = []
start_time = time.time()
while time.time() - start_time < duration:
try:
data = sock.recv(4096).decode()
if data:
responses.append(data.strip())
print(f"[{name}] {data.strip()}")
except socket.timeout:
continue
except:
break
return responses
except Exception as e:
print(f"Error reading from {name}: {e}")
return []
def test_gline_enforcement():
print("\n=== TESTING GLINE ENFORCEMENT ===")
# Connect admin client
admin = connect_client("AdminUser")
if not admin:
print("Failed to connect admin")
return
read_responses(admin, "Admin", 1)
# Give admin operator privileges (OPER)
admin.send("OPER admin adminpass\r\n".encode())
read_responses(admin, "Admin", 1)
# Connect a test user first
print("\n1. Connecting test user before GLINE...")
test_user = connect_client("TestUser")
if test_user:
read_responses(test_user, "TestUser", 1)
print(" ✓ TestUser connected successfully")
# Issue GLINE command
print("\n2. Issuing GLINE for TestUser...")
admin.send("GLINE TestUser 1h Testing GLINE enforcement\r\n".encode())
read_responses(admin, "Admin", 1)
# Try to send a message as the existing user
print("\n3. TestUser trying to send message after GLINE...")
if test_user:
test_user.send("PRIVMSG #test :This should work, GLINE only affects new connections\r\n".encode())
read_responses(test_user, "TestUser", 1)
# Try to connect a new user with same nick pattern
print("\n4. Trying to connect new TestUser after GLINE...")
new_test_user = connect_client("TestUser2")
if new_test_user:
read_responses(new_test_user, "TestUser2", 2)
new_test_user.close()
# Cleanup
if test_user:
test_user.close()
admin.close()
def test_shun_enforcement():
print("\n\n=== TESTING SHUN ENFORCEMENT ===")
# Connect admin client
admin = connect_client("AdminUser")
if not admin:
print("Failed to connect admin")
return
read_responses(admin, "Admin", 1)
# Give admin operator privileges
admin.send("OPER admin adminpass\r\n".encode())
read_responses(admin, "Admin", 1)
# Connect test user
print("\n1. Connecting test user...")
test_user = connect_client("ShunUser")
if not test_user:
print("Failed to connect test user")
admin.close()
return
read_responses(test_user, "ShunUser", 1)
# Test user sends a message before SHUN
print("\n2. ShunUser sending message before SHUN...")
test_user.send("PRIVMSG #test :This message should work\r\n".encode())
read_responses(test_user, "ShunUser", 1)
# Issue SHUN command
print("\n3. Issuing SHUN for ShunUser...")
admin.send("SHUN ShunUser 1h Testing SHUN enforcement\r\n".encode())
read_responses(admin, "Admin", 1)
# Test user tries to send message after SHUN
print("\n4. ShunUser trying to send message after SHUN (should be ignored)...")
test_user.send("PRIVMSG #test :This message should be silently ignored\r\n".encode())
read_responses(test_user, "ShunUser", 2)
# Test user tries other commands
print("\n5. ShunUser trying other commands after SHUN...")
test_user.send("JOIN #testchan\r\n".encode())
read_responses(test_user, "ShunUser", 1)
test_user.send("NOTICE #test :This notice should also be ignored\r\n".encode())
read_responses(test_user, "ShunUser", 1)
# Cleanup
test_user.close()
admin.close()
if __name__ == "__main__":
print("Testing TechIRCd Ban Enforcement")
print("Make sure TechIRCd server is running on localhost:6667")
time.sleep(1)
test_gline_enforcement()
test_shun_enforcement()
print("\n=== TEST COMPLETE ===")

23
test_cap_list.sh Normal file
View File

@@ -0,0 +1,23 @@
#!/bin/bash
# Test script to verify CAP LIST functionality
echo "Testing CAP LIST functionality..."
# Connect to server and test CAP negotiation + LIST
{
echo "CAP LS"
sleep 0.1
echo "CAP REQ :away-notify chghost multi-prefix"
sleep 0.1
echo "CAP LIST"
sleep 0.1
echo "CAP END"
sleep 0.1
echo "NICK TestUser"
sleep 0.1
echo "USER test 0 * :Test User"
sleep 0.1
echo "CAP LIST"
sleep 0.1
echo "QUIT :Testing complete"
} | telnet localhost 6667

56
test_shutdown.sh Normal file
View File

@@ -0,0 +1,56 @@
#!/bin/bash
# TechIRCd Shutdown Test Script
echo "=== TechIRCd Shutdown Test ==="
echo
# Start the server in background
echo "Starting TechIRCd..."
./techircd start &
SERVER_PID=$!
echo "Server started with PID: $SERVER_PID"
echo "Waiting 5 seconds for server to initialize..."
sleep 5
# Test connection
echo "Testing initial connection..."
timeout 5s bash -c '
exec 3<>/dev/tcp/localhost/6667
echo "NICK TestUser" >&3
echo "USER testuser 0 * :Test User" >&3
sleep 2
echo "PING :test" >&3
read -t 3 response <&3
echo "Response: $response"
exec 3<&-
exec 3>&-
' && echo "✅ Connection test passed" || echo "❌ Connection test failed"
echo
echo "Testing graceful shutdown (SIGTERM)..."
echo "Sending SIGTERM to server..."
# Send SIGTERM and monitor for graceful shutdown
kill -TERM $SERVER_PID
# Wait up to 15 seconds for graceful shutdown
TIMEOUT=15
for i in $(seq 1 $TIMEOUT); do
if ! kill -0 $SERVER_PID 2>/dev/null; then
echo "✅ Server shut down gracefully after $i seconds"
echo
echo "=== Shutdown Test Complete ==="
exit 0
fi
sleep 1
echo -n "."
done
echo
echo "⚠️ Server did not shut down gracefully within $TIMEOUT seconds"
echo "Forcing shutdown..."
kill -9 $SERVER_PID 2>/dev/null
echo
echo "=== Shutdown Test Complete ==="

138
test_stress.sh Normal file
View File

@@ -0,0 +1,138 @@
#!/bin/bash
# TechIRCd Stability Stress Test
echo "=== TechIRCd Stability Stress Test ==="
echo "This test simulates conditions that previously caused freezing"
echo
# Start the server
echo "Starting TechIRCd..."
./techircd start &
SERVER_PID=$!
echo "Server started with PID: $SERVER_PID"
echo "Waiting 3 seconds for server to initialize..."
sleep 3
# Function to create a client connection
create_client() {
local client_id=$1
local duration=$2
timeout ${duration}s bash -c "
exec 3<>/dev/tcp/localhost/6667
echo 'NICK TestUser$client_id' >&3
echo 'USER testuser$client_id 0 * :Test User $client_id' >&3
sleep 2
echo 'JOIN #test' >&3
# Send periodic messages to keep connection alive
for i in {1..20}; do
echo 'PING :client$client_id' >&3
sleep 1
echo 'PRIVMSG #test :Message \$i from client $client_id' >&3
sleep 1
done
echo 'QUIT :Test complete' >&3
exec 3<&-
exec 3>&-
" 2>/dev/null &
}
# Function to create unstable client (connects and disconnects quickly)
create_unstable_client() {
local client_id=$1
timeout 5s bash -c "
exec 3<>/dev/tcp/localhost/6667
echo 'NICK Unstable$client_id' >&3
echo 'USER unstable$client_id 0 * :Unstable User $client_id' >&3
sleep 1
# Abruptly close connection without QUIT
exec 3<&-
exec 3>&-
" 2>/dev/null &
}
echo "Phase 1: Testing multiple stable connections..."
# Create 10 stable clients
for i in {1..10}; do
create_client $i 30
echo -n "."
sleep 0.5
done
echo " (10 stable clients created)"
echo "Phase 2: Testing unstable connections (simulating network issues)..."
# Create 20 unstable clients that disconnect abruptly
for i in {1..20}; do
create_unstable_client $i
echo -n "."
sleep 0.2
done
echo " (20 unstable clients created)"
echo "Phase 3: Testing rapid connection attempts..."
# Create many short-lived connections
for i in {1..30}; do
timeout 2s bash -c "
exec 3<>/dev/tcp/localhost/6667
echo 'NICK Rapid$i' >&3
exec 3<&-
exec 3>&-
" 2>/dev/null &
sleep 0.1
done
echo "30 rapid connections created"
echo
echo "Monitoring server health for 45 seconds..."
echo "Press Ctrl+C to interrupt test early"
# Monitor server for 45 seconds
for i in {1..45}; do
if ! kill -0 $SERVER_PID 2>/dev/null; then
echo "❌ Server died during stress test!"
echo "=== Stress Test Failed ==="
exit 1
fi
# Test server responsiveness every 10 seconds
if [ $((i % 10)) -eq 0 ]; then
echo "Testing server responsiveness at ${i}s..."
timeout 3s bash -c '
exec 3<>/dev/tcp/localhost/6667
echo "NICK HealthCheck$RANDOM" >&3
echo "USER healthcheck 0 * :Health Check" >&3
sleep 1
echo "QUIT :Health check complete" >&3
exec 3<&-
exec 3>&-
' 2>/dev/null && echo "✅ Server responsive" || echo "⚠️ Server response slow/failed"
fi
echo -n "."
sleep 1
done
echo
echo "✅ Server survived 45-second stress test!"
echo "Testing graceful shutdown after stress..."
kill -TERM $SERVER_PID
# Wait for shutdown
TIMEOUT=15
for i in $(seq 1 $TIMEOUT); do
if ! kill -0 $SERVER_PID 2>/dev/null; then
echo "✅ Server shut down gracefully after stress test"
echo "=== Stress Test Passed ==="
exit 0
fi
sleep 1
done
echo "⚠️ Server did not shut down gracefully, forcing..."
kill -9 $SERVER_PID 2>/dev/null
echo "=== Stress Test Completed with Warning ==="

272
tools/build.go Normal file
View File

@@ -0,0 +1,272 @@
package main
import (
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
const (
binaryName = "techircd"
version = "1.0.0"
)
func main() {
var (
buildFlag = flag.Bool("build", false, "Build the binary")
runFlag = flag.Bool("run", false, "Build and run the server")
testFlag = flag.Bool("test", false, "Run all tests")
cleanFlag = flag.Bool("clean", false, "Clean build artifacts")
fmtFlag = flag.Bool("fmt", false, "Format Go code")
lintFlag = flag.Bool("lint", false, "Run linters")
buildAllFlag = flag.Bool("build-all", false, "Build for multiple platforms")
releaseFlag = flag.Bool("release", false, "Create optimized release build")
helpFlag = flag.Bool("help", false, "Show help message")
)
flag.Parse()
if *helpFlag || flag.NFlag() == 0 {
showHelp()
return
}
switch {
case *buildFlag:
build()
case *runFlag:
build()
run()
case *testFlag:
test()
case *cleanFlag:
clean()
case *fmtFlag:
format()
case *lintFlag:
lint()
case *buildAllFlag:
buildAll()
case *releaseFlag:
release()
}
}
func showHelp() {
fmt.Println("TechIRCd Build Tool")
fmt.Println("")
fmt.Println("Usage:")
fmt.Println(" go run build.go [options]")
fmt.Println("")
fmt.Println("Options:")
fmt.Println(" -build Build the binary")
fmt.Println(" -run Build and run the server")
fmt.Println(" -test Run all tests")
fmt.Println(" -clean Clean build artifacts")
fmt.Println(" -fmt Format Go code")
fmt.Println(" -lint Run linters")
fmt.Println(" -build-all Build for multiple platforms")
fmt.Println(" -release Create optimized release build")
fmt.Println(" -help Show this help message")
}
func build() {
fmt.Println("Building TechIRCd...")
gitVersion, err := exec.Command("git", "describe", "--tags", "--always", "--dirty").Output()
var versionStr string
if err != nil {
versionStr = version
} else {
versionStr = strings.TrimSpace(string(gitVersion))
}
ldflags := fmt.Sprintf("-ldflags=-X main.version=%s", versionStr)
cmd := exec.Command("go", "build", ldflags, "-o", binaryName, ".")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Printf("Build failed: %v\n", err)
os.Exit(1)
}
fmt.Println("Build completed successfully!")
}
func run() {
fmt.Println("Starting TechIRCd...")
cmd := exec.Command("./" + binaryName)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
if err := cmd.Run(); err != nil {
fmt.Printf("Run failed: %v\n", err)
os.Exit(1)
}
}
func test() {
fmt.Println("Running tests...")
cmd := exec.Command("go", "test", "-v", "-race", "./...")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Printf("Tests failed: %v\n", err)
os.Exit(1)
}
fmt.Println("All tests passed!")
}
func clean() {
fmt.Println("Cleaning build artifacts...")
// Remove binary files
patterns := []string{
binaryName + "*",
"coverage.out",
"coverage.html",
}
for _, pattern := range patterns {
matches, err := filepath.Glob(pattern)
if err != nil {
continue
}
for _, match := range matches {
if err := os.Remove(match); err != nil {
fmt.Printf("Failed to remove %s: %v\n", match, err)
} else {
fmt.Printf("Removed %s\n", match)
}
}
}
fmt.Println("Clean completed!")
}
func format() {
fmt.Println("Formatting Go code...")
cmd := exec.Command("go", "fmt", "./...")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Printf("Format failed: %v\n", err)
os.Exit(1)
}
// Try to run goimports if available
if _, err := exec.LookPath("goimports"); err == nil {
fmt.Println("Running goimports...")
cmd := exec.Command("goimports", "-w", "-local", "github.com/ComputerTech312/TechIRCd", ".")
cmd.Run() // Don't fail if this doesn't work
}
fmt.Println("Format completed!")
}
func lint() {
fmt.Println("Running linters...")
if _, err := exec.LookPath("golangci-lint"); err != nil {
fmt.Println("golangci-lint not found, skipping...")
return
}
cmd := exec.Command("golangci-lint", "run")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Printf("Linting found issues: %v\n", err)
// Don't exit on lint errors, just report them
} else {
fmt.Println("No linting issues found!")
}
}
func buildAll() {
fmt.Println("Building for multiple platforms...")
platforms := []struct {
goos string
goarch string
ext string
}{
{"linux", "amd64", ""},
{"windows", "amd64", ".exe"},
{"darwin", "amd64", ""},
{"darwin", "arm64", ""},
}
gitVersion, err := exec.Command("git", "describe", "--tags", "--always", "--dirty").Output()
var versionStr string
if err != nil {
versionStr = version
} else {
versionStr = strings.TrimSpace(string(gitVersion))
}
for _, platform := range platforms {
outputName := fmt.Sprintf("%s-%s-%s%s", binaryName, platform.goos, platform.goarch, platform.ext)
fmt.Printf("Building %s...\n", outputName)
ldflags := fmt.Sprintf("-ldflags=-X main.version=%s", versionStr)
cmd := exec.Command("go", "build", ldflags, "-o", outputName, ".")
cmd.Env = append(os.Environ(),
"GOOS="+platform.goos,
"GOARCH="+platform.goarch,
)
if err := cmd.Run(); err != nil {
fmt.Printf("Failed to build %s: %v\n", outputName, err)
} else {
fmt.Printf("Built %s successfully!\n", outputName)
}
}
fmt.Println("Cross-platform build completed!")
}
func release() {
fmt.Println("Creating optimized release build...")
gitVersion, err := exec.Command("git", "describe", "--tags", "--always", "--dirty").Output()
var versionStr string
if err != nil {
versionStr = version
} else {
versionStr = strings.TrimSpace(string(gitVersion))
}
ldflags := fmt.Sprintf("-ldflags=-X main.version=%s", versionStr)
cmd := exec.Command("go", "build", ldflags, "-a", "-installsuffix", "cgo", "-o", binaryName, ".")
cmd.Env = append(os.Environ(), "CGO_ENABLED=0")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Printf("Release build failed: %v\n", err)
os.Exit(1)
}
// Get file info to show size
if info, err := os.Stat(binaryName); err == nil {
fmt.Printf("Release build completed! Binary size: %.2f MB\n", float64(info.Size())/1024/1024)
} else {
fmt.Println("Release build completed!")
}
}

525
tools/go_stress_test.go Normal file
View File

@@ -0,0 +1,525 @@
package main
import (
"bufio"
"encoding/json"
"fmt"
"log"
"math/rand"
"net"
"os"
"sync"
"time"
)
// StressConfig defines the configuration for stress testing
type StressConfig struct {
Server struct {
Host string `json:"host"`
Port int `json:"port"`
} `json:"server"`
Test struct {
MaxClients int `json:"max_clients"`
ConnectDelay int `json:"connect_delay_ms"`
ActionInterval int `json:"action_interval_ms"`
TestDuration int `json:"test_duration_seconds"`
RandomSeed int `json:"random_seed"`
} `json:"test"`
Behavior struct {
JoinChannels bool `json:"join_channels"`
SendMessages bool `json:"send_messages"`
ChangeNicks bool `json:"change_nicks"`
UseNewCommands bool `json:"use_new_commands"`
RandomQuit bool `json:"random_quit"`
MessageRate float64 `json:"message_rate"`
ChannelJoinRate float64 `json:"channel_join_rate"`
CommandRate float64 `json:"command_rate"`
} `json:"behavior"`
Channels []string `json:"channels"`
Messages []string `json:"messages"`
Commands []string `json:"commands"`
}
// IRCClient represents a single IRC client connection
type IRCClient struct {
ID int
Nick string
Conn net.Conn
Reader *bufio.Reader
Writer *bufio.Writer
Channels []string
Active bool
mu sync.Mutex
}
// StressTest manages the entire stress testing operation
type StressTest struct {
Config *StressConfig
Clients []*IRCClient
Stats *TestStats
quit chan bool
}
// TestStats tracks testing statistics
type TestStats struct {
ConnectedClients int
MessagesSent int
CommandsSent int
ChannelsJoined int
Errors int
StartTime time.Time
mu sync.Mutex
}
// LoadConfig loads configuration from JSON file
func LoadConfig(filename string) (*StressConfig, error) {
file, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("error opening config file: %v", err)
}
defer file.Close()
config := &StressConfig{}
decoder := json.NewDecoder(file)
if err := decoder.Decode(config); err != nil {
return nil, fmt.Errorf("error parsing config: %v", err)
}
return config, nil
}
// CreateDefaultConfig creates a default configuration file
func CreateDefaultConfig(filename string) error {
config := &StressConfig{
Server: struct {
Host string `json:"host"`
Port int `json:"port"`
}{
Host: "localhost",
Port: 6667,
},
Test: struct {
MaxClients int `json:"max_clients"`
ConnectDelay int `json:"connect_delay_ms"`
ActionInterval int `json:"action_interval_ms"`
TestDuration int `json:"test_duration_seconds"`
RandomSeed int `json:"random_seed"`
}{
MaxClients: 500,
ConnectDelay: 50,
ActionInterval: 1000,
TestDuration: 300,
RandomSeed: 42,
},
Behavior: struct {
JoinChannels bool `json:"join_channels"`
SendMessages bool `json:"send_messages"`
ChangeNicks bool `json:"change_nicks"`
UseNewCommands bool `json:"use_new_commands"`
RandomQuit bool `json:"random_quit"`
MessageRate float64 `json:"message_rate"`
ChannelJoinRate float64 `json:"channel_join_rate"`
CommandRate float64 `json:"command_rate"`
}{
JoinChannels: true,
SendMessages: true,
ChangeNicks: true,
UseNewCommands: true,
RandomQuit: false,
MessageRate: 0.3,
ChannelJoinRate: 0.2,
CommandRate: 0.1,
},
Channels: []string{
"#test", "#stress", "#general", "#random", "#chaos",
"#lobby", "#gaming", "#tech", "#chat", "#help",
},
Messages: []string{
"Hello everyone!",
"This is a stress test message",
"How is everyone doing?",
"Testing the server stability",
"Random message from client",
"IRC is awesome!",
"TechIRCd rocks!",
"Can you see this message?",
"Stress testing in progress",
"Everything working fine here",
},
Commands: []string{
"MOTD", "RULES", "MAP", "TIME", "VERSION", "LUSERS",
"WHO #test", "WHOIS testuser", "LIST",
},
}
file, err := os.Create(filename)
if err != nil {
return fmt.Errorf("error creating config file: %v", err)
}
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
return encoder.Encode(config)
}
// NewStressTest creates a new stress test instance
func NewStressTest(config *StressConfig) *StressTest {
return &StressTest{
Config: config,
Clients: make([]*IRCClient, 0, config.Test.MaxClients),
Stats: &TestStats{
StartTime: time.Now(),
},
quit: make(chan bool),
}
}
// Connect establishes connection to IRC server
func (c *IRCClient) Connect(host string, port int) error {
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
if err != nil {
return fmt.Errorf("failed to connect: %v", err)
}
c.Conn = conn
c.Reader = bufio.NewReader(conn)
c.Writer = bufio.NewWriter(conn)
c.Active = true
return nil
}
// Register performs IRC client registration
func (c *IRCClient) Register() error {
commands := []string{
fmt.Sprintf("NICK %s", c.Nick),
fmt.Sprintf("USER %s 0 * :Stress Test Client %d", c.Nick, c.ID),
}
for _, cmd := range commands {
if err := c.SendCommand(cmd); err != nil {
return fmt.Errorf("registration failed: %v", err)
}
}
return nil
}
// SendCommand sends a command to the IRC server
func (c *IRCClient) SendCommand(command string) error {
c.mu.Lock()
defer c.mu.Unlock()
if !c.Active || c.Writer == nil {
return fmt.Errorf("client not active")
}
_, err := c.Writer.WriteString(command + "\r\n")
if err != nil {
return err
}
return c.Writer.Flush()
}
// ReadMessages continuously reads messages from server
func (c *IRCClient) ReadMessages(stats *TestStats) {
defer func() {
c.Active = false
if c.Conn != nil {
c.Conn.Close()
}
}()
for c.Active {
if c.Reader == nil {
break
}
line, err := c.Reader.ReadString('\n')
if err != nil {
stats.mu.Lock()
stats.Errors++
stats.mu.Unlock()
break
}
// Handle PING responses
if len(line) > 4 && line[:4] == "PING" {
pong := "PONG" + line[4:]
c.SendCommand(pong[:len(pong)-2]) // Remove \r\n
}
}
}
// PerformRandomAction performs a random IRC action
func (c *IRCClient) PerformRandomAction(config *StressConfig, stats *TestStats) {
if !c.Active {
return
}
action := rand.Float64()
switch {
case action < config.Behavior.MessageRate && config.Behavior.SendMessages:
c.SendRandomMessage(config, stats)
case action < config.Behavior.MessageRate+config.Behavior.ChannelJoinRate && config.Behavior.JoinChannels:
c.JoinRandomChannel(config, stats)
case action < config.Behavior.MessageRate+config.Behavior.ChannelJoinRate+config.Behavior.CommandRate && config.Behavior.UseNewCommands:
c.SendRandomCommand(config, stats)
case config.Behavior.ChangeNicks && rand.Float64() < 0.05:
c.ChangeNick(stats)
}
}
// SendRandomMessage sends a random message to a random channel
func (c *IRCClient) SendRandomMessage(config *StressConfig, stats *TestStats) {
if len(c.Channels) == 0 {
return
}
channel := c.Channels[rand.Intn(len(c.Channels))]
message := config.Messages[rand.Intn(len(config.Messages))]
cmd := fmt.Sprintf("PRIVMSG %s :%s", channel, message)
if err := c.SendCommand(cmd); err == nil {
stats.mu.Lock()
stats.MessagesSent++
stats.mu.Unlock()
} else {
stats.mu.Lock()
stats.Errors++
stats.mu.Unlock()
}
}
// JoinRandomChannel joins a random channel
func (c *IRCClient) JoinRandomChannel(config *StressConfig, stats *TestStats) {
channel := config.Channels[rand.Intn(len(config.Channels))]
// Check if already in channel
for _, ch := range c.Channels {
if ch == channel {
return
}
}
cmd := fmt.Sprintf("JOIN %s", channel)
if err := c.SendCommand(cmd); err == nil {
c.Channels = append(c.Channels, channel)
stats.mu.Lock()
stats.ChannelsJoined++
stats.mu.Unlock()
} else {
stats.mu.Lock()
stats.Errors++
stats.mu.Unlock()
}
}
// SendRandomCommand sends a random IRC command
func (c *IRCClient) SendRandomCommand(config *StressConfig, stats *TestStats) {
command := config.Commands[rand.Intn(len(config.Commands))]
if err := c.SendCommand(command); err == nil {
stats.mu.Lock()
stats.CommandsSent++
stats.mu.Unlock()
} else {
stats.mu.Lock()
stats.Errors++
stats.mu.Unlock()
}
}
// ChangeNick changes the client's nickname
func (c *IRCClient) ChangeNick(stats *TestStats) {
newNick := fmt.Sprintf("User%d_%d", c.ID, rand.Intn(1000))
cmd := fmt.Sprintf("NICK %s", newNick)
if err := c.SendCommand(cmd); err == nil {
c.Nick = newNick
} else {
stats.mu.Lock()
stats.Errors++
stats.mu.Unlock()
}
}
// CreateClient creates and connects a new IRC client
func (st *StressTest) CreateClient(id int) error {
client := &IRCClient{
ID: id,
Nick: fmt.Sprintf("StressUser%d", id),
Channels: make([]string, 0),
}
// Connect to server
if err := client.Connect(st.Config.Server.Host, st.Config.Server.Port); err != nil {
return fmt.Errorf("client %d connection failed: %v", id, err)
}
// Register with IRC server
if err := client.Register(); err != nil {
client.Conn.Close()
return fmt.Errorf("client %d registration failed: %v", id, err)
}
st.Clients = append(st.Clients, client)
st.Stats.mu.Lock()
st.Stats.ConnectedClients++
st.Stats.mu.Unlock()
// Start message reader goroutine
go client.ReadMessages(st.Stats)
return nil
}
// RunStressTest executes the complete stress test
func (st *StressTest) RunStressTest() error {
log.Printf("Starting stress test with %d clients", st.Config.Test.MaxClients)
// Set random seed
rand.Seed(int64(st.Config.Test.RandomSeed))
// Connect clients gradually
connectDelay := time.Duration(st.Config.Test.ConnectDelay) * time.Millisecond
for i := 0; i < st.Config.Test.MaxClients; i++ {
if err := st.CreateClient(i); err != nil {
log.Printf("Failed to create client %d: %v", i, err)
st.Stats.mu.Lock()
st.Stats.Errors++
st.Stats.mu.Unlock()
continue
}
if i%10 == 0 {
log.Printf("Connected %d/%d clients", i+1, st.Config.Test.MaxClients)
}
time.Sleep(connectDelay)
}
log.Printf("All clients connected. Starting activity simulation...")
// Start activity simulation
actionInterval := time.Duration(st.Config.Test.ActionInterval) * time.Millisecond
testDuration := time.Duration(st.Config.Test.TestDuration) * time.Second
actionTicker := time.NewTicker(actionInterval)
defer actionTicker.Stop()
statsTicker := time.NewTicker(10 * time.Second)
defer statsTicker.Stop()
testTimer := time.NewTimer(testDuration)
defer testTimer.Stop()
for {
select {
case <-actionTicker.C:
// Perform random actions for random clients
numActions := rand.Intn(len(st.Clients)/4) + 1
for i := 0; i < numActions; i++ {
if len(st.Clients) > 0 {
clientIndex := rand.Intn(len(st.Clients))
go st.Clients[clientIndex].PerformRandomAction(st.Config, st.Stats)
}
}
case <-statsTicker.C:
st.PrintStats()
case <-testTimer.C:
log.Println("Test duration completed. Shutting down...")
st.Shutdown()
return nil
case <-st.quit:
log.Println("Test interrupted. Shutting down...")
st.Shutdown()
return nil
}
}
}
// PrintStats prints current test statistics
func (st *StressTest) PrintStats() {
st.Stats.mu.Lock()
defer st.Stats.mu.Unlock()
elapsed := time.Since(st.Stats.StartTime)
log.Printf("=== STRESS TEST STATS ===")
log.Printf("Runtime: %v", elapsed.Round(time.Second))
log.Printf("Connected Clients: %d", st.Stats.ConnectedClients)
log.Printf("Messages Sent: %d", st.Stats.MessagesSent)
log.Printf("Commands Sent: %d", st.Stats.CommandsSent)
log.Printf("Channels Joined: %d", st.Stats.ChannelsJoined)
log.Printf("Errors: %d", st.Stats.Errors)
if elapsed.Seconds() > 0 {
log.Printf("Messages/sec: %.2f", float64(st.Stats.MessagesSent)/elapsed.Seconds())
log.Printf("Commands/sec: %.2f", float64(st.Stats.CommandsSent)/elapsed.Seconds())
}
log.Printf("========================")
}
// Shutdown gracefully shuts down all clients
func (st *StressTest) Shutdown() {
log.Println("Shutting down all clients...")
for i, client := range st.Clients {
if client.Active {
client.SendCommand("QUIT :Stress test completed")
client.Active = false
if client.Conn != nil {
client.Conn.Close()
}
}
if i%50 == 0 {
log.Printf("Disconnected %d/%d clients", i+1, len(st.Clients))
}
}
st.PrintStats()
log.Println("Stress test completed!")
}
func main() {
configFile := "stress_test_config.json"
// Check if config file exists, create default if not
if _, err := os.Stat(configFile); os.IsNotExist(err) {
log.Printf("Config file %s not found. Creating default...", configFile)
if err := CreateDefaultConfig(configFile); err != nil {
log.Fatalf("Failed to create default config: %v", err)
}
log.Printf("Default config created at %s. Please review and modify as needed.", configFile)
log.Println("Run the command again to start the stress test.")
return
}
// Load configuration
config, err := LoadConfig(configFile)
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
log.Printf("Loaded config: %d clients, %d second test", config.Test.MaxClients, config.Test.TestDuration)
// Create and run stress test
stressTest := NewStressTest(config)
if err := stressTest.RunStressTest(); err != nil {
log.Fatalf("Stress test failed: %v", err)
}
}

19
tools/run_stress_test.sh Normal file
View File

@@ -0,0 +1,19 @@
#!/bin/bash
# Build and run the Go IRC Stress Tester
echo "Building Go IRC Stress Tester..."
cd "$(dirname "$0")"
# Build the stress tester
go build -o irc_stress_test go_stress_test.go
if [ $? -eq 0 ]; then
echo "Build successful!"
echo "Running stress test..."
echo "========================"
./irc_stress_test
else
echo "Build failed!"
exit 1
fi

528
tools/stress_test.go Normal file
View File

@@ -0,0 +1,528 @@
package main
import (
"bufio"
"encoding/json"
"fmt"
"log"
"math/rand"
"net"
"os"
"sync"
"time"
)
// StressConfig defines the configuration for stress testing
type StressConfig struct {
Server struct {
Host string `json:"host"`
Port int `json:"port"`
} `json:"server"`
Test struct {
MaxClients int `json:"max_clients"`
ConnectDelay int `json:"connect_delay_ms"`
ActionInterval int `json:"action_interval_ms"`
TestDuration int `json:"test_duration_seconds"`
RandomSeed int `json:"random_seed"`
} `json:"test"`
Behavior struct {
JoinChannels bool `json:"join_channels"`
SendMessages bool `json:"send_messages"`
ChangeNicks bool `json:"change_nicks"`
UseNewCommands bool `json:"use_new_commands"`
RandomQuit bool `json:"random_quit"`
MessageRate float64 `json:"message_rate"`
ChannelJoinRate float64 `json:"channel_join_rate"`
CommandRate float64 `json:"command_rate"`
} `json:"behavior"`
Channels []string `json:"channels"`
Messages []string `json:"messages"`
Commands []string `json:"commands"`
}
// IRCClient represents a single IRC client connection
type IRCClient struct {
ID int
Nick string
Conn net.Conn
Reader *bufio.Reader
Writer *bufio.Writer
Channels []string
Active bool
mu sync.Mutex
}
// StressTest manages the entire stress testing operation
type StressTest struct {
Config *StressConfig
Clients []*IRCClient
Stats *TestStats
wg sync.WaitGroup
quit chan bool
}
// TestStats tracks testing statistics
type TestStats struct {
ConnectedClients int
MessagesSent int
CommandsSent int
ChannelsJoined int
Errors int
StartTime time.Time
mu sync.Mutex
}
// LoadConfig loads configuration from JSON file
func LoadConfig(filename string) (*StressConfig, error) {
file, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("error opening config file: %v", err)
}
defer file.Close()
config := &StressConfig{}
decoder := json.NewDecoder(file)
if err := decoder.Decode(config); err != nil {
return nil, fmt.Errorf("error parsing config: %v", err)
}
return config, nil
}
// CreateDefaultConfig creates a default configuration file
func CreateDefaultConfig(filename string) error {
config := &StressConfig{
Server: struct {
Host string `json:"host"`
Port int `json:"port"`
}{
Host: "localhost",
Port: 6667,
},
Test: struct {
MaxClients int `json:"max_clients"`
ConnectDelay int `json:"connect_delay_ms"`
ActionInterval int `json:"action_interval_ms"`
TestDuration int `json:"test_duration_seconds"`
RandomSeed int `json:"random_seed"`
}{
MaxClients: 500,
ConnectDelay: 50,
ActionInterval: 1000,
TestDuration: 300,
RandomSeed: 42,
},
Behavior: struct {
JoinChannels bool `json:"join_channels"`
SendMessages bool `json:"send_messages"`
ChangeNicks bool `json:"change_nicks"`
UseNewCommands bool `json:"use_new_commands"`
RandomQuit bool `json:"random_quit"`
MessageRate float64 `json:"message_rate"`
ChannelJoinRate float64 `json:"channel_join_rate"`
CommandRate float64 `json:"command_rate"`
}{
JoinChannels: true,
SendMessages: true,
ChangeNicks: true,
UseNewCommands: true,
RandomQuit: false,
MessageRate: 0.3,
ChannelJoinRate: 0.2,
CommandRate: 0.1,
},
Channels: []string{
"#test", "#stress", "#general", "#random", "#chaos",
"#lobby", "#gaming", "#tech", "#chat", "#help",
},
Messages: []string{
"Hello everyone!",
"This is a stress test message",
"How is everyone doing?",
"Testing the server stability",
"Random message from client",
"IRC is awesome!",
"TechIRCd rocks!",
"Can you see this message?",
"Stress testing in progress",
"Everything working fine here",
},
Commands: []string{
"MOTD", "RULES", "MAP", "TIME", "VERSION", "LUSERS",
"WHO #test", "WHOIS testuser", "LIST",
},
}
file, err := os.Create(filename)
if err != nil {
return fmt.Errorf("error creating config file: %v", err)
}
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
return encoder.Encode(config)
}
// NewStressTest creates a new stress test instance
func NewStressTest(config *StressConfig) *StressTest {
return &StressTest{
Config: config,
Clients: make([]*IRCClient, 0, config.Test.MaxClients),
Stats: &TestStats{
StartTime: time.Now(),
},
quit: make(chan bool),
}
}
// Connect establishes connection to IRC server
func (c *IRCClient) Connect(host string, port int) error {
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
if err != nil {
return fmt.Errorf("failed to connect: %v", err)
}
c.Conn = conn
c.Reader = bufio.NewReader(conn)
c.Writer = bufio.NewWriter(conn)
c.Active = true
return nil
}
// Register performs IRC client registration
func (c *IRCClient) Register() error {
commands := []string{
fmt.Sprintf("NICK %s", c.Nick),
fmt.Sprintf("USER %s 0 * :Stress Test Client %d", c.Nick, c.ID),
}
for _, cmd := range commands {
if err := c.SendCommand(cmd); err != nil {
return fmt.Errorf("registration failed: %v", err)
}
}
return nil
}
// SendCommand sends a command to the IRC server
func (c *IRCClient) SendCommand(command string) error {
c.mu.Lock()
defer c.mu.Unlock()
if !c.Active || c.Writer == nil {
return fmt.Errorf("client not active")
}
_, err := c.Writer.WriteString(command + "\r\n")
if err != nil {
return err
}
return c.Writer.Flush()
}
// ReadMessages continuously reads messages from server
func (c *IRCClient) ReadMessages(stats *TestStats) {
defer func() {
c.Active = false
if c.Conn != nil {
c.Conn.Close()
}
}()
for c.Active {
if c.Reader == nil {
break
}
line, err := c.Reader.ReadString('\n')
if err != nil {
stats.mu.Lock()
stats.Errors++
stats.mu.Unlock()
break
}
// Handle PING responses
if len(line) > 4 && line[:4] == "PING" {
pong := "PONG" + line[4:]
c.SendCommand(pong[:len(pong)-2]) // Remove \r\n
}
}
}
// PerformRandomAction performs a random IRC action
func (c *IRCClient) PerformRandomAction(config *StressConfig, stats *TestStats) {
if !c.Active {
return
}
action := rand.Float64()
switch {
case action < config.Behavior.MessageRate && config.Behavior.SendMessages:
c.SendRandomMessage(config, stats)
case action < config.Behavior.MessageRate+config.Behavior.ChannelJoinRate && config.Behavior.JoinChannels:
c.JoinRandomChannel(config, stats)
case action < config.Behavior.MessageRate+config.Behavior.ChannelJoinRate+config.Behavior.CommandRate && config.Behavior.UseNewCommands:
c.SendRandomCommand(config, stats)
case config.Behavior.ChangeNicks && rand.Float64() < 0.05:
c.ChangeNick(stats)
}
}
// SendRandomMessage sends a random message to a random channel
func (c *IRCClient) SendRandomMessage(config *StressConfig, stats *TestStats) {
if len(c.Channels) == 0 {
return
}
channel := c.Channels[rand.Intn(len(c.Channels))]
message := config.Messages[rand.Intn(len(config.Messages))]
cmd := fmt.Sprintf("PRIVMSG %s :%s", channel, message)
if err := c.SendCommand(cmd); err == nil {
stats.mu.Lock()
stats.MessagesSent++
stats.mu.Unlock()
} else {
stats.mu.Lock()
stats.Errors++
stats.mu.Unlock()
}
}
// JoinRandomChannel joins a random channel
func (c *IRCClient) JoinRandomChannel(config *StressConfig, stats *TestStats) {
channel := config.Channels[rand.Intn(len(config.Channels))]
// Check if already in channel
for _, ch := range c.Channels {
if ch == channel {
return
}
}
cmd := fmt.Sprintf("JOIN %s", channel)
if err := c.SendCommand(cmd); err == nil {
c.Channels = append(c.Channels, channel)
stats.mu.Lock()
stats.ChannelsJoined++
stats.mu.Unlock()
} else {
stats.mu.Lock()
stats.Errors++
stats.mu.Unlock()
}
}
// SendRandomCommand sends a random IRC command
func (c *IRCClient) SendRandomCommand(config *StressConfig, stats *TestStats) {
command := config.Commands[rand.Intn(len(config.Commands))]
if err := c.SendCommand(command); err == nil {
stats.mu.Lock()
stats.CommandsSent++
stats.mu.Unlock()
} else {
stats.mu.Lock()
stats.Errors++
stats.mu.Unlock()
}
}
// ChangeNick changes the client's nickname
func (c *IRCClient) ChangeNick(stats *TestStats) {
newNick := fmt.Sprintf("User%d_%d", c.ID, rand.Intn(1000))
cmd := fmt.Sprintf("NICK %s", newNick)
if err := c.SendCommand(cmd); err == nil {
c.Nick = newNick
} else {
stats.mu.Lock()
stats.Errors++
stats.mu.Unlock()
}
}
// CreateClient creates and connects a new IRC client
func (st *StressTest) CreateClient(id int) error {
client := &IRCClient{
ID: id,
Nick: fmt.Sprintf("StressUser%d", id),
Channels: make([]string, 0),
}
// Connect to server
if err := client.Connect(st.Config.Server.Host, st.Config.Server.Port); err != nil {
return fmt.Errorf("client %d connection failed: %v", id, err)
}
// Register with IRC server
if err := client.Register(); err != nil {
client.Conn.Close()
return fmt.Errorf("client %d registration failed: %v", id, err)
}
st.Clients = append(st.Clients, client)
st.Stats.mu.Lock()
st.Stats.ConnectedClients++
st.Stats.mu.Unlock()
// Start message reader goroutine
go client.ReadMessages(st.Stats)
return nil
}
// RunStressTest executes the complete stress test
func (st *StressTest) RunStressTest() error {
log.Printf("Starting stress test with %d clients", st.Config.Test.MaxClients)
// Set random seed
rand.Seed(int64(st.Config.Test.RandomSeed))
// Connect clients gradually
connectDelay := time.Duration(st.Config.Test.ConnectDelay) * time.Millisecond
for i := 0; i < st.Config.Test.MaxClients; i++ {
if err := st.CreateClient(i); err != nil {
log.Printf("Failed to create client %d: %v", i, err)
st.Stats.mu.Lock()
st.Stats.Errors++
st.Stats.mu.Unlock()
continue
}
if i%10 == 0 {
log.Printf("Connected %d/%d clients", i+1, st.Config.Test.MaxClients)
}
time.Sleep(connectDelay)
}
log.Printf("All clients connected. Starting activity simulation...")
// Start activity simulation
actionInterval := time.Duration(st.Config.Test.ActionInterval) * time.Millisecond
testDuration := time.Duration(st.Config.Test.TestDuration) * time.Second
actionTicker := time.NewTicker(actionInterval)
defer actionTicker.Stop()
statsTicker := time.NewTicker(10 * time.Second)
defer statsTicker.Stop()
testTimer := time.NewTimer(testDuration)
defer testTimer.Stop()
for {
select {
case <-actionTicker.C:
// Perform random actions for random clients
numActions := rand.Intn(len(st.Clients)/4) + 1
for i := 0; i < numActions; i++ {
if len(st.Clients) > 0 {
clientIndex := rand.Intn(len(st.Clients))
go st.Clients[clientIndex].PerformRandomAction(st.Config, st.Stats)
}
}
case <-statsTicker.C:
st.PrintStats()
case <-testTimer.C:
log.Println("Test duration completed. Shutting down...")
st.Shutdown()
return nil
case <-st.quit:
log.Println("Test interrupted. Shutting down...")
st.Shutdown()
return nil
}
}
}
// PrintStats prints current test statistics
func (st *StressTest) PrintStats() {
st.Stats.mu.Lock()
defer st.Stats.mu.Unlock()
elapsed := time.Since(st.Stats.StartTime)
log.Printf("=== STRESS TEST STATS ===")
log.Printf("Runtime: %v", elapsed.Round(time.Second))
log.Printf("Connected Clients: %d", st.Stats.ConnectedClients)
log.Printf("Messages Sent: %d", st.Stats.MessagesSent)
log.Printf("Commands Sent: %d", st.Stats.CommandsSent)
log.Printf("Channels Joined: %d", st.Stats.ChannelsJoined)
log.Printf("Errors: %d", st.Stats.Errors)
if elapsed.Seconds() > 0 {
log.Printf("Messages/sec: %.2f", float64(st.Stats.MessagesSent)/elapsed.Seconds())
log.Printf("Commands/sec: %.2f", float64(st.Stats.CommandsSent)/elapsed.Seconds())
}
log.Printf("========================")
}
// Shutdown gracefully shuts down all clients
func (st *StressTest) Shutdown() {
log.Println("Shutting down all clients...")
for i, client := range st.Clients {
if client.Active {
client.SendCommand("QUIT :Stress test completed")
client.Active = false
if client.Conn != nil {
client.Conn.Close()
}
}
if i%50 == 0 {
log.Printf("Disconnected %d/%d clients", i+1, len(st.Clients))
}
}
st.PrintStats()
log.Println("Stress test completed!")
}
func main() {
configFile := "stress_test_config.json"
// Check if config file exists, create default if not
if _, err := os.Stat(configFile); os.IsNotExist(err) {
log.Printf("Config file %s not found. Creating default...", configFile)
if err := CreateDefaultConfig(configFile); err != nil {
log.Fatalf("Failed to create default config: %v", err)
}
log.Printf("Default config created at %s. Please review and modify as needed.", configFile)
log.Println("Run the command again to start the stress test.")
return
}
// Load configuration
config, err := LoadConfig(configFile)
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
log.Printf("Loaded config: %d clients, %d second test", config.Test.MaxClients, config.Test.TestDuration)
// Create and run stress test
stressTest := NewStressTest(config)
if err := stressTest.RunStressTest(); err != nil {
log.Fatalf("Stress test failed: %v", err)
}
}

464
tools/stress_test.py Normal file
View File

@@ -0,0 +1,464 @@
#!/usr/bin/env python3
"""
TechIRCd Advanced Stress Tester
===============================
Comprehensive IRC server stress testing tool with configurable behavior.
"""
import asyncio
import random
import time
import logging
from typing import List, Dict, Optional
import json
# =============================================================================
# CONFIGURATION - Edit these values to customize your stress test
# =============================================================================
CONFIG = {
# Server connection settings
"SERVER": {
"host": "localhost",
"port": 6667,
},
# Test parameters
"TEST": {
"max_clients": 300, # Number of concurrent clients
"connect_delay": 0.05, # Seconds between connections
"test_duration": 300, # Test duration in seconds
"action_interval": 1.0, # Seconds between random actions
"stats_interval": 10, # Seconds between stats reports
},
# Client behavior probabilities (0.0 = never, 1.0 = always)
"BEHAVIOR": {
"join_channels": True,
"send_messages": True,
"use_new_commands": True,
"change_nicks": True,
"random_quit": False,
# Action rates (probability per action cycle)
"message_rate": 0.4, # Chance to send a message
"channel_join_rate": 0.2, # Chance to join a channel
"command_rate": 0.15, # Chance to send a command
"nick_change_rate": 0.05, # Chance to change nick
"quit_rate": 0.01, # Chance to quit and reconnect
},
# IRC channels to use
"CHANNELS": [
"#test", "#stress", "#general", "#random", "#chaos",
"#lobby", "#gaming", "#tech", "#chat", "#help",
"#dev", "#admin", "#support", "#lounge", "#public"
],
# Random messages to send
"MESSAGES": [
"Hello everyone!",
"This is a stress test message",
"How is everyone doing today?",
"Testing server stability under load",
"Random message from stress test client",
"IRC is still the best chat protocol!",
"TechIRCd is handling this load well",
"Can you see this message?",
"Stress testing in progress...",
"Everything working fine here",
"Anyone else here for the stress test?",
"Server performance looking good!",
"Testing new IRC commands",
"This channel is quite active",
"Load testing is important for stability"
],
# IRC commands to test (including new ones!)
"COMMANDS": [
"MOTD",
"RULES",
"MAP",
"TIME",
"VERSION",
"LUSERS",
"WHO #test",
"WHOIS testuser",
"LIST",
"ADMIN",
"INFO",
"KNOCK #test",
"SETNAME :New real name from stress test"
],
# Logging configuration
"LOGGING": {
"level": "INFO", # DEBUG, INFO, WARNING, ERROR
"show_irc_traffic": False, # Set to True to see all IRC messages
}
}
# =============================================================================
# STRESS TESTING CODE - Don't modify unless you know what you're doing
# =============================================================================
class StressTestStats:
"""Tracks statistics during stress testing"""
def __init__(self):
self.start_time = time.time()
self.connected_clients = 0
self.messages_sent = 0
self.commands_sent = 0
self.channels_joined = 0
self.nick_changes = 0
self.errors = 0
self.reconnections = 0
def runtime(self) -> float:
return time.time() - self.start_time
def print_stats(self):
runtime = self.runtime()
print("\n" + "="*50)
print("STRESS TEST STATISTICS")
print("="*50)
print(f"Runtime: {runtime:.1f}s")
print(f"Connected Clients: {self.connected_clients}")
print(f"Messages Sent: {self.messages_sent}")
print(f"Commands Sent: {self.commands_sent}")
print(f"Channels Joined: {self.channels_joined}")
print(f"Nick Changes: {self.nick_changes}")
print(f"Reconnections: {self.reconnections}")
print(f"Errors: {self.errors}")
if runtime > 0:
print(f"Messages/sec: {self.messages_sent/runtime:.2f}")
print(f"Commands/sec: {self.commands_sent/runtime:.2f}")
print(f"Actions/sec: {(self.messages_sent + self.commands_sent)/runtime:.2f}")
print("="*50)
class IRCStressClient:
"""Individual IRC client for stress testing"""
def __init__(self, client_id: int, stats: StressTestStats):
self.client_id = client_id
self.nick = f"StressUser{client_id}"
self.user = f"stress{client_id}"
self.realname = f"Stress Test Client {client_id}"
self.channels = []
self.stats = stats
self.reader = None
self.writer = None
self.connected = False
self.registered = False
self.running = True
async def connect(self, host: str, port: int) -> bool:
"""Connect to IRC server"""
try:
self.reader, self.writer = await asyncio.open_connection(host, port)
self.connected = True
logging.debug(f"Client {self.client_id} connected to {host}:{port}")
return True
except Exception as e:
logging.error(f"Client {self.client_id} connection failed: {e}")
self.stats.errors += 1
return False
async def register(self) -> bool:
"""Register with IRC server"""
try:
await self.send_command(f"NICK {self.nick}")
await self.send_command(f"USER {self.user} 0 * :{self.realname}")
self.registered = True
self.stats.connected_clients += 1
logging.debug(f"Client {self.client_id} registered as {self.nick}")
return True
except Exception as e:
logging.error(f"Client {self.client_id} registration failed: {e}")
self.stats.errors += 1
return False
async def send_command(self, command: str):
"""Send IRC command to server"""
if not self.connected or not self.writer:
return
try:
message = f"{command}\r\n"
self.writer.write(message.encode())
await self.writer.drain()
if CONFIG["LOGGING"]["show_irc_traffic"]:
logging.debug(f"Client {self.client_id} >>> {command}")
except Exception as e:
logging.error(f"Client {self.client_id} send error: {e}")
self.stats.errors += 1
async def read_messages(self):
"""Read and handle messages from server"""
while self.running and self.connected:
try:
if not self.reader:
break
line = await self.reader.readline()
if not line:
break
message = line.decode().strip()
if not message:
continue
if CONFIG["LOGGING"]["show_irc_traffic"]:
logging.debug(f"Client {self.client_id} <<< {message}")
# Handle PING
if message.startswith("PING"):
pong = message.replace("PING", "PONG", 1)
await self.send_command(pong)
# Handle other server messages if needed
except Exception as e:
logging.error(f"Client {self.client_id} read error: {e}")
self.stats.errors += 1
break
async def join_random_channel(self):
"""Join a random channel"""
if not CONFIG["BEHAVIOR"]["join_channels"]:
return
channel = random.choice(CONFIG["CHANNELS"])
if channel not in self.channels:
await self.send_command(f"JOIN {channel}")
self.channels.append(channel)
self.stats.channels_joined += 1
logging.debug(f"Client {self.client_id} joined {channel}")
async def send_random_message(self):
"""Send a random message to a random channel"""
if not CONFIG["BEHAVIOR"]["send_messages"] or not self.channels:
return
channel = random.choice(self.channels)
message = random.choice(CONFIG["MESSAGES"])
await self.send_command(f"PRIVMSG {channel} :{message}")
self.stats.messages_sent += 1
logging.debug(f"Client {self.client_id} sent message to {channel}")
async def send_random_command(self):
"""Send a random IRC command"""
if not CONFIG["BEHAVIOR"]["use_new_commands"]:
return
command = random.choice(CONFIG["COMMANDS"])
await self.send_command(command)
self.stats.commands_sent += 1
logging.debug(f"Client {self.client_id} sent command: {command}")
async def change_nick(self):
"""Change nickname"""
if not CONFIG["BEHAVIOR"]["change_nicks"]:
return
new_nick = f"User{self.client_id}_{random.randint(1, 9999)}"
await self.send_command(f"NICK {new_nick}")
self.nick = new_nick
self.stats.nick_changes += 1
logging.debug(f"Client {self.client_id} changed nick to {new_nick}")
async def random_quit_reconnect(self):
"""Randomly quit and reconnect"""
if not CONFIG["BEHAVIOR"]["random_quit"]:
return
await self.send_command("QUIT :Reconnecting...")
await self.disconnect()
# Wait a moment then reconnect
await asyncio.sleep(random.uniform(1, 3))
if await self.connect(CONFIG["SERVER"]["host"], CONFIG["SERVER"]["port"]):
await self.register()
self.stats.reconnections += 1
logging.debug(f"Client {self.client_id} reconnected")
async def perform_random_action(self):
"""Perform a random IRC action"""
if not self.registered:
return
action = random.random()
if action < CONFIG["BEHAVIOR"]["message_rate"]:
await self.send_random_message()
elif action < CONFIG["BEHAVIOR"]["message_rate"] + CONFIG["BEHAVIOR"]["channel_join_rate"]:
await self.join_random_channel()
elif action < CONFIG["BEHAVIOR"]["message_rate"] + CONFIG["BEHAVIOR"]["channel_join_rate"] + CONFIG["BEHAVIOR"]["command_rate"]:
await self.send_random_command()
elif action < CONFIG["BEHAVIOR"]["message_rate"] + CONFIG["BEHAVIOR"]["channel_join_rate"] + CONFIG["BEHAVIOR"]["command_rate"] + CONFIG["BEHAVIOR"]["nick_change_rate"]:
await self.change_nick()
elif action < CONFIG["BEHAVIOR"]["message_rate"] + CONFIG["BEHAVIOR"]["channel_join_rate"] + CONFIG["BEHAVIOR"]["command_rate"] + CONFIG["BEHAVIOR"]["nick_change_rate"] + CONFIG["BEHAVIOR"]["quit_rate"]:
await self.random_quit_reconnect()
async def disconnect(self):
"""Disconnect from server"""
self.running = False
self.connected = False
if self.writer:
try:
await self.send_command("QUIT :Stress test completed")
self.writer.close()
await self.writer.wait_closed()
except:
pass
if self.registered:
self.stats.connected_clients -= 1
self.registered = False
class IRCStressTester:
"""Main stress testing coordinator"""
def __init__(self):
self.clients: List[IRCStressClient] = []
self.stats = StressTestStats()
self.running = False
# Setup logging
log_level = getattr(logging, CONFIG["LOGGING"]["level"])
logging.basicConfig(
level=log_level,
format='%(asctime)s - %(levelname)s - %(message)s',
datefmt='%H:%M:%S'
)
async def create_client(self, client_id: int) -> Optional[IRCStressClient]:
"""Create and connect a new client"""
client = IRCStressClient(client_id, self.stats)
if await client.connect(CONFIG["SERVER"]["host"], CONFIG["SERVER"]["port"]):
if await client.register():
# Start message reader
asyncio.create_task(client.read_messages())
return client
return None
async def connect_all_clients(self):
"""Connect all clients with delay"""
print(f"Connecting {CONFIG['TEST']['max_clients']} clients...")
for i in range(CONFIG["TEST"]["max_clients"]):
client = await self.create_client(i)
if client:
self.clients.append(client)
# Join initial channel
if CONFIG["BEHAVIOR"]["join_channels"]:
await client.join_random_channel()
# Progress reporting
if (i + 1) % 10 == 0:
print(f"Connected {i + 1}/{CONFIG['TEST']['max_clients']} clients")
# Delay between connections
if CONFIG["TEST"]["connect_delay"] > 0:
await asyncio.sleep(CONFIG["TEST"]["connect_delay"])
print(f"All {len(self.clients)} clients connected!")
async def run_activity_simulation(self):
"""Run the main activity simulation"""
print("Starting activity simulation...")
start_time = time.time()
last_stats = time.time()
while self.running and (time.time() - start_time) < CONFIG["TEST"]["test_duration"]:
# Perform random actions
active_clients = [c for c in self.clients if c.registered]
if active_clients:
# Select random clients to perform actions
num_actions = random.randint(1, len(active_clients) // 4 + 1)
selected_clients = random.sample(active_clients, min(num_actions, len(active_clients)))
# Perform actions concurrently
tasks = [client.perform_random_action() for client in selected_clients]
await asyncio.gather(*tasks, return_exceptions=True)
# Print stats periodically
if time.time() - last_stats >= CONFIG["TEST"]["stats_interval"]:
self.stats.print_stats()
last_stats = time.time()
# Wait before next action cycle
await asyncio.sleep(CONFIG["TEST"]["action_interval"])
async def shutdown_all_clients(self):
"""Gracefully disconnect all clients"""
print("Shutting down all clients...")
disconnect_tasks = []
for client in self.clients:
disconnect_tasks.append(client.disconnect())
# Wait for all disconnections
await asyncio.gather(*disconnect_tasks, return_exceptions=True)
print("All clients disconnected.")
async def run_stress_test(self):
"""Run the complete stress test"""
print("="*60)
print("TECHIRCD ADVANCED STRESS TESTER")
print("="*60)
print(f"Target: {CONFIG['SERVER']['host']}:{CONFIG['SERVER']['port']}")
print(f"Clients: {CONFIG['TEST']['max_clients']}")
print(f"Duration: {CONFIG['TEST']['test_duration']}s")
print(f"Channels: {len(CONFIG['CHANNELS'])}")
print(f"Commands: {len(CONFIG['COMMANDS'])}")
print("="*60)
self.running = True
try:
# Connect all clients
await self.connect_all_clients()
# Run activity simulation
await self.run_activity_simulation()
except KeyboardInterrupt:
print("\nTest interrupted by user!")
except Exception as e:
print(f"\nTest failed with error: {e}")
finally:
self.running = False
await self.shutdown_all_clients()
# Final stats
print("\nFINAL RESULTS:")
self.stats.print_stats()
async def main():
"""Main entry point"""
stress_tester = IRCStressTester()
await stress_tester.run_stress_test()
if __name__ == "__main__":
print("Starting TechIRCd Advanced Stress Tester...")
print("Press Ctrl+C to stop the test early.\n")
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\nTest terminated by user.")
except Exception as e:
print(f"\nFatal error: {e}")

533
tools/stress_tester.go Normal file
View File

@@ -0,0 +1,533 @@
package main
package main
import (
"bufio"
"encoding/json"
"fmt"
"log"
"math/rand"
"net"
"os"
"sync"
"time"
)
// StressConfig defines the configuration for stress testing
type StressConfig struct {
Server struct {
Host string `json:"host"`
Port int `json:"port"`
} `json:"server"`
Test struct {
MaxClients int `json:"max_clients"`
ConnectDelay int `json:"connect_delay_ms"`
ActionInterval int `json:"action_interval_ms"`
TestDuration int `json:"test_duration_seconds"`
RandomSeed int `json:"random_seed"`
} `json:"test"`
Behavior struct {
JoinChannels bool `json:"join_channels"`
SendMessages bool `json:"send_messages"`
ChangeNicks bool `json:"change_nicks"`
UseNewCommands bool `json:"use_new_commands"`
RandomQuit bool `json:"random_quit"`
MessageRate float64 `json:"message_rate"`
ChannelJoinRate float64 `json:"channel_join_rate"`
CommandRate float64 `json:"command_rate"`
} `json:"behavior"`
Channels []string `json:"channels"`
Messages []string `json:"messages"`
Commands []string `json:"commands"`
}
// IRCClient represents a single IRC client connection
type IRCClient struct {
ID int
Nick string
Conn net.Conn
Reader *bufio.Reader
Writer *bufio.Writer
Channels []string
Active bool
mu sync.Mutex
}
// StressTest manages the entire stress testing operation
type StressTest struct {
Config *StressConfig
Clients []*IRCClient
Stats *TestStats
quit chan bool
}
// TestStats tracks testing statistics
type TestStats struct {
ConnectedClients int
MessagesSent int
CommandsSent int
ChannelsJoined int
Errors int
StartTime time.Time
mu sync.Mutex
}
// LoadConfig loads configuration from JSON file
func LoadConfig(filename string) (*StressConfig, error) {
file, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("error opening config file: %v", err)
}
defer file.Close()
config := &StressConfig{}
decoder := json.NewDecoder(file)
if err := decoder.Decode(config); err != nil {
return nil, fmt.Errorf("error parsing config: %v", err)
}
return config, nil
}
// CreateDefaultConfig creates a default configuration file
func CreateDefaultConfig(filename string) error {
config := &StressConfig{
Server: struct {
Host string `json:"host"`
Port int `json:"port"`
}{
Host: "localhost",
Port: 6667,
},
Test: struct {
MaxClients int `json:"max_clients"`
ConnectDelay int `json:"connect_delay_ms"`
ActionInterval int `json:"action_interval_ms"`
TestDuration int `json:"test_duration_seconds"`
RandomSeed int `json:"random_seed"`
}{
MaxClients: 300,
ConnectDelay: 100,
ActionInterval: 2000,
TestDuration: 180,
RandomSeed: 42,
},
Behavior: struct {
JoinChannels bool `json:"join_channels"`
SendMessages bool `json:"send_messages"`
ChangeNicks bool `json:"change_nicks"`
UseNewCommands bool `json:"use_new_commands"`
RandomQuit bool `json:"random_quit"`
MessageRate float64 `json:"message_rate"`
ChannelJoinRate float64 `json:"channel_join_rate"`
CommandRate float64 `json:"command_rate"`
}{
JoinChannels: true,
SendMessages: true,
ChangeNicks: true,
UseNewCommands: true,
RandomQuit: false,
MessageRate: 0.4,
ChannelJoinRate: 0.3,
CommandRate: 0.2,
},
Channels: []string{
"#test", "#stress", "#general", "#random", "#chaos",
"#lobby", "#gaming", "#tech", "#chat", "#help",
},
Messages: []string{
"Hello everyone!",
"This is a stress test message",
"How is everyone doing?",
"Testing the server stability",
"Random message from client",
"IRC is awesome!",
"TechIRCd rocks!",
"Can you see this message?",
"Stress testing in progress",
"Everything working fine here",
"Testing new IRC commands",
"Server performance looks good",
},
Commands: []string{
"MOTD", "RULES", "MAP", "TIME", "VERSION", "LUSERS",
"WHO #test", "WHOIS testuser", "LIST", "WHOWAS olduser",
},
}
file, err := os.Create(filename)
if err != nil {
return fmt.Errorf("error creating config file: %v", err)
}
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
return encoder.Encode(config)
}
// NewStressTest creates a new stress test instance
func NewStressTest(config *StressConfig) *StressTest {
return &StressTest{
Config: config,
Clients: make([]*IRCClient, 0, config.Test.MaxClients),
Stats: &TestStats{
StartTime: time.Now(),
},
quit: make(chan bool),
}
}
// Connect establishes connection to IRC server
func (c *IRCClient) Connect(host string, port int) error {
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
if err != nil {
return fmt.Errorf("failed to connect: %v", err)
}
c.Conn = conn
c.Reader = bufio.NewReader(conn)
c.Writer = bufio.NewWriter(conn)
c.Active = true
return nil
}
// Register performs IRC client registration
func (c *IRCClient) Register() error {
commands := []string{
fmt.Sprintf("NICK %s", c.Nick),
fmt.Sprintf("USER %s 0 * :Stress Test Client %d", c.Nick, c.ID),
}
for _, cmd := range commands {
if err := c.SendCommand(cmd); err != nil {
return fmt.Errorf("registration failed: %v", err)
}
}
return nil
}
// SendCommand sends a command to the IRC server
func (c *IRCClient) SendCommand(command string) error {
c.mu.Lock()
defer c.mu.Unlock()
if !c.Active || c.Writer == nil {
return fmt.Errorf("client not active")
}
_, err := c.Writer.WriteString(command + "\r\n")
if err != nil {
return err
}
return c.Writer.Flush()
}
// ReadMessages continuously reads messages from server
func (c *IRCClient) ReadMessages(stats *TestStats) {
defer func() {
c.Active = false
if c.Conn != nil {
c.Conn.Close()
}
}()
for c.Active {
if c.Reader == nil {
break
}
line, err := c.Reader.ReadString('\n')
if err != nil {
stats.mu.Lock()
stats.Errors++
stats.mu.Unlock()
break
}
// Handle PING responses
if len(line) > 4 && line[:4] == "PING" {
pong := "PONG" + line[4:]
c.SendCommand(pong[:len(pong)-2]) // Remove \r\n
}
}
}
// PerformRandomAction performs a random IRC action
func (c *IRCClient) PerformRandomAction(config *StressConfig, stats *TestStats) {
if !c.Active {
return
}
action := rand.Float64()
switch {
case action < config.Behavior.MessageRate && config.Behavior.SendMessages:
c.SendRandomMessage(config, stats)
case action < config.Behavior.MessageRate+config.Behavior.ChannelJoinRate && config.Behavior.JoinChannels:
c.JoinRandomChannel(config, stats)
case action < config.Behavior.MessageRate+config.Behavior.ChannelJoinRate+config.Behavior.CommandRate && config.Behavior.UseNewCommands:
c.SendRandomCommand(config, stats)
case config.Behavior.ChangeNicks && rand.Float64() < 0.05:
c.ChangeNick(stats)
}
}
// SendRandomMessage sends a random message to a random channel
func (c *IRCClient) SendRandomMessage(config *StressConfig, stats *TestStats) {
if len(c.Channels) == 0 {
return
}
channel := c.Channels[rand.Intn(len(c.Channels))]
message := config.Messages[rand.Intn(len(config.Messages))]
cmd := fmt.Sprintf("PRIVMSG %s :%s", channel, message)
if err := c.SendCommand(cmd); err == nil {
stats.mu.Lock()
stats.MessagesSent++
stats.mu.Unlock()
} else {
stats.mu.Lock()
stats.Errors++
stats.mu.Unlock()
}
}
// JoinRandomChannel joins a random channel
func (c *IRCClient) JoinRandomChannel(config *StressConfig, stats *TestStats) {
channel := config.Channels[rand.Intn(len(config.Channels))]
// Check if already in channel
for _, ch := range c.Channels {
if ch == channel {
return
}
}
cmd := fmt.Sprintf("JOIN %s", channel)
if err := c.SendCommand(cmd); err == nil {
c.Channels = append(c.Channels, channel)
stats.mu.Lock()
stats.ChannelsJoined++
stats.mu.Unlock()
} else {
stats.mu.Lock()
stats.Errors++
stats.mu.Unlock()
}
}
// SendRandomCommand sends a random IRC command
func (c *IRCClient) SendRandomCommand(config *StressConfig, stats *TestStats) {
command := config.Commands[rand.Intn(len(config.Commands))]
if err := c.SendCommand(command); err == nil {
stats.mu.Lock()
stats.CommandsSent++
stats.mu.Unlock()
} else {
stats.mu.Lock()
stats.Errors++
stats.mu.Unlock()
}
}
// ChangeNick changes the client's nickname
func (c *IRCClient) ChangeNick(stats *TestStats) {
newNick := fmt.Sprintf("User%d_%d", c.ID, rand.Intn(1000))
cmd := fmt.Sprintf("NICK %s", newNick)
if err := c.SendCommand(cmd); err == nil {
c.Nick = newNick
} else {
stats.mu.Lock()
stats.Errors++
stats.mu.Unlock()
}
}
// CreateClient creates and connects a new IRC client
func (st *StressTest) CreateClient(id int) error {
client := &IRCClient{
ID: id,
Nick: fmt.Sprintf("StressUser%d", id),
Channels: make([]string, 0),
}
// Connect to server
if err := client.Connect(st.Config.Server.Host, st.Config.Server.Port); err != nil {
return fmt.Errorf("client %d connection failed: %v", id, err)
}
// Register with IRC server
if err := client.Register(); err != nil {
client.Conn.Close()
return fmt.Errorf("client %d registration failed: %v", id, err)
}
st.Clients = append(st.Clients, client)
st.Stats.mu.Lock()
st.Stats.ConnectedClients++
st.Stats.mu.Unlock()
// Start message reader goroutine
go client.ReadMessages(st.Stats)
return nil
}
// RunStressTest executes the complete stress test
func (st *StressTest) RunStressTest() error {
log.Printf("🚀 Starting TechIRCd stress test with %d clients", st.Config.Test.MaxClients)
log.Printf("📡 Target server: %s:%d", st.Config.Server.Host, st.Config.Server.Port)
log.Printf("⏱️ Test duration: %d seconds", st.Config.Test.TestDuration)
// Set random seed
rand.Seed(int64(st.Config.Test.RandomSeed))
// Connect clients gradually
connectDelay := time.Duration(st.Config.Test.ConnectDelay) * time.Millisecond
for i := 0; i < st.Config.Test.MaxClients; i++ {
if err := st.CreateClient(i); err != nil {
log.Printf("❌ Failed to create client %d: %v", i, err)
st.Stats.mu.Lock()
st.Stats.Errors++
st.Stats.mu.Unlock()
continue
}
if i%25 == 0 {
log.Printf("🔗 Connected %d/%d clients", i+1, st.Config.Test.MaxClients)
}
time.Sleep(connectDelay)
}
log.Printf("✅ All clients connected! Starting chaos simulation...")
// Start activity simulation
actionInterval := time.Duration(st.Config.Test.ActionInterval) * time.Millisecond
testDuration := time.Duration(st.Config.Test.TestDuration) * time.Second
actionTicker := time.NewTicker(actionInterval)
defer actionTicker.Stop()
statsTicker := time.NewTicker(15 * time.Second)
defer statsTicker.Stop()
testTimer := time.NewTimer(testDuration)
defer testTimer.Stop()
for {
select {
case <-actionTicker.C:
// Perform random actions for random clients
numActions := rand.Intn(len(st.Clients)/3) + 1
for i := 0; i < numActions; i++ {
if len(st.Clients) > 0 {
clientIndex := rand.Intn(len(st.Clients))
go st.Clients[clientIndex].PerformRandomAction(st.Config, st.Stats)
}
}
case <-statsTicker.C:
st.PrintStats()
case <-testTimer.C:
log.Println("⏰ Test duration completed. Shutting down...")
st.Shutdown()
return nil
case <-st.quit:
log.Println("🛑 Test interrupted. Shutting down...")
st.Shutdown()
return nil
}
}
}
// PrintStats prints current test statistics
func (st *StressTest) PrintStats() {
st.Stats.mu.Lock()
defer st.Stats.mu.Unlock()
elapsed := time.Since(st.Stats.StartTime)
log.Printf("📊 === STRESS TEST STATS ===")
log.Printf("⏱️ Runtime: %v", elapsed.Round(time.Second))
log.Printf("👥 Connected Clients: %d", st.Stats.ConnectedClients)
log.Printf("💬 Messages Sent: %d", st.Stats.MessagesSent)
log.Printf("⚡ Commands Sent: %d", st.Stats.CommandsSent)
log.Printf("🏠 Channels Joined: %d", st.Stats.ChannelsJoined)
log.Printf("❌ Errors: %d", st.Stats.Errors)
if elapsed.Seconds() > 0 {
log.Printf("📈 Messages/sec: %.2f", float64(st.Stats.MessagesSent)/elapsed.Seconds())
log.Printf("📈 Commands/sec: %.2f", float64(st.Stats.CommandsSent)/elapsed.Seconds())
}
log.Printf("============================")
}
// Shutdown gracefully shuts down all clients
func (st *StressTest) Shutdown() {
log.Println("🔌 Shutting down all clients...")
for i, client := range st.Clients {
if client.Active {
client.SendCommand("QUIT :Stress test completed")
client.Active = false
if client.Conn != nil {
client.Conn.Close()
}
}
if i%50 == 0 && i > 0 {
log.Printf("🔌 Disconnected %d/%d clients", i+1, len(st.Clients))
}
}
st.PrintStats()
log.Println("🎉 Stress test completed successfully!")
}
func main() {
configFile := "stress_test_config.json"
log.Println("🎯 TechIRCd Go Stress Tester v1.0")
log.Println("==================================")
// Check if config file exists, create default if not
if _, err := os.Stat(configFile); os.IsNotExist(err) {
log.Printf("📝 Config file %s not found. Creating default...", configFile)
if err := CreateDefaultConfig(configFile); err != nil {
log.Fatalf("❌ Failed to create default config: %v", err)
}
log.Printf("✅ Default config created at %s", configFile)
log.Println("📖 Please review and modify the config as needed, then run again.")
return
}
// Load configuration
config, err := LoadConfig(configFile)
if err != nil {
log.Fatalf("❌ Failed to load config: %v", err)
}
log.Printf("📋 Loaded config: %d clients, %d second test", config.Test.MaxClients, config.Test.TestDuration)
// Create and run stress test
stressTest := NewStressTest(config)
if err := stressTest.RunStressTest(); err != nil {
log.Fatalf("❌ Stress test failed: %v", err)
}
}

134
validation.go Normal file
View File

@@ -0,0 +1,134 @@
package main
import (
"fmt"
"strings"
)
// ValidateConfig performs comprehensive validation of the server configuration
func (c *Config) Validate() error {
// Validate server settings
if c.Server.Name == "" {
return fmt.Errorf("server name cannot be empty")
}
if c.Server.Network == "" {
return fmt.Errorf("network name cannot be empty")
}
if c.Server.Listen.Host == "" {
c.Server.Listen.Host = "localhost" // Default fallback
}
if c.Server.Listen.Port <= 0 || c.Server.Listen.Port > 65535 {
return fmt.Errorf("invalid port number: %d", c.Server.Listen.Port)
}
if c.Server.Listen.EnableSSL && (c.Server.Listen.SSLPort <= 0 || c.Server.Listen.SSLPort > 65535) {
return fmt.Errorf("invalid SSL port number: %d", c.Server.Listen.SSLPort)
}
// Validate limits
if c.Limits.MaxClients <= 0 {
c.Limits.MaxClients = 1000 // Default
}
if c.Limits.MaxChannels <= 0 {
c.Limits.MaxChannels = 100 // Default
}
if c.Limits.MaxNickLength <= 0 || c.Limits.MaxNickLength > 50 {
c.Limits.MaxNickLength = 30 // Default
}
if c.Limits.PingTimeout <= 0 {
c.Limits.PingTimeout = 300 // Default 5 minutes
}
if c.Limits.FloodLines <= 0 {
c.Limits.FloodLines = 10 // Default
}
if c.Limits.FloodSeconds <= 0 {
c.Limits.FloodSeconds = 60 // Default
}
// Validate channels
for _, channelName := range c.Channels.AutoJoin {
if !isChannelName(channelName) {
return fmt.Errorf("invalid channel name in auto_join: %s", channelName)
}
}
// Validate default modes
validChannelModes := "mntisp"
for _, mode := range c.Channels.DefaultModes {
if mode != '+' && !strings.ContainsRune(validChannelModes, mode) {
return fmt.Errorf("invalid default channel mode: %c", mode)
}
}
// Validate founder mode
validFounderModes := []string{"q", "a", "o", "h", "v"}
foundValidMode := false
for _, validMode := range validFounderModes {
if c.Channels.FounderMode == validMode {
foundValidMode = true
break
}
}
if !foundValidMode {
return fmt.Errorf("invalid founder_mode '%s', must be one of: q (owner), a (admin), o (operator), h (halfop), v (voice)", c.Channels.FounderMode)
}
// Validate operators
for i, oper := range c.Opers {
if oper.Name == "" {
return fmt.Errorf("operator %d: name cannot be empty", i)
}
if oper.Password == "" {
return fmt.Errorf("operator %s: password cannot be empty", oper.Name)
}
if oper.Host == "" {
return fmt.Errorf("operator %s: host cannot be empty", oper.Name)
}
}
return nil
}
// SanitizeConfig applies safe defaults and sanitizes configuration values
func (c *Config) SanitizeConfig() {
// Ensure reasonable limits
if c.Limits.MaxClients > 10000 {
c.Limits.MaxClients = 10000
}
if c.Limits.MaxChannels > 1000 {
c.Limits.MaxChannels = 1000
}
// Set default founder mode if empty or invalid
if c.Channels.FounderMode == "" {
c.Channels.FounderMode = "o" // Default to operator
}
// Ensure reasonable string lengths
if c.Limits.MaxTopicLength > 2048 {
c.Limits.MaxTopicLength = 2048
}
if c.Limits.MaxKickLength > 2048 {
c.Limits.MaxKickLength = 2048
}
// Ensure MOTD isn't too long
if len(c.MOTD) > 50 {
c.MOTD = c.MOTD[:50]
}
// Sanitize channel modes
if c.Channels.DefaultModes == "" {
c.Channels.DefaultModes = "+nt"
}
}

127
web_interface.go Normal file
View File

@@ -0,0 +1,127 @@
package main
import (
"encoding/json"
"net/http"
"time"
)
// Web administration interface
type WebInterface struct {
Enable bool `json:"enable"`
Port int `json:"port"`
Host string `json:"host"`
SSL struct {
Enable bool `json:"enable"`
CertFile string `json:"cert_file"`
KeyFile string `json:"key_file"`
} `json:"ssl"`
Authentication struct {
Method string `json:"method"` // basic, oauth, jwt
Username string `json:"username"`
Password string `json:"password"`
} `json:"authentication"`
}
// WebServerStats represents statistics for the web interface
type WebServerStats struct {
Uptime time.Duration `json:"uptime"`
ActiveUsers int `json:"active_users"`
ActiveChannels int `json:"active_channels"`
LinkedServers int `json:"linked_servers"`
MessagesPerSec float64 `json:"messages_per_sec"`
MemoryUsage uint64 `json:"memory_usage"`
CPUUsage float64 `json:"cpu_usage"`
}
// WebUserInfo represents user information for the web interface
type WebUserInfo struct {
Nick string `json:"nick"`
User string `json:"user"`
Host string `json:"host"`
Channels int `json:"channels"`
IsOper bool `json:"is_oper"`
ConnTime time.Time `json:"conn_time"`
}
// REST API endpoints
func (w *WebInterface) RegisterRoutes() {
http.HandleFunc("/api/v1/server/stats", w.handleServerStats)
http.HandleFunc("/api/v1/users", w.handleUsers)
http.HandleFunc("/api/v1/channels", w.handleChannels)
http.HandleFunc("/api/v1/operators", w.handleOperators)
http.HandleFunc("/api/v1/config", w.handleConfig)
http.HandleFunc("/api/v1/logs", w.handleLogs)
http.HandleFunc("/api/v1/bans", w.handleBans)
http.HandleFunc("/api/v1/network", w.handleNetwork)
// WebSocket for real-time updates
http.HandleFunc("/ws/live", w.handleWebSocket)
// Static files for web interface
http.Handle("/", http.FileServer(http.Dir("web/static/")))
}
func (w *WebInterface) handleServerStats(rw http.ResponseWriter, r *http.Request) {
// TODO: Get actual server statistics
stats := WebServerStats{
Uptime: time.Duration(0),
ActiveUsers: 0,
ActiveChannels: 0,
LinkedServers: 0,
MessagesPerSec: 0.0,
MemoryUsage: 0,
CPUUsage: 0.0,
}
rw.Header().Set("Content-Type", "application/json")
json.NewEncoder(rw).Encode(stats)
}
func (w *WebInterface) handleUsers(rw http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
// List all users
users := make([]*WebUserInfo, 0)
// TODO: Get actual users from server
json.NewEncoder(rw).Encode(users)
case "POST":
// Send message to user or perform action
http.Error(rw, "Not implemented", http.StatusNotImplemented)
case "DELETE":
// Disconnect user
http.Error(rw, "Not implemented", http.StatusNotImplemented)
}
}
func (w *WebInterface) handleChannels(rw http.ResponseWriter, r *http.Request) {
http.Error(rw, "Not implemented", http.StatusNotImplemented)
}
func (w *WebInterface) handleOperators(rw http.ResponseWriter, r *http.Request) {
http.Error(rw, "Not implemented", http.StatusNotImplemented)
}
func (w *WebInterface) handleConfig(rw http.ResponseWriter, r *http.Request) {
http.Error(rw, "Not implemented", http.StatusNotImplemented)
}
func (w *WebInterface) handleLogs(rw http.ResponseWriter, r *http.Request) {
http.Error(rw, "Not implemented", http.StatusNotImplemented)
}
func (w *WebInterface) handleBans(rw http.ResponseWriter, r *http.Request) {
http.Error(rw, "Not implemented", http.StatusNotImplemented)
}
func (w *WebInterface) handleNetwork(rw http.ResponseWriter, r *http.Request) {
http.Error(rw, "Not implemented", http.StatusNotImplemented)
}
// Real-time dashboard with WebSocket
func (w *WebInterface) handleWebSocket(rw http.ResponseWriter, r *http.Request) {
// TODO: Upgrade to WebSocket and send real-time updates
http.Error(rw, "WebSocket not implemented", http.StatusNotImplemented)
}

180
worker_pool.go Normal file
View File

@@ -0,0 +1,180 @@
package main
import (
"log"
"net"
"runtime"
"sync"
"time"
)
// WorkerPool manages goroutines for handling client connections
type WorkerPool struct {
workers int
jobs chan net.Conn
server *Server
wg sync.WaitGroup
shutdown chan bool
stats *PoolStats
}
type PoolStats struct {
ActiveWorkers int64
ProcessedJobs int64
QueuedJobs int64
ErrorCount int64
mu sync.RWMutex
}
// NewWorkerPool creates a new worker pool
func NewWorkerPool(workers int, server *Server) *WorkerPool {
if workers <= 0 {
workers = runtime.NumCPU() * 4 // Default: 4 workers per CPU core
}
return &WorkerPool{
workers: workers,
jobs: make(chan net.Conn, workers*10), // Buffer for connection queue
server: server,
shutdown: make(chan bool),
stats: &PoolStats{},
}
}
// Start initializes and starts the worker pool
func (wp *WorkerPool) Start() {
log.Printf("Starting worker pool with %d workers", wp.workers)
for i := 0; i < wp.workers; i++ {
wp.wg.Add(1)
go wp.worker(i)
}
// Start stats reporter
go wp.statsReporter()
}
// worker processes incoming connections
func (wp *WorkerPool) worker(id int) {
defer wp.wg.Done()
wp.updateActiveWorkers(1)
defer wp.updateActiveWorkers(-1)
log.Printf("Worker %d started", id)
for {
select {
case conn := <-wp.jobs:
wp.handleConnection(conn, id)
wp.updateProcessedJobs(1)
case <-wp.shutdown:
log.Printf("Worker %d shutting down", id)
return
}
}
}
// handleConnection processes a single client connection
func (wp *WorkerPool) handleConnection(conn net.Conn, workerID int) {
defer func() {
if r := recover(); r != nil {
log.Printf("Worker %d recovered from panic: %v", workerID, r)
wp.updateErrorCount(1)
conn.Close()
}
}()
// Set connection timeouts
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
conn.SetWriteDeadline(time.Now().Add(30 * time.Second))
client := NewClient(conn, wp.server)
wp.server.AddClient(client)
log.Printf("Worker %d handling client from %s", workerID, conn.RemoteAddr())
// Handle client in this worker goroutine
client.Handle()
}
// Submit adds a connection to the worker pool queue
func (wp *WorkerPool) Submit(conn net.Conn) bool {
wp.updateQueuedJobs(1)
select {
case wp.jobs <- conn:
return true
default:
// Queue is full
wp.updateQueuedJobs(-1)
wp.updateErrorCount(1)
log.Printf("Worker pool queue full, rejecting connection from %s", conn.RemoteAddr())
return false
}
}
// Shutdown gracefully stops the worker pool
func (wp *WorkerPool) Shutdown() {
log.Println("Shutting down worker pool...")
close(wp.shutdown)
close(wp.jobs)
// Wait for all workers to finish
wp.wg.Wait()
log.Println("Worker pool shutdown complete")
}
// Stats update methods
func (wp *WorkerPool) updateActiveWorkers(delta int64) {
wp.stats.mu.Lock()
wp.stats.ActiveWorkers += delta
wp.stats.mu.Unlock()
}
func (wp *WorkerPool) updateProcessedJobs(delta int64) {
wp.stats.mu.Lock()
wp.stats.ProcessedJobs += delta
wp.stats.QueuedJobs -= delta
wp.stats.mu.Unlock()
}
func (wp *WorkerPool) updateQueuedJobs(delta int64) {
wp.stats.mu.Lock()
wp.stats.QueuedJobs += delta
wp.stats.mu.Unlock()
}
func (wp *WorkerPool) updateErrorCount(delta int64) {
wp.stats.mu.Lock()
wp.stats.ErrorCount += delta
wp.stats.mu.Unlock()
}
// GetStats returns current pool statistics
func (wp *WorkerPool) GetStats() PoolStats {
wp.stats.mu.RLock()
defer wp.stats.mu.RUnlock()
return *wp.stats
}
// statsReporter periodically logs worker pool statistics
func (wp *WorkerPool) statsReporter() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
stats := wp.GetStats()
log.Printf("Worker Pool Stats - Active: %d, Processed: %d, Queued: %d, Errors: %d",
stats.ActiveWorkers, stats.ProcessedJobs, stats.QueuedJobs, stats.ErrorCount)
case <-wp.shutdown:
return
}
}
}