Skip to content

Commit 07eb503

Browse files
committed
feat: CLI SIP trunk identity (-sip_from, -pai, -provider, extra headers)
- Engine ExtraKeywords trunk_from/trunk_pai/trunk_provider/trunk_extra - Built-in uac and invite_media insert optional headers after Via - Run profile JSON: sip_from, sip_pai, sip_provider, sip_extra_headers - Shell readable aliases (caller_id, pai, provider_token); docs table
1 parent e9538e1 commit 07eb503

11 files changed

Lines changed: 228 additions & 76 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ The current MVP implements:
2525
- TLS transports `l1` and `ln` (UAC or UAS; same codes as for TCP, plus TLS files as needed)
2626
- Global and per-user variable scopes
2727
- SIPp-style auth credentials via `-au` / `-ap` for challenged `401` / `407` request retries, inline `[authentication username=... password=...]`, and server-side `verifyauth`
28+
- Optional SIP identity for built-in UAC / `invite_media`: `-sip_from` (From before `;tag=`), `-sip_pai`, `-sip_provider`, repeatable `-sip_extra_header` (see `docs/compatibility.md` `[trunk_*]` keywords)
2829
- Concurrent call generation with rate limiting
2930
- Interactive terminal UI via `gossipper tui` / `gossipper -interactive` for launch presets and live runtime control
3031
- Basic statistics and JSON summary export (`-summary_json`), optional standalone **HTML** report (`-summary_html` or `gossipper report-html -in … -out …`; see `docs/summary-json.md`)

docs/compatibility.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ into no-ops or empty strings.
117117
| `[$n]` / `[$name]` | supported | Action and string variables |
118118
| `[dynamic_id]` | supported | Atomic per-message counter with `INT32` wraparound to mirror SIPp; supports `+/-offset` |
119119
| `[routes]` | supported | When a `recv` command uses `rrs="true"`, captured `Record-Route` headers are replayed as `Route` headers (reverse order) in subsequent rendered messages |
120+
| `[trunk_from]` | supported | Built-in UAC / `invite_media`: value before `;tag=` in `From`; default `gossip <sip:gossip@local_bind:port>`; CLI `-sip_from` |
121+
| `[trunk_pai]` | supported | Optional full header line `P-Asserted-Identity: …` plus CRLF; empty when `-sip_pai` unset |
122+
| `[trunk_provider]` | supported | Optional `X-provider: …` plus CRLF; empty when `-sip_provider` unset |
123+
| `[trunk_extra]` | supported | Optional extra headers after `Via` (each line ends with CRLF); repeatable CLI `-sip_extra_header` |
120124
| `[clock_tick]` | supported | Milliseconds elapsed since engine start; supports `+/-offset` |
121125
| `[sipp_version]` | supported | Renders runtime version string (defaults to `Gossipper` when not explicitly provided) |
122126
| `[tdmmap]` | partial | Stub: renders `0.0.0/0`; `-tdmmap` CLI flag is not parsed and per-call slot allocation is deferred |

internal/cli/config.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,12 @@ type Config struct {
125125

126126
// PCAPLinkLayer selects PCAP datalink decoding for play_pcap_* (-pcap-link).
127127
PCAPLinkLayer string
128+
129+
// SipFrom / SipPAI / SipProvider / SipExtraHeaders drive [trunk_*] keywords in built-in scenarios (see docs/compatibility.md).
130+
SipFrom string
131+
SipPAI string
132+
SipProvider string
133+
SipExtraHeaders []string
128134
}
129135

