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
This commit is contained in:
parent
f25f8e4d01
commit
3c698a8bdb
235
cmd/siptest/main.go
Normal file
235
cmd/siptest/main.go
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
// 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]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user