Maksim Totmin 3c698a8bdb feat: SIP load testing tool
- SIP UAC mode (call FreeSWITCH, rate/configurable CPS)
- SIP UAS mode (respond 200 OK as PBX node)
- Configurable: rate, concurrency, total calls, caller/dest numbers
2026-06-25 16:59:02 +07:00

236 lines
6.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// SIP тестер для e2e проверки pulse-lets-go + FreeSWITCH.
// Два режима:
// uas — отвечает 200 OK на SIP INVITE, симулирует PBX-ноду
// uac — шлёт SIP INVITE в FreeSWITCH, симулирует оператора
//
// Примеры:
// ./siptest -mode uas (PBX-нода на порту 5090)
// ./siptest -mode uac -r 10 -l 100 (оператор, 10 CPS, до 100 одновременных)
package main
import (
"crypto/rand"
"encoding/hex"
"flag"
"fmt"
"log"
"net"
"strings"
"sync"
"sync/atomic"
"time"
)
const (
uasPort = 5090 // порт PBX-ноды (принимает bridged вызовы)
uacPort = 5061 // порт оператора (шлёт INVITE в FS)
fsPort = 5080 // порт FreeSWITCH (external profile — для транков)
)
// Статистика
var (
callsSent atomic.Int64
callsOK atomic.Int64
callsFailed atomic.Int64
)
func main() {
mode := flag.String("mode", "uas", "uas | uac")
rate := flag.Int("r", 10, "CPS (для uac)")
limit := flag.Int("l", 100, "максимум одновременных (для uac)")
maxCalls := flag.Int("m", 1000, "всего вызовов (для uac)")
host := flag.String("host", "127.0.0.1", "FS host/IP")
dest := flag.String("dest", "88005553535", "dest номер")
caller := flag.String("caller", "74951234567", "caller_id")
flag.Parse()
log.Printf("[siptest] режим=%s", *mode)
switch *mode {
case "uas":
runUAS()
case "uac":
runUAC(*rate, *limit, *maxCalls, *dest, *caller, *host)
default:
log.Fatalf("неизвестный режим: %s", *mode)
}
}
// ── UAS: PBX-нода (отвечает 200 OK на INVITE) ────────────────
func runUAS() {
addr := fmt.Sprintf("127.0.0.1:%d", uasPort)
conn, err := net.ListenPacket("udp", addr)
if err != nil {
log.Fatalf("UAS listen %s: %v", addr, err)
}
defer conn.Close()
log.Printf("[uas] PBX-нода слушает %s", addr)
buf := make([]byte, 4096)
for {
n, remoteAddr, err := conn.ReadFrom(buf)
if err != nil {
log.Printf("[uas] ошибка чтения: %v", err)
continue
}
msg := string(buf[:n])
if strings.HasPrefix(msg, "INVITE") || strings.Contains(msg, "INVITE") {
response := buildSIPResponse(msg, addr)
time.Sleep(50 * time.Millisecond) // имитация обработки вызова
if _, err := conn.WriteTo([]byte(response), remoteAddr); err != nil {
log.Printf("[uas] ошибка ответа: %v", err)
}
log.Printf("[uas] INVITE отвечен: %v", remoteAddr)
}
// ACK и BYE игнорируем
if strings.Contains(msg, "ACK") || strings.Contains(msg, "BYE") {
continue
}
}
}
func buildSIPResponse(invite, localAddr string) string {
via := extractHeader(invite, "Via:")
from := extractHeader(invite, "From:")
to := extractHeader(invite, "To:")
callID := extractHeader(invite, "Call-ID:")
cseq := extractHeader(invite, "CSeq:")
// Добавляем tag к To если его нет
to = strings.TrimSpace(to)
if !strings.Contains(to, ";tag=") {
to += ";tag=uas-" + randomHex(4)
}
return fmt.Sprintf("SIP/2.0 200 OK\r\n"+
"%s\r\n"+
"%s\r\n"+
"%s\r\n"+
"%s\r\n"+
"%s\r\n"+
"Contact: <sip:pbx@%s>\r\n"+
"Content-Length: 0\r\n"+
"\r\n",
via, from, to, callID, cseq, localAddr)
}
func extractHeader(msg, prefix string) string {
lines := strings.Split(msg, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
line = strings.TrimRight(line, "\r")
if strings.HasPrefix(line, prefix) {
return line
}
}
return prefix
}
// ── UAC: Оператор (шлёт INVITE в FS) ─────────────────────────
func runUAC(rate, limit, maxCalls int, dest, caller, host string) {
fsAddr := fmt.Sprintf("%s:%d", host, fsPort)
srcAddr := fmt.Sprintf("127.0.0.1:%d", uacPort)
localAddr, err := net.ResolveUDPAddr("udp", srcAddr)
if err != nil {
log.Fatalf("resolve %s: %v", srcAddr, err)
}
remoteAddr, err := net.ResolveUDPAddr("udp", fsAddr)
if err != nil {
log.Fatalf("resolve %s: %v", fsAddr, err)
}
conn, err := net.ListenUDP("udp", localAddr)
if err != nil {
log.Fatalf("listen %s: %v", srcAddr, err)
}
defer conn.Close()
log.Printf("[uac] оператор %s → FS %s (rate=%d/s, limit=%d, total=%d)", srcAddr, fsAddr, rate, limit, maxCalls)
sem := make(chan struct{}, limit) // ограничение одновременных
var wg sync.WaitGroup
ticker := time.NewTicker(time.Second / time.Duration(rate))
defer ticker.Stop()
// Читаем ответы от FS
go func() {
buf := make([]byte, 4096)
for {
n, _, err := conn.ReadFrom(buf)
if err != nil {
return
}
msg := string(buf[:n])
if strings.Contains(msg, "200 OK") {
callsOK.Add(1)
}
}
}()
startTime := time.Now()
callNum := 0
for callNum < maxCalls {
select {
case <-ticker.C:
sem <- struct{}{} // занимаем слот
wg.Add(1)
callNum++
go func(num int) {
defer wg.Done()
defer func() { <-sem }() // освобождаем слот
invite := buildSIPInvite(dest, caller, num, srcAddr, fsAddr)
_, err := conn.WriteTo([]byte(invite), remoteAddr)
callsSent.Add(1)
if err != nil {
callsFailed.Add(1)
return
}
}(callNum)
}
}
wg.Wait()
elapsed := time.Since(startTime)
totalCalls := callsSent.Load()
okCalls := callsOK.Load()
log.Printf("[uac] ================ ИТОГИ ================")
log.Printf(" Всего вызовов: %d", totalCalls)
log.Printf(" Успешно (200 OK): %d", okCalls)
log.Printf(" CPS (факт): %.1f", float64(totalCalls)/elapsed.Seconds())
log.Printf(" Длительность: %v", elapsed.Round(time.Millisecond))
}
func buildSIPInvite(dest, caller string, callNum int, srcAddr, fsAddr string) string {
callID := fmt.Sprintf("call-%d-%s@%s", callNum, randomHex(4), "127.0.0.1")
branch := "z9hG4bK-" + randomHex(8)
return fmt.Sprintf("INVITE sip:%s@%s SIP/2.0\r\n"+
"Via: SIP/2.0/UDP %s;branch=%s\r\n"+
"From: <sip:%s@127.0.0.1>;tag=caller-%d\r\n"+
"To: <sip:%s@127.0.0.1>\r\n"+
"Call-ID: %s\r\n"+
"CSeq: 1 INVITE\r\n"+
"Contact: <sip:%s@%s>\r\n"+
"Content-Length: 0\r\n"+
"\r\n",
dest, fsAddr, srcAddr, branch, caller, callNum, dest, callID, caller, srcAddr)
}
func randomHex(n int) string {
b := make([]byte, n)
rand.Read(b)
return hex.EncodeToString(b)[:n]
}