This document describes the architecture and internal design of Defenra Agent.
- Overview
- Component Architecture
- Data Flow
- Configuration Management
- DNS Server
- HTTP/HTTPS Proxy
- Lua WAF
- TCP/UDP Proxy
- Performance
- Security
Defenra Agent is a high-performance, multi-protocol proxy and DNS server written in Go. It acts as an edge node in a distributed network, providing:
- GeoDNS - Geographic DNS routing
- Reverse Proxy - HTTP/HTTPS traffic forwarding
- WAF - Web Application Firewall with Lua
- Port Forwarding - TCP/UDP proxy
┌─────────────────────────────────────────────────────────────┐
│ Defenra Agent (GoLang) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ DNS Server │ │ HTTP Proxy │ │ TCP/UDP Proxy│ │
│ │ (Port 53) │ │ (80/443) │ │ (custom) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └──────────────────┴──────────────────┘ │
│ │ │
│ ┌────────▼─────────┐ │
│ │ Config Manager │ │
│ │ (Poll Core API) │ │
│ └────────┬─────────┘ │
│ │ │
│ ┌────────▼─────────┐ │
│ │ GeoIP Service │ │
│ │ Lua WAF VM │ │
│ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
▲
│ HTTPS Poll (every 60s)
│
┌─────────▼──────────┐
│ Defenra Core │
│ (Node.js API) │
└────────────────────┘
Responsibilities:
- Initialize all components
- Load environment variables
- Start all services in goroutines
- Handle graceful shutdown
Key Functions:
func main()
func getEnvInt(key string, defaultVal int) intResponsibilities:
- Poll configuration from Core API
- Store and manage configuration
- Provide thread-safe access to config
- Track statistics
Key Components:
ConfigManager- Main managerConfig- Configuration structureDomain- Domain configurationProxy- Proxy configuration
Thread Safety:
- Uses
sync.RWMutexfor concurrent access - Atomic updates to prevent inconsistency
Responsibilities:
- Handle DNS queries (UDP/TCP)
- Resolve A, AAAA, CNAME, MX, TXT records
- Perform geographic routing (GeoDNS)
- Cache DNS responses
- Track query statistics
Key Components:
DNSServer- Main DNS serverDNSCache- In-memory cacheGeoIPService- Geographic IP lookup
Performance:
- Uses
github.com/miekg/dnslibrary - In-memory caching with TTL
- Concurrent query handling
- Atomic statistics counters
Responsibilities:
- Forward HTTP/HTTPS requests
- Terminate SSL connections
- Execute WAF rules
- Add proxy headers
- Track request statistics
Key Components:
HTTPProxyServer- HTTP proxyHTTPSProxyServer- HTTPS proxy with SSLProxyManager- TCP/UDP proxy managerTCPProxy- TCP proxyUDPProxy- UDP proxy
Features:
- SNI support for multiple domains
- Dynamic certificate loading
- Connection pooling
- Header manipulation
Responsibilities:
- Execute Lua scripts for request filtering
- Provide nginx-like API
- Shared memory for rate limiting
- Thread-safe script execution
Key Components:
LuaWAF- WAF engineWAFResponse- Response structure
Performance:
- Uses
github.com/yuin/gopher-lua - Lua state pooling
- Sandbox environment
- Timeout protection
Responsibilities:
- Provide health status endpoint
- Expose runtime statistics
- Monitor agent status
Endpoints:
/health- Basic health check/stats- Detailed statistics
1. Timer Tick (every 60s)
│
▼
2. ConfigManager.poll()
│
▼
3. HTTP POST to Core API
│
├─ Success ─────────────────┐
│ │
▼ ▼
4. Parse JSON Response Log Error
│
▼
5. ConfigManager.updateConfig()
│
├─ Lock mutex
├─ Update domains
├─ Update proxies
└─ Unlock mutex
│
▼
6. Components read new config
1. Client sends DNS query
│
▼
2. DNSServer.handleDNSRequest()
│
├─ Extract query details
├─ Extract client IP
└─ Get domain config
│
├─ Domain not found ───> NXDOMAIN
│
▼
3. Check if GeoDNS enabled
│
├─ Yes ─────────────────────┐
│ │
▼ ▼
4. Regular DNS Query 5. GeoDNS Query
│ │
├─ Find record ├─ Get client location (GeoIP)
├─ Build answer ├─ Find best agent IP
└─ Return response └─ Return agent IP
1. Client sends HTTP request
│
▼
2. HTTPProxyServer.handleRequest()
│
├─ Extract host
├─ Get domain config
└─ Check proxy enabled
│
├─ Not enabled ───> 403 Forbidden
│
▼
3. Execute Lua WAF (if configured)
│
├─ Blocked ───> Return error
│
▼
4. Find backend target
│
├─ Not found ───> 502 Bad Gateway
│
▼
5. Proxy request to backend
│
├─ Add X-Forwarded-* headers
├─ Forward request
├─ Receive response
└─ Return to client
1. Client initiates TLS handshake
│
▼
2. GetCertificate callback
│
├─ Extract SNI hostname
├─ Get domain config
└─ Load certificate
│
├─ No cert ───> TLS error
│
▼
3. TLS handshake complete
│
▼
4. Same as HTTP flow (steps 2-5)
The agent polls configuration from Core API at regular intervals:
type PollRequest struct {
AgentID string `json:"agentId"`
AgentKey string `json:"agentKey"`
}
type PollResponse struct {
Success bool `json:"success"`
Domains []Domain `json:"domains"`
Proxies []Proxy `json:"proxies"`
}Polling Interval: 60 seconds (configurable)
Retry Logic:
- On failure, log error and continue
- Next poll in 60 seconds
- No exponential backoff (keeps trying)
type Config struct {
Domains []Domain
Proxies []Proxy
LastUpdate time.Time
mu sync.RWMutex
}
type Domain struct {
Domain string
DNSRecords []DNSRecord
GeoDNSMap map[string]string // location -> IP
HTTPProxy HTTPProxy
SSL SSL
LuaCode string
}
type Proxy struct {
Name string
Protocol string // "tcp" or "udp"
ListenPort int
TargetHost string
TargetPort int
}All configuration access is protected by sync.RWMutex:
- Read operations use
RLock()- Multiple readers allowed - Write operations use
Lock()- Exclusive access
func (cm *ConfigManager) GetDomain(domain string) *Domain {
cm.mu.RLock()
defer cm.mu.RUnlock()
// ... search domain ...
}
func (cm *ConfigManager) updateConfig(resp PollResponse) {
cm.mu.Lock()
defer cm.mu.Unlock()
// ... update config ...
}Supported DNS record types:
- A - IPv4 address
- AAAA - IPv6 address
- CNAME - Canonical name
- MX - Mail exchange
- TXT - Text record
GeoDNS provides different responses based on client location:
func (s *DNSServer) handleGeoDNSQuery(w dns.ResponseWriter, r *dns.Msg,
domainConfig *config.Domain, clientIP string) {
// 1. Determine client location
clientLocation := s.geoIP.GetLocation(clientIP)
// 2. Find best agent IP for this location
agentIP := findBestAgentIP(domainConfig.GeoDNSMap, clientLocation)
// 3. Return agent IP
msg.Answer = append(msg.Answer, &dns.A{
Hdr: dns.RR_Header{
Name: r.Question[0].Name,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: 60, // Low TTL for dynamic routing
},
A: net.ParseIP(agentIP),
})
}Location Mapping:
Client Location → Agent Location → Agent IP
cn (China) → asia → 103.xxx.xxx.xxx
us (USA) → us → 45.xxx.xxx.xxx
de (Germany) → europe → 51.91.242.9
(unknown) → default → 51.91.242.9
Uses MaxMind GeoLite2-City database:
db, err := geoip2.Open("GeoLite2-City.mmdb")
record, err := db.City(net.ParseIP(clientIP))
countryCode := record.Country.IsoCode // "US", "CN", etc.
continent := record.Continent.Code // "NA", "AS", etc.Caching:
- IP → Location mapping is cached in memory
- Reduces database lookups
- Improves performance
Dynamic certificate loading using SNI:
tlsConfig := &tls.Config{
GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
domainConfig := configMgr.GetDomain(hello.ServerName)
cert, err := tls.X509KeyPair(
[]byte(domainConfig.SSL.Certificate),
[]byte(domainConfig.SSL.PrivateKey),
)
return &cert, nil
},
}Features:
- Multiple domains on single IP
- Dynamic cert reload (no restart needed)
- TLS 1.2+ only
- HTTP/2 support
Forwards requests to backend servers:
proxyReq, _ := http.NewRequest(r.Method, targetURL, r.Body)
// Copy headers
for key, values := range r.Header {
proxyReq.Header.Add(key, values[0])
}
// Add proxy headers
proxyReq.Header.Set("X-Forwarded-For", clientIP)
proxyReq.Header.Set("X-Forwarded-Proto", "https")
proxyReq.Header.Set("X-Real-IP", clientIP)
// Execute request
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(proxyReq)
// Forward response
for key, values := range resp.Header {
w.Header().Add(key, values[0])
}
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)Provides familiar API for Nginx users:
-- Request variables
ngx.var.remote_addr -- Client IP
ngx.var.uri -- Request URI
ngx.var.host -- Host header
-- Block request
ngx.exit(403)
-- Shared memory (rate limiting)
local count = ngx.shared.cache:get(key)
ngx.shared.cache:set(key, value, ttl)
ngx.shared.cache:incr(key, delta, init, ttl)
-- Response headers
ngx.header["X-Custom"] = "value"func (w *LuaWAF) Execute(luaCode string, r *http.Request) (bool, WAFResponse) {
// 1. Get Lua state from pool
L := w.pool.Get().(*lua.LState)
defer w.pool.Put(L)
// 2. Inject request context
requestTable := L.NewTable()
L.SetField(requestTable, "method", lua.LString(r.Method))
L.SetGlobal("request", requestTable)
// 3. Setup nginx API
w.setupNginxAPI(L)
// 4. Execute Lua code
err := L.DoString(luaCode)
// 5. Check if blocked
blocked := L.GetGlobal("_blocked")
if blocked != lua.LNil {
return true, WAFResponse{Blocked: true, StatusCode: 403}
}
return false, WAFResponse{}
}- State Pooling - Reuse Lua states
- Sandbox - Limited functionality for security
- Timeout - Prevent long-running scripts
- Shared Memory - In-memory cache for rate limiting
Bidirectional forwarding:
func (p *TCPProxy) handleConnection(clientConn net.Conn) {
targetConn, _ := net.Dial("tcp", targetAddr)
// Client → Backend
go io.Copy(targetConn, clientConn)
// Backend → Client
io.Copy(clientConn, targetConn)
}Stateless packet forwarding:
func handleUDPPacket(conn *net.UDPConn, clientAddr *net.UDPAddr,
data []byte, config ProxyConfig) {
// Forward to backend
targetConn, _ := net.DialUDP("udp", nil, targetAddr)
targetConn.Write(data)
// Wait for response
targetConn.SetReadDeadline(time.Now().Add(5 * time.Second))
n, _ := targetConn.Read(responseBuffer)
// Send back to client
conn.WriteToUDP(responseBuffer[:n], clientAddr)
}DNS Server:
- 10,000+ QPS per agent
- < 10ms average latency
- < 100MB memory usage
HTTP Proxy:
- 5,000+ RPS per agent
- < 20ms average latency
- < 200MB memory usage
Lua WAF:
- < 5ms overhead per request
- State pooling for efficiency
- Goroutines - Concurrent request handling
- Connection Pooling - Reuse HTTP connections
- Caching - DNS and GeoIP caching
- Atomic Operations - Lock-free statistics
- Read-Write Locks - Multiple concurrent readers
- Max Goroutines: Unlimited (OS limited)
- Max Memory: 2GB (configurable)
- Max Connections: 65535 (OS limit)
- Max File Descriptors: 65536
- DNS queries validated
- HTTP headers sanitized
- Lua scripts sandboxed
- Runs as non-root user (with capabilities)
- Limited file system access
- No shell access
- TLS 1.2+ only
- Strong cipher suites
- Certificate validation
- No file system access
- No network access
- No OS commands
- CPU/memory limits
- Prometheus Metrics - Better observability
- Hot Reload - Config reload without restart
- Redis Backend - Distributed cache
- HTTP/3 Support - QUIC protocol
- Auto SSL - Let's Encrypt integration
- Rate Limiting - Built-in rate limiter
- DDoS Protection - Layer 7 protection