// 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 ( "flag" "fmt" "log" "net" "strings" "sync" "sync/atomic" "time" "github.com/pulse-lets-go/internal/util" ) 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-" + util.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: \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, util.RandomHex(4), "127.0.0.1") branch := "z9hG4bK-" + util.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: ;tag=caller-%d\r\n"+ "To: \r\n"+ "Call-ID: %s\r\n"+ "CSeq: 1 INVITE\r\n"+ "Contact: \r\n"+ "Content-Length: 0\r\n"+ "\r\n", dest, fsAddr, srcAddr, branch, caller, callNum, dest, callID, caller, srcAddr) }