130136
func DefaultConfig() Config {
@@ -215,6 +221,13 @@ func Parse(args []string) (Config, error) {
215221
fs.IntVar(&cfg.IPField, "ipfield", cfg.IPField, "alias for -ip_field (SIPp-compatible)")
216222
fs.StringVar(&cfg.AuthUsername, "au", cfg.AuthUsername, "authorization username for authentication challenges")
217223
fs.StringVar(&cfg.AuthPassword, "ap", cfg.AuthPassword, "authorization password for authentication challenges")
224+
fs.StringVar(&cfg.SipFrom, "sip_from", cfg.SipFrom, "SIP From value before ;tag= in built-in UAC scenarios (name-addr or URI); empty = gossip <sip:gossip@local_ip:local_port>")
225+
fs.StringVar(&cfg.SipPAI, "sip_pai", cfg.SipPAI, "P-Asserted-Identity value only (no header name); empty omits the header")
226+
fs.StringVar(&cfg.SipProvider, "sip_provider", cfg.SipProvider, "sets X-provider to this token; empty omits")
227+
fs.Func("sip_extra_header", "repeatable: one extra SIP header line \"Name: value\" after Via on first in-dialog requests in built-in UAC scenarios", func(s string) error {
228+
cfg.SipExtraHeaders = append(cfg.SipExtraHeaders, strings.TrimSpace(s))
229+
return nil
230+
})
218231
fs.Float64Var(&cfg.Rate, "r", cfg.Rate, "calls per second")
219232
fs.Float64Var(&cfg.RateScale, "rate_scale", cfg.RateScale, "interactive rate control step scale (SIPp-compatible)")
220233
fs.Float64Var(&cfg.RateIncrease, "rate_increase", 0, "change target cps by this amount every -rate_interval milliseconds")

internal/cli/run_profile.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ type runSpec struct {
4646
SendMediaReport *bool `json:"send_media_report,omitempty"`
4747
SummaryJSON *string `json:"summary_json,omitempty"`
4848
SummaryHTML *string `json:"summary_html,omitempty"`
49+
SipFrom *string `json:"sip_from,omitempty"`
50+
SipPAI *string `json:"sip_pai,omitempty"`
51+
SipProvider *string `json:"sip_provider,omitempty"`
52+
SipExtraHeaders []string `json:"sip_extra_headers,omitempty"`
4953
TraceMessages *bool `json:"trace_msg,omitempty"`
5054
StatPrintPeriod *string `json:"stat_period,omitempty"`
5155
InjectionFile *string `json:"injection_file,omitempty"`
@@ -221,6 +225,18 @@ func applyRunSpec(cfg *Config, spec *runSpec, configDir string) error {
221225
if spec.SummaryHTML != nil {
222226
cfg.SummaryHTML = strings.TrimSpace(*spec.SummaryHTML)
223227
}
228+
if spec.SipFrom != nil {
229+
cfg.SipFrom = strings.TrimSpace(*spec.SipFrom)
230+
}
231+
if spec.SipPAI != nil {
232+
cfg.SipPAI = strings.TrimSpace(*spec.SipPAI)
233+
}
234+
if spec.SipProvider != nil {
235+
cfg.SipProvider = strings.TrimSpace(*spec.SipProvider)
236+
}
237+
if len(spec.SipExtraHeaders) > 0 {
238+
cfg.SipExtraHeaders = append([]string(nil), spec.SipExtraHeaders...)
239+
}
224240
if spec.TraceMessages != nil {
225241
cfg.TraceMessages = *spec.TraceMessages
226242
}

internal/engine/engine.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,15 @@ type Config struct {
115115
// PCAPLinkLayer selects the PCAP datalink decoder for play_pcap_* replay (and mirrors CLI -pcap-link).
116116
// Empty means auto (uses file DLT; LINUX_SLL2 is detected from the global header).
117117
PCAPLinkLayer string
118+
119+
// SipFrom is the SIP From header value before ";tag=" (name-addr or URI). Empty uses gossip@local.
120+
SipFrom string
121+
// SipPAI is the P-Asserted-Identity header value (without the header name).
122+
SipPAI string
123+
// SipProvider sets X-provider to this token (empty omits the header).
124+
SipProvider string
125+
// SipExtraHeaders are full header lines "Name: value" appended after Via on the first request (repeatable -sip_extra_header).
126+
SipExtraHeaders []string
118127
}
119128

120129
type Engine struct {
@@ -1452,6 +1461,7 @@ func (e *Engine) executeCall(
14521461
BasePath: scen.BasePath,
14531462
InjectionFile: e.cfg.InjectionFile,
14541463
}
1464+
applySIPIdentityKeywords(renderCtx.ExtraKeywords, e.cfg, localIP, localPort)
14551465
currentRemoteHost := remoteHost
14561466
currentRemoteIP := remoteHost
14571467
currentRemotePort := remotePort

internal/engine/sip_identity.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package engine
2+
3+
import (
4+
"strconv"
5+
"strings"
6+
)
7+
8+
// applySIPIdentityKeywords sets ExtraKeywords used by built-in scenarios:
9+
//
10+
// [trunk_from] — From display-name/URI (without ";tag="; tag is in XML)
11+
// [trunk_pai] — optional "P-Asserted-Identity: …\r\n"
12+
// [trunk_provider] — optional "X-provider: …\r\n"
13+
// [trunk_extra] — optional additional header lines (each ends with CRLF)
14+
func applySIPIdentityKeywords(m map[string]string, cfg Config, localIP string, localPort int) {
15+
if m == nil {
16+
return
17+
}
18+
from := strings.TrimSpace(cfg.SipFrom)
19+
if from == "" {
20+
from = "gossip <sip:gossip@" + localIP + ":" + strconv.Itoa(localPort) + ">"
21+
}
22+
m["trunk_from"] = from
23+
24+
if p := strings.TrimSpace(cfg.SipPAI); p != "" {
25+
m["trunk_pai"] = "P-Asserted-Identity: " + p + "\r\n"
26+
} else {
27+
m["trunk_pai"] = ""
28+
}
29+
30+
if pr := strings.TrimSpace(cfg.SipProvider); pr != "" {
31+
m["trunk_provider"] = "X-provider: " + pr + "\r\n"
32+
} else {
33+
m["trunk_provider"] = ""
34+
}
35+
36+
var b strings.Builder
37+
for _, line := range cfg.SipExtraHeaders {
38+
line = strings.TrimSpace(line)
39+
if line == "" || !strings.Contains(line, ":") {
40+
continue
41+
}
42+
b.WriteString(line)
43+
switch {
44+
case strings.HasSuffix(line, "\r\n"):
45+
// ok
46+
case strings.HasSuffix(line, "\n"):
47+
b.WriteString("\r")
48+
default:
49+
b.WriteString("\r\n")
50+
}
51+
}
52+
m["trunk_extra"] = b.String()
53+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package engine
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestApplySIPIdentityKeywordsDefaults(t *testing.T) {
8+
t.Parallel()
9+
m := map[string]string{"routes": ""}
10+
cfg := Config{}
11+
applySIPIdentityKeywords(m, cfg, "10.0.0.5", 5060)
12+
if got := m["trunk_from"]; got != "gossip <sip:gossip@10.0.0.5:5060>" {
13+
t.Fatalf("trunk_from: %q", got)
14+
}
15+
if m["trunk_pai"] != "" || m["trunk_provider"] != "" || m["trunk_extra"] != "" {
16+
t.Fatalf("expected empty optional headers, got pai=%q prov=%q extra=%q", m["trunk_pai"], m["trunk_provider"], m["trunk_extra"])
17+
}
18+
}
19+
20+
func TestApplySIPIdentityKeywordsTrunk(t *testing.T) {
21+
t.Parallel()
22+
m := map[string]string{}
23+
cfg := Config{
24+
SipFrom: `"ACME" <sip:+15551212@trunk.example>`,
25+
SipPAI: "sip:+15551212@trunk.example",
26+
SipProvider: "prov1",
27+
SipExtraHeaders: []string{"X-Custom: abc", "X-Other: def"},
28+
}
29+
applySIPIdentityKeywords(m, cfg, "10.0.0.1", 5060)
30+
if m["trunk_from"] != cfg.SipFrom {
31+
t.Fatalf("trunk_from: %q", m["trunk_from"])
32+
}
33+
if m["trunk_pai"] != "P-Asserted-Identity: sip:+15551212@trunk.example\r\n" {
34+
t.Fatalf("trunk_pai: %q", m["trunk_pai"])
35+
}
36+
if m["trunk_provider"] != "X-provider: prov1\r\n" {
37+
t.Fatalf("trunk_provider: %q", m["trunk_provider"])
38+
}
39+
if m["trunk_extra"] != "X-Custom: abc\r\nX-Other: def\r\n" {
40+
t.Fatalf("trunk_extra: %q", m["trunk_extra"])
41+
}
42+
}

internal/launcher/prepare.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ func Prepare(cfg cli.Config) (Prepared, error) {
114114
InjectionFile: cfg.InjectionFile,
115115
Role: roleFromScenario(sc),
116116
PCAPLinkLayer: cfg.PCAPLinkLayer,
117+
SipFrom: cfg.SipFrom,
118+
SipPAI: cfg.SipPAI,
119+
SipProvider: cfg.SipProvider,
120+
SipExtraHeaders: append([]string(nil), cfg.SipExtraHeaders...),
117121
}
118122

119123
return Prepared{

internal/scenario/defaults.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const defaultUAC = `<?xml version="1.0" encoding="UTF-8"?>
66
<![CDATA[
77
INVITE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
88
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
9-
From: gossip <sip:gossip@[local_ip]:[local_port]>;tag=[pid]GossipTag00[call_number]
9+
[trunk_pai][trunk_provider][trunk_extra]From: [trunk_from];tag=[pid]GossipTag00[call_number]
1010
To: [service] <sip:[service]@[remote_ip]:[remote_port]>
1111
Call-ID: [call_id]
1212
CSeq: 1 INVITE
@@ -23,7 +23,7 @@ Content-Length: 0
2323
<![CDATA[
2424
ACK sip:[service]@[remote_ip]:[remote_port] SIP/2.0
2525
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
26-
From: gossip <sip:gossip@[local_ip]:[local_port]>;tag=[pid]GossipTag00[call_number]
26+
[trunk_pai][trunk_provider][trunk_extra]From: [trunk_from];tag=[pid]GossipTag00[call_number]
2727
To: [service] <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
2828
Call-ID: [call_id]
2929
CSeq: 1 ACK
@@ -38,7 +38,7 @@ Content-Length: 0
3838
<![CDATA[
3939
BYE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
4040
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
41-
From: gossip <sip:gossip@[local_ip]:[local_port]>;tag=[pid]GossipTag00[call_number]
41+
[trunk_pai][trunk_provider][trunk_extra]From: [trunk_from];tag=[pid]GossipTag00[call_number]
4242
To: [service] <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
4343
Call-ID: [call_id]
4444
CSeq: 2 BYE
@@ -104,7 +104,7 @@ const defaultInviteMedia = `<?xml version="1.0" encoding="UTF-8"?>
104104
<![CDATA[
105105
INVITE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
106106
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
107-
From: gossip <sip:gossip@[local_ip]:[local_port]>;tag=[pid]InvMedia[call_number]
107+
[trunk_pai][trunk_provider][trunk_extra]From: [trunk_from];tag=[pid]InvMedia[call_number]
108108
To: [service] <sip:[service]@[remote_ip]:[remote_port]>
109109
Call-ID: [call_id]
110110
CSeq: 1 INVITE
@@ -136,7 +136,7 @@ a=rtpmap:0 PCMU/8000
136136
<![CDATA[
137137
ACK sip:[service]@[remote_ip]:[remote_port] SIP/2.0
138138
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
139-
From: gossip <sip:gossip@[local_ip]:[local_port]>;tag=[pid]InvMedia[call_number]
139+
[trunk_pai][trunk_provider][trunk_extra]From: [trunk_from];tag=[pid]InvMedia[call_number]
140140
To: [service] <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
141141
Call-ID: [call_id]
142142
CSeq: 1 ACK
@@ -158,7 +158,7 @@ Content-Length: 0
158158
<![CDATA[
159159
BYE sip:[service]@[remote_ip]:[remote_port] SIP/2.0
160160
Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
161-
From: gossip <sip:gossip@[local_ip]:[local_port]>;tag=[pid]InvMedia[call_number]
161+
[trunk_pai][trunk_provider][trunk_extra]From: [trunk_from];tag=[pid]InvMedia[call_number]
162162
To: [service] <sip:[service]@[remote_ip]:[remote_port]>[peer_tag_param]
163163
Call-ID: [call_id]
164164
CSeq: 2 BYE

internal/shell/hint.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ func printSetCheatsheet(out io.Writer) {
9393
fmt.Fprintln(out, " set transport u1|cl|l1|s1|sn|sl|… — transport (short: t; UAC TLS: l1/ln or cl/cln; UAS UDP: s1/sn; UAS TLS: l1/ln/sl)")
9494
fmt.Fprintln(out, " set total_calls N — total calls; 0 = unlimited (short: m)")
9595
fmt.Fprintln(out, " set calls_per_second R — UAC rate (short: r)")
96+
fmt.Fprintln(out, " set sip_from \"Name <sip:u@h>\" — built-in UAC From before ;tag= (CLI -sip_from)")
97+
fmt.Fprintln(out, " set sip_pai sip:user@domain — P-Asserted-Identity value (CLI -sip_pai)")
98+
fmt.Fprintln(out, " set sip_provider TOKEN — X-provider (CLI -sip_provider)")
9699
fmt.Fprintln(out, " set stat_period 5s — periodic stats line to stderr")
97100
fmt.Fprintln(out, " set trace_msg — SIP trace (off: set trace_msg false)")
98101
fmt.Fprintln(out, "Full flag list: gossipper -h. Smarter review: hint")

0 commit comments

Comments
 (0)