mirror of
https://github.com/lukaszraczylo/lolcathost.git
synced 2026-06-11 00:08:57 +00:00
Initial commit.
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
// Package daemon provides security functions including rate limiting and audit logging.
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// AuditLogPath is the path to the audit log file.
|
||||
AuditLogPath = "/var/log/lolcathost/audit.log"
|
||||
// RateLimit is the maximum requests per minute per PID.
|
||||
RateLimit = 100
|
||||
// RateLimitWindow is the time window for rate limiting.
|
||||
RateLimitWindow = time.Minute
|
||||
)
|
||||
|
||||
// RateLimiter implements per-PID rate limiting.
|
||||
type RateLimiter struct {
|
||||
mu sync.Mutex
|
||||
requests map[int32][]time.Time
|
||||
limit int
|
||||
window time.Duration
|
||||
}
|
||||
|
||||
// NewRateLimiter creates a new rate limiter.
|
||||
func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
|
||||
return &RateLimiter{
|
||||
requests: make(map[int32][]time.Time),
|
||||
limit: limit,
|
||||
window: window,
|
||||
}
|
||||
}
|
||||
|
||||
// Allow checks if a request from the given PID should be allowed.
|
||||
func (r *RateLimiter) Allow(pid int32) bool {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
cutoff := now.Add(-r.window)
|
||||
|
||||
// Get existing requests for this PID
|
||||
reqs := r.requests[pid]
|
||||
|
||||
// Filter out old requests
|
||||
var validReqs []time.Time
|
||||
for _, t := range reqs {
|
||||
if t.After(cutoff) {
|
||||
validReqs = append(validReqs, t)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if under limit
|
||||
if len(validReqs) >= r.limit {
|
||||
r.requests[pid] = validReqs
|
||||
return false
|
||||
}
|
||||
|
||||
// Add new request
|
||||
validReqs = append(validReqs, now)
|
||||
r.requests[pid] = validReqs
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Cleanup removes old entries from the rate limiter.
|
||||
func (r *RateLimiter) Cleanup() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
cutoff := now.Add(-r.window)
|
||||
|
||||
for pid, reqs := range r.requests {
|
||||
var validReqs []time.Time
|
||||
for _, t := range reqs {
|
||||
if t.After(cutoff) {
|
||||
validReqs = append(validReqs, t)
|
||||
}
|
||||
}
|
||||
if len(validReqs) == 0 {
|
||||
delete(r.requests, pid)
|
||||
} else {
|
||||
r.requests[pid] = validReqs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AuditLogger handles audit logging.
|
||||
type AuditLogger struct {
|
||||
mu sync.Mutex
|
||||
file *os.File
|
||||
path string
|
||||
encoder *json.Encoder
|
||||
}
|
||||
|
||||
// AuditEntry represents a single audit log entry.
|
||||
type AuditEntry struct {
|
||||
Timestamp string `json:"timestamp"`
|
||||
UID uint32 `json:"uid"`
|
||||
PID int32 `json:"pid"`
|
||||
Action string `json:"action"`
|
||||
Details any `json:"details,omitempty"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// NewAuditLogger creates a new audit logger.
|
||||
func NewAuditLogger(path string) (*AuditLogger, error) {
|
||||
// Ensure directory exists
|
||||
dir := path[:len(path)-len("/audit.log")]
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create log directory: %w", err)
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open audit log: %w", err)
|
||||
}
|
||||
|
||||
return &AuditLogger{
|
||||
file: file,
|
||||
path: path,
|
||||
encoder: json.NewEncoder(file),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Log writes an audit entry.
|
||||
func (a *AuditLogger) Log(uid uint32, pid int32, action string, details any, success bool, errMsg string) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
entry := AuditEntry{
|
||||
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
||||
UID: uid,
|
||||
PID: pid,
|
||||
Action: action,
|
||||
Details: details,
|
||||
Success: success,
|
||||
Error: errMsg,
|
||||
}
|
||||
|
||||
// Ignore encoding errors - audit logging should not fail the operation
|
||||
_ = a.encoder.Encode(entry)
|
||||
}
|
||||
|
||||
// Close closes the audit logger.
|
||||
func (a *AuditLogger) Close() error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
if a.file != nil {
|
||||
err := a.file.Close()
|
||||
a.file = nil // Prevent double close
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PeerCredentials holds the credentials of a connected peer.
|
||||
type PeerCredentials struct {
|
||||
UID uint32
|
||||
GID uint32
|
||||
PID int32
|
||||
}
|
||||
|
||||
// isUserInGroup checks if a user (by UID) is a member of a group (by GID).
|
||||
// This checks supplementary groups, not just the primary GID.
|
||||
func isUserInGroup(uid uint32, targetGID uint32) bool {
|
||||
// Look up user by UID
|
||||
u, err := user.LookupId(fmt.Sprintf("%d", uid))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Get user's group IDs
|
||||
groupIDs, err := u.GroupIds()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if target GID is in the list
|
||||
targetGIDStr := fmt.Sprintf("%d", targetGID)
|
||||
for _, gid := range groupIDs {
|
||||
if gid == targetGIDStr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user