sip

package
v0.12.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 17, 2026 License: MIT Imports: 32 Imported by: 0

Documentation

Index

Constants

View Source
const (
	ContentTypeSDP              = "application/sdp"
	ContentTypeRSMetadata       = "application/rs-metadata+xml"
	ContentTypeRSMetadataLegacy = "application/rs-metadata"
)

SIP body content types we recognize.

Both SIPREC metadata types are accepted because the two RFCs contradicted each other: RFC 7865 specified application/rs-metadata+xml, RFC 7866 specified application/rs-metadata, and neither registered the type. RFC 9806 resolves that erratum (Err7987) in favour of +xml and registers it, but it also records the interoperability consequence -- an SRC written to RFC 7866 sends the unsuffixed name, which was normative for nine years.

View Source
const (
	// DefaultMinSE is the minimum session interval we accept (RFC 4028 §4).
	DefaultMinSE = 90

	// DefaultSessionExpires is the default session interval when the remote
	// requests timers but doesn't specify an interval.
	DefaultSessionExpires = 1800
)
View Source
const (
	DirSendRecv = "sendrecv"
	DirSendOnly = "sendonly"
	DirRecvOnly = "recvonly"
	DirInactive = "inactive"
)

Media direction attribute values (RFC 4566 / RFC 8866).

View Source
const OptionTagSIPREC = "siprec"

OptionTagSIPREC is the option tag an SRC puts in Require to demand a recording session (RFC 7866 §6.1).

View Source
const (
	WhatsAppOutboundHost = "wa.meta.vc"
)

Variables

View Source
var ErrNotRTP = errors.New("not an RTP packet")

ErrNotRTP is returned by ReadRTP when a received UDP packet is not valid RTP (e.g. RTCP, STUN). Callers should continue reading on this error.

Functions

func AMRNBModeSet added in v0.9.0

func AMRNBModeSet(fmtp string) []int

AMRNBModeSet parses an AMR-NB "mode-set=a,b,c" fmtp parameter into the set of allowed speech modes (0..7). Returns nil when no valid mode-set is present (RFC 4867: absence means all modes are permitted). Modes outside 0..7 are dropped.

func AMRNBOctetAligned added in v0.9.0

func AMRNBOctetAligned(fmtp string) bool

AMRNBOctetAligned reports whether the AMR-NB fmtp params select octet-aligned framing. Per RFC 4867 the default (no octet-align) is bandwidth-efficient, so an absent or "octet-align=0" parameter means bandwidth-efficient.

func AMRWBModeSet added in v0.9.0

func AMRWBModeSet(fmtp string) []int

AMRWBModeSet parses an AMR-WB "mode-set=a,b,c" fmtp parameter into the set of allowed speech modes (0..8). Returns nil when no valid mode-set is present (RFC 4867: absence means all modes are permitted). Modes outside 0..8 are dropped.

func AMRWBOctetAligned added in v0.9.0

func AMRWBOctetAligned(fmtp string) bool

AMRWBOctetAligned reports whether the AMR-WB fmtp params select octet-aligned framing. Per RFC 4867 the default (no octet-align) is bandwidth-efficient, so an absent or "octet-align=0" parameter means bandwidth-efficient.

func AddressFamily added in v0.6.0

func AddressFamily(ip string) string

AddressFamily returns the SDP address-type token ("IP4" or "IP6") for an IP literal, or "" when the input is empty / not a literal IP.

func BuildMultipartMixed added in v0.12.0

func BuildMultipartMixed(boundary string, parts []BodyPart) (string, []byte, error)

BuildMultipartMixed renders parts as a multipart/mixed body, returning the Content-Type header value to send with it. The boundary is supplied by the caller so the output is deterministic.

func CanonicalizeAOR added in v0.8.0

func CanonicalizeAOR(u sip.Uri) string

CanonicalizeAOR returns the registry key for an AOR URI. The transformation is: lower-case scheme, strip user-info parameters, lower-case host, keep port only when explicit, drop URI params. Returns "" for malformed input.

func ClampAMRNBMode added in v0.9.0

func ClampAMRNBMode(ceiling int, modeSet []int) int

ClampAMRNBMode constrains a desired ceiling mode to the peer's negotiated mode-set. Behaviour mirrors ClampAMRWBMode but ranges over AMR-NB modes 0..7.

func ClampAMRWBMode added in v0.9.0

func ClampAMRWBMode(ceiling int, modeSet []int) int

ClampAMRWBMode constrains a desired ceiling mode to the peer's negotiated mode-set: it returns the highest set member <= ceiling, or — when the ceiling is below every member — the lowest member (so the result always stays inside the set). A nil/empty set means no restriction, so the ceiling is returned.

func DTMFDigitToEvent

func DTMFDigitToEvent(digit rune) (uint8, bool)

DTMFDigitToEvent converts a DTMF digit character to its RFC 4733 event code.

func DTMFEventToDigit

func DTMFEventToDigit(event uint8) (rune, bool)

DTMFEventToDigit converts an RFC 4733 event code to a digit character.

func EncodeDTMFEvent

func EncodeDTMFEvent(ev DTMFEvent) []byte

EncodeDTMFEvent encodes a DTMFEvent into a 4-byte RFC 4733 payload.

func FormatAMRNBModeSet added in v0.9.0

func FormatAMRNBModeSet(modes []int) string

FormatAMRNBModeSet renders modes as a "0,4,7" mode-set value (the inverse of AMRNBModeSet); returns "" for an empty set.

func FormatAMRWBModeSet added in v0.9.0

func FormatAMRWBModeSet(modes []int) string

FormatAMRWBModeSet renders modes as a "0,1,2" mode-set value (the inverse of AMRWBModeSet); returns "" for an empty set.

func FormatSessionExpires

func FormatSessionExpires(interval uint32, refresher string) string

FormatSessionExpires formats a Session-Expires header value.

func GenerateAnswer

func GenerateAnswer(cfg SDPConfig, selected codec.CodecType, selectedPT uint8, textRejected bool) []byte

GenerateAnswer builds an SDP answer with a single selected codec. selectedPT echoes the remote offer's PT for dynamic codecs. When cfg.TextRTPPort != 0 the answer accepts RTT; when textRejected is true the answer includes a port=0 m=text section per RFC 3264.

func GenerateDTMFPackets

func GenerateDTMFPackets(digit rune, pt uint8, ssrc uint32, baseSeq uint16, baseTS uint32, samplesPerPkt uint16) []*rtp.Packet

GenerateDTMFPackets builds the sequence of RTP packets for one DTMF digit. Returns 7 packets: 4 normal (80ms) + 3 end-of-event, each 20ms apart. The timestamp remains fixed for the entire event per RFC 4733. samplesPerPkt is the telephone-event clock rate per 20ms (e.g. 160 at 8kHz, 320 at AMR-WB's 16kHz); it sets the units of the encoded event duration and must match the negotiated telephone-event clock rate or strict peers drop the digit as too short.

func GenerateOffer

func GenerateOffer(cfg SDPConfig) []byte

GenerateOffer builds an SDP offer with all configured codecs.

func GenerateReInviteSDP

func GenerateReInviteSDP(cfg SDPConfig, selected codec.CodecType, selectedPT uint8, direction string) []byte

GenerateReInviteSDP builds an SDP body for a re-INVITE (hold/unhold). It is similar to GenerateAnswer but uses the specified direction attribute.

func HasOptionTag added in v0.12.0

func HasOptionTag(msg BodyCarrier, tag string) bool

HasOptionTag reports whether Require or Proxy-Require demands tag.

func HasSRCFeatureTag added in v0.12.0

func HasSRCFeatureTag(req *sip.Request) bool

HasSRCFeatureTag reports whether the Contact header identifies a session recording client.

func HoldDirection added in v0.12.0

func HoldDirection(desired string, held bool) string

HoldDirection returns the direction to advertise for a stream whose intent is desired while the leg is on hold. Off hold the intent passes through.

func IsSIPRECInvite added in v0.12.0

func IsSIPRECInvite(call *InboundCall) bool

IsSIPRECInvite reports whether an inbound INVITE opens a recording session.

func IsWhatsAppInvite added in v0.5.0

func IsWhatsAppInvite(call *InboundCall) bool

IsWhatsAppInvite reports whether an inbound INVITE comes from a Meta WhatsApp gateway (meta.vc or any subdomain).

func JoinHostPort added in v0.6.0

func JoinHostPort(host string, port int) string

JoinHostPort wraps net.JoinHostPort with an int port — bracket-safe for IPv6 literals and a no-op for IPv4 / hostnames.

func MirrorDirection added in v0.12.0

func MirrorDirection(offered string) string

MirrorDirection returns the direction an answer must carry for an offered direction, per RFC 3264 §6.1. An offered sendrecv permits any answer; we mirror it as sendrecv and let callers narrow it further.

func MultipartBoundary added in v0.12.0

func MultipartBoundary(seed string) string

MultipartBoundary derives a stable boundary token from seed (typically the Call-ID), so retransmissions and golden tests reproduce the same bytes.

func NarrowDirection added in v0.12.0

func NarrowDirection(mirrored, max string) string

NarrowDirection restricts a mirrored answer direction to at most max, which RFC 3264 §6.1 permits: an answer to sendrecv may be recvonly. An empty max leaves the mirrored direction alone. A recording session uses this to stay receive-only even when the offer was sendrecv.

func NegotiateCodec

func NegotiateCodec(remote *SDPMedia, supported []codec.CodecType) (codec.CodecType, uint8, bool)

NegotiateCodec finds the first codec in the remote SDP that is also in the supported list. Returns the codec type, the payload type from the remote SDP, and whether negotiation succeeded.

func NegotiateCodecPreferred added in v0.6.0

func NegotiateCodecPreferred(remote *SDPMedia, supported []codec.CodecType, preferred codec.CodecType) (codec.CodecType, uint8, bool)

NegotiateCodecPreferred is like NegotiateCodec but biases the choice toward preferred when it is non-zero. The preferred codec must appear in both the remote offer and the supported list; otherwise selection falls back to the regular preference order.

func NegotiateCodecStream added in v0.12.0

func NegotiateCodecStream(remote *RemoteAudioStream, supported []codec.CodecType, preferred codec.CodecType) (codec.CodecType, uint8, bool)

NegotiateCodecStream is NegotiateCodecPreferred for one parsed m=audio section, so each stream of a multi-stream offer negotiates independently.

func OptionTags added in v0.12.0

func OptionTags(msg BodyCarrier, header string) []string

OptionTags returns the option tags carried by every instance of a header, lowercased and trimmed.

func ParseMinSE

func ParseMinSE(value string) uint32

ParseMinSE parses a Min-SE header value, e.g. "90" → 90.

func ParseSessionExpires

func ParseSessionExpires(value string) (interval uint32, refresher string)

ParseSessionExpires parses a Session-Expires header value, e.g. "1800;refresher=uac" → interval=1800, refresher="uac".

func ParseSipfrag added in v0.3.0

func ParseSipfrag(body []byte) (int, string)

ParseSipfrag returns (statusCode, reason) from a sipfrag status line, or (0,"").

func SDPOf added in v0.12.0

func SDPOf(msg BodyCarrier) ([]byte, error)

SDPOf extracts the session description from a SIP message, transparently unwrapping a multipart body.

func TelephoneEventClockRate added in v0.9.0

func TelephoneEventClockRate(c codec.CodecType) int

TelephoneEventClockRate returns the RTP clock rate to pair with the telephone-event (RFC 4733) format for codec c. RFC 4733 requires the telephone-event clock rate to match the audio codec's RTP clock rate, so AMR-WB uses 16 kHz; all other codecs use the conventional 8 kHz.

func UDPNetwork added in v0.6.0

func UDPNetwork(listenIP string) string

UDPNetwork returns the UDP network string ("udp", "udp4", or "udp6") to use for a given listen IP literal. An empty string or "::" yields "udp" so the OS can give us a dual-stack socket on Linux when bindv6only=0.

func UnsupportedHeader added in v0.12.0

func UnsupportedHeader(tags ...string) sip.Header

UnsupportedHeader builds the Unsupported header that must accompany a 420 Bad Extension (RFC 3261 §8.2.2.3).

func WhatsAppRecipientURI added in v0.5.0

func WhatsAppRecipientURI(toUser string) sip.Uri

WhatsAppRecipientURI builds the Request-URI for an outbound call. Meta uses "sip:+E164@wa.meta.vc;transport=tls" — using "sips:" returns 404 because their internal routing isn't strict-TLS end-to-end.

Types

type AnswerOptions added in v0.12.0

type AnswerOptions struct {
	SupportedCodecs []codec.CodecType
	// Preferred biases codec selection for the first accepted audio section.
	Preferred codec.CodecType

	// StrictMLines makes the plan cover every offered m= section, rejecting the
	// ones we don't handle with port 0 as RFC 3264 §6 requires. With it off
	// the plan omits unhandled non-audio sections, preserving the legacy
	// (spec-violating) answer shape.
	StrictMLines bool

	// TextMLineIndex is the m= section the caller renders itself as m=text, or
	// -1 when there is none.
	TextMLineIndex int

	// MaxDirection caps every accepted section's answer direction. Empty means
	// no cap; DirRecvOnly makes the answer receive-only whatever was offered,
	// which is what a recording session (RFC 7866) needs.
	MaxDirection string
}

AnswerOptions parameterizes PlanAnswer.

type AudioStream added in v0.12.0

type AudioStream struct {
	// MID (RFC 5888), Label (RFC 4574), Content (RFC 4796) and Lang (RFC 8866,
	// a BCP 47 tag) are each omitted when empty.
	MID     string
	Label   string
	Content string
	Lang    string

	Port      int
	Direction string // "" means sendrecv
	Ptime     int    // 0 means 20

	// Codecs holds every codec for an offer, or the single selected codec for
	// an answer. CodecPTs overrides the default payload type per codec —
	// answers must echo the offerer's dynamic PTs.
	Codecs   []codec.CodecType
	CodecPTs map[codec.CodecType]uint8

	DTMFPT        uint8
	DTMFClockRate int
	OfferTE48k    bool // emit the extra telephone-event/48000 line used with Opus

	AMRWBOctetAligned bool
	AMRWBModeSet      string
	AMRNBOctetAligned bool
	AMRNBModeSet      string
}

AudioStream describes one m=audio section to generate. A zero Port emits a rejected/disabled section per RFC 3264 §6.

func (*AudioStream) PayloadType added in v0.12.0

func (s *AudioStream) PayloadType(c codec.CodecType) uint8

PayloadType returns the payload type to advertise for c, preferring an explicit CodecPTs entry over the codec's static default.

type AuthResult added in v0.10.0

type AuthResult int

AuthResult is the outcome of verifying an Authorization header against a previously issued challenge.

const (
	// AuthNone means no Authorization header was present, or no live pending
	// challenge matched it. The caller should surface the request as
	// unauthenticated and let the client decide whether to challenge.
	AuthNone AuthResult = iota
	// AuthValid means the digest response matched the expected credential.
	AuthValid
	// AuthInvalid means a matching challenge was found but the response (or
	// username) did not verify. The caller should reject with 403.
	AuthInvalid
)

type Binding added in v0.8.0

type Binding struct {
	AOR            string
	Contact        string
	Socket         string
	Transport      string
	UserAgent      string
	CallID         string
	AppID          string
	CreatedAt      time.Time
	LastRefresh    time.Time
	ExpiresAt      time.Time
	GrantedExpires int // seconds granted at bind time (constant for the binding)
}

Binding represents one Contact registered under an AOR.

type BodyCarrier added in v0.12.0

type BodyCarrier interface {
	Body() []byte
	GetHeaders(name string) []sip.Header
}

BodyCarrier is the subset of a SIP message that carries a body. Both *sip.Request and *sip.Response satisfy it.

type BodyPart added in v0.12.0

type BodyPart struct {
	// ContentType is lowercased with parameters stripped, e.g. "application/sdp".
	ContentType string
	Params      map[string]string
	// Disposition is the Content-Disposition type ("session",
	// "recording-session"), lowercased and without parameters.
	Disposition string
	Headers     map[string]string
	Data        []byte
}

BodyPart is one part of a SIP message body. A non-multipart body is represented as a single part holding the whole body.

type ChallengeParams added in v0.10.0

type ChallengeParams struct {
	Realm     string   // digest realm advertised in the challenge
	Username  string   // expected username; empty accepts whatever username the response carries
	Password  string   // plaintext secret (mutually exclusive with HA1)
	HA1       string   // precomputed H(username:realm:password) hex (mutually exclusive with Password)
	Algorithm string   // "MD5" (default), "SHA-256", "SHA-512-256"
	QOP       []string // advertised qop, e.g. ["auth"]; empty omits the qop directive
}

ChallengeParams carries the digest-challenge inputs a VSI/REST client supplies when it decides to challenge an inbound INVITE or REGISTER. The credential (Password or HA1) is held only for the lifetime of the pending challenge and is never persisted. Exactly one of Password or HA1 must be set.

type DTMFEvent

type DTMFEvent struct {
	Event      uint8 // 0-9, 10=*, 11=#, 12-15=A-D
	EndOfEvent bool
	Volume     uint8  // 0-63
	Duration   uint16 // in timestamp units
}

DTMFEvent represents an RFC 4733 telephone-event payload.

func DecodeDTMFEvent

func DecodeDTMFEvent(payload []byte) (DTMFEvent, error)

DecodeDTMFEvent decodes a 4-byte RFC 4733 payload into a DTMFEvent.

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine wraps sipgo server/client + dialog caches for SIP signaling.

func NewEngine

func NewEngine(cfg EngineConfig) (*Engine, error)

NewEngine creates a SIP engine with the given configuration.

func (*Engine) AMRNBMode added in v0.9.0

func (e *Engine) AMRNBMode() int

AMRNBMode returns the configured AMR-NB encoder speech mode (0..7).

func (*Engine) AMRNBOctetAligned added in v0.9.0

func (e *Engine) AMRNBOctetAligned() bool

AMRNBOctetAligned reports whether AMR-NB offers advertise octet-aligned framing.

func (*Engine) AMRWBMode added in v0.9.0

func (e *Engine) AMRWBMode() int

AMRWBMode returns the configured AMR-WB encoder speech mode (0..8).

func (*Engine) AMRWBOctetAligned added in v0.9.0

func (e *Engine) AMRWBOctetAligned() bool

AMRWBOctetAligned reports whether AMR-WB offers advertise octet-aligned framing.

func (*Engine) AdvertisedIPForFamily added in v0.6.0

func (e *Engine) AdvertisedIPForFamily(family string) string

AdvertisedIPForFamily returns the configured advertised IP for the given SDP address family token ("IP4" or "IP6"). Falls back to the other family when the requested one is unconfigured. Returns empty when neither is configured (caller should reject the call).

func (*Engine) AllowHeader added in v0.9.0

func (e *Engine) AllowHeader() sip.Header

AllowHeader returns an Allow header listing every SIP method this UA answers (RFC 3261 §20.5). The list comes from sipgo's registered request handlers so it can never drift from the methods we actually accept.

func (*Engine) BindIP

func (e *Engine) BindIP() string

BindIP returns the engine's IPv4 advertised address. May be empty in IPv6-only deployments.

func (*Engine) BindIPV6 added in v0.6.0

func (e *Engine) BindIPV6() string

BindIPV6 returns the engine's IPv6 advertised address. Empty when no v6 is configured.

func (*Engine) ChallengeInvite added in v0.10.0

func (e *Engine) ChallengeInvite(call *InboundCall, p ChallengeParams) error

ChallengeInvite sends a 401 Unauthorized carrying a digest WWW-Authenticate challenge on an unanswered inbound INVITE and records the pending challenge so the credentialed re-INVITE (same Call-ID) can be verified by VerifyInboundAuth.

func (*Engine) Codecs

func (e *Engine) Codecs() []codec.CodecType

Codecs returns the engine's supported codecs.

func (*Engine) ContactForInvite added in v0.6.0

func (e *Engine) ContactForInvite(req *sip.Request) *sip.ContactHeader

ContactForInvite is the public form of contactForInvite, used by callers outside this package that need to attach a transport-appropriate Contact header to a dialog response.

func (*Engine) DestinationsPinned added in v0.6.0

func (e *Engine) DestinationsPinned() uint64

DestinationsPinned returns the cumulative count of responses whose destination has been overridden via SIP_USE_SOURCE_SOCKET. Tests use this to assert the flag actually fired rather than silently no-op'd.

func (*Engine) DialogRespond added in v0.6.0

func (e *Engine) DialogRespond(d *sipgo.DialogServerSession, statusCode int, reason string, body []byte, headers ...sip.Header) error

DialogRespond sends a response on a UAS dialog, pinning the destination to the original INVITE source when SIP_USE_SOURCE_SOCKET is enabled. Replaces direct calls to dialog.Respond / dialog.RespondSDP — those reach tx.Respond internally without giving us a hook to override Destination.

func (*Engine) Invite

func (e *Engine) Invite(ctx context.Context, recipient sip.Uri, opts InviteOptions) (*OutboundCall, error)

Invite sends an outbound INVITE and returns an OutboundCall on success. When opts.ForkTargets has more than one entry the INVITE is parallel-forked (RFC 3261 §16); the first branch to reach a 2xx response wins and the others are CANCELled.

func (*Engine) InviteWhatsApp added in v0.5.0

func (e *Engine) InviteWhatsApp(ctx context.Context, recipient sip.Uri, opts WhatsAppInviteOptions) (*WhatsAppOutboundCall, error)

InviteWhatsApp sends an outbound INVITE over SIP/TLS with a pre-built WebRTC SDP offer and digest auth credentials.

func (*Engine) LogSyntheticResponse added in v0.5.0

func (e *Engine) LogSyntheticResponse(req *sip.Request, statusCode int, reason string, body []byte, headers ...sip.Header)

LogSyntheticResponse constructs a response from a request (mirroring what sipgo would build internally) purely for SIP_DEBUG logging. The actual response still goes out through dialog.Respond / dialog.RespondSDP; this is a best-effort wire-form dump so the body and headers we ask sipgo to include are visible on the debug channel.

func (*Engine) OnInvite

func (e *Engine) OnInvite(handler func(*InboundCall))

OnInvite registers a handler for inbound INVITE requests.

func (*Engine) OnNotify added in v0.3.0

func (e *Engine) OnNotify(handler func(callID string, statusCode int, reason string, terminated bool))

OnNotify registers a handler for in-dialog NOTIFY requests carrying a "refer" subscription (RFC 3515 sipfrag). It is invoked once per NOTIFY with the subscription's terminal/transient SIP status parsed from the sipfrag body.

func (*Engine) OnReInvite

func (e *Engine) OnReInvite(handler func(callID string, body *MessageBody) []byte)

OnReInvite registers a handler for in-dialog re-INVITE requests (hold/unhold). The handler receives the SIP Call-ID and the SDP direction attribute, and returns the SDP body to include in the 200 OK response (nil = no SDP).

func (*Engine) OnRefer added in v0.3.0

func (e *Engine) OnRefer(handler func(callID string, target string, replaces *ReplacesParams, req *sip.Request, tx sip.ServerTransaction))

OnRefer registers a handler for in-dialog REFER requests (transfer). The handler is responsible for sending the SIP response (typically 202 Accepted, or 603 Decline when transfers are disabled). req is provided so the handler can pass it to sip.NewResponseFromRequest.

func (*Engine) OnRegisterAttempt added in v0.10.0

func (e *Engine) OnRegisterAttempt(handler func(*RegisterAttempt) RegisterDecision)

OnRegisterAttempt registers the decision callback consulted for each inbound REGISTER that would create or remove a binding. The handler returns whether to accept, challenge (401), or reject the REGISTER. When nil (handler not registered) every REGISTER is auto-accepted, preserving the unauthenticated default.

func (*Engine) OnUpdate added in v0.9.0

func (e *Engine) OnUpdate(handler func(callID string, body *MessageBody, hasSDP bool) []byte)

OnUpdate registers a handler for in-dialog UPDATE requests (RFC 3311), used for session-timer refresh (RFC 4028) and mid-dialog media changes. hasSDP indicates whether the UPDATE carried an SDP offer; when true, direction is the parsed a=sendrecv/sendonly/recvonly/inactive attribute and the returned []byte is the SDP answer for the 200 OK. When false, direction is "" and the handler should only refresh session-timer state.

func (*Engine) PortAllocator

func (e *Engine) PortAllocator() *PortAllocator

PortAllocator returns the engine's port allocator (nil if OS-assigned).

func (*Engine) PublicHost added in v0.5.0

func (e *Engine) PublicHost() string

PublicHost returns the canonical signalling hostname (SIP_DOMAIN when set, otherwise the advertised IP).

func (*Engine) Registrar added in v0.8.0

func (e *Engine) Registrar() *Registrar

Registrar returns the engine's AOR registry (may be nil when the engine was created without one).

func (*Engine) RespondFromSource added in v0.3.0

func (e *Engine) RespondFromSource(tx sip.ServerTransaction, req *sip.Request, statusCode int, reason string) error

RespondFromSource pins the response destination to the request's UDP source so peers with unroutable Via headers still get our reply.

func (*Engine) RespondInviteSDP added in v0.5.0

func (e *Engine) RespondInviteSDP(dialog *sipgo.DialogServerSession, sdp []byte) error

RespondInviteSDP sends a 2xx response to an inbound INVITE with a transport-appropriate Contact header. This is required for WhatsApp inbound calls, which arrive over TLS and need a sips: Contact pointing at our TLS port — otherwise the remote's ACK is routed to the wrong scheme/port, the dialog stays in Early state, and retransmits eventually kill the transaction.

func (*Engine) SIPDebug added in v0.5.0

func (e *Engine) SIPDebug() bool

SIPDebug reports whether SIP_DEBUG is enabled. Consumers that send responses via sipgo (dialog.Respond / RespondSDP) use this to gate LogSyntheticResponse calls.

func (*Engine) SIPHost added in v0.4.0

func (e *Engine) SIPHost() string

func (*Engine) SendNotifySipfrag added in v0.3.0

func (e *Engine) SendNotifySipfrag(ctx context.Context, dialog interface{}, statusCode int, reason string, terminated bool) error

SendNotifySipfrag sends a "refer" subscription NOTIFY with a sipfrag body.

func (*Engine) SendReInvite

func (e *Engine) SendReInvite(ctx context.Context, dialog interface{}, sdpBody []byte) error

SendReInvite sends a re-INVITE within an existing dialog for hold/unhold, discarding the peer's answer. Prefer SendReInviteAnswer when the answer matters — which it does for anything beyond a plain direction flip. dialog must be either *sipgo.DialogServerSession or *sipgo.DialogClientSession.

func (*Engine) SendReInviteAnswer added in v0.12.0

func (e *Engine) SendReInviteAnswer(ctx context.Context, dialog interface{}, sdpBody []byte, apply func(answer []byte)) ([]byte, error)

SendReInviteAnswer sends a re-INVITE and returns the peer's answer body from the 200 OK. When apply is non-nil it is invoked with that body *before* the ACK is written, so local media state is already consistent by the time the peer is cleared to send.

func (*Engine) SendReInviteBody added in v0.12.0

func (e *Engine) SendReInviteBody(ctx context.Context, dialog interface{}, sdpBody []byte, extra []BodyPart, apply func(answer []byte)) ([]byte, error)

SendReInviteBody is SendReInviteAnswer with extra body parts appended, so a re-INVITE can carry an updated SIPREC metadata document alongside the offer. With no extra parts the request is byte-identical to the plain SDP form.

func (*Engine) SendRefer added in v0.3.0

func (e *Engine) SendRefer(ctx context.Context, dialog interface{}, referTo string, replaces *ReplacesParams) error

SendRefer sends an in-dialog REFER, returning nil on 202 Accepted.

func (*Engine) Serve

func (e *Engine) Serve(ctx context.Context) error

Serve starts the SIP server and blocks until ctx is cancelled. When TLSBindPort is configured it runs UDP and TLS listeners concurrently; if either fails the other is torn down via ctx cancellation. A secondary UDP listener is started when listenIPV6 is set to a literal distinct from listenIP (rare dual-bind case for hosts with bindv6only=1).

func (*Engine) ServerHeader added in v0.4.0

func (e *Engine) ServerHeader() sip.Header

ServerHeader returns a SIP Server header for UAS responses.

func (*Engine) StrictMLineAnswer added in v0.12.0

func (e *Engine) StrictMLineAnswer() bool

StrictMLineAnswer reports whether answers cover every offered m= section.

func (*Engine) TLSPort added in v0.5.0

func (e *Engine) TLSPort() int

TLSPort returns the configured SIP TLS port (0 = disabled).

func (*Engine) Trunks added in v0.9.0

func (e *Engine) Trunks() *TrunkManager

Trunks returns the engine's outbound SIP trunk manager. Always non-nil.

func (*Engine) UserAgentHeader added in v0.9.0

func (e *Engine) UserAgentHeader() sip.Header

UserAgentHeader returns a SIP User-Agent header for UAC requests. Mirrors the Server header so peers see the same identity in both directions.

func (*Engine) VerifyInboundAuth added in v0.10.0

func (e *Engine) VerifyInboundAuth(req *sip.Request, method string) (AuthResult, string, int)

VerifyInboundAuth validates the Authorization header of an inbound request against the challenge previously issued for its Call-ID. method is the SIP method ("INVITE" / "REGISTER") signed by the digest. On AuthValid the second return value is the authenticated username and the third is the TTL cap (seconds, 0 = none) recorded with the challenge — meaningful only for REGISTER. Non-valid results return "" and 0.

type EngineConfig

type EngineConfig struct {
	BindIP      string // IPv4 advertised address for SDP c= line / Contact (when v4 is in use)
	BindIPV6    string // IPv6 advertised address; empty = v6 not advertised
	ListenIP    string // IPv4 socket bind (default: same as BindIP). Special values: "0.0.0.0", "::" (dual-stack)
	ListenIPV6  string // IPv6 socket bind (default: same as BindIPV6). Used when configured separately from ListenIP.
	ExternalIP  string // IPv4 public IP override for NAT/Docker (v6 has no equivalent — set BindIPV6 directly)
	PublicHost  string // FQDN advertised in From/Contact/Via signaling headers; falls back to ExternalIP/BindIP when empty
	BindPort    int
	TLSBindPort int    // 0 = TLS disabled
	TLSCertPath string // CA-signed cert (fullchain.pem) — required when TLSBindPort > 0
	TLSKeyPath  string // private key (privkey.pem) — required when TLSBindPort > 0
	SIPDebug    bool   // dump full SIP request/response bodies on the debug channel
	SIPHost     string
	// UseSourceSocket forces SIP responses and in-dialog requests to be
	// routed back to the request's source socket (req.Source()) instead of
	// the peer's Contact URI / Via sent-by. Required when peers are behind
	// NAT and advertise unroutable addresses.
	UseSourceSocket bool
	Codecs          []codec.CodecType
	// AMRWBMode is the AMR-WB encoder speech mode (0..8) offered/transmitted.
	AMRWBMode int
	// AMRWBOctetAligned selects octet-aligned (true) vs bandwidth-efficient
	// (false) AMR-WB framing in generated offers.
	AMRWBOctetAligned bool
	// AMRNBMode is the AMR-NB encoder speech mode (0..7) offered/transmitted.
	AMRNBMode int
	// AMRNBOctetAligned selects octet-aligned (true) vs bandwidth-efficient
	// (false) AMR-NB framing in generated offers.
	AMRNBOctetAligned bool
	Log               *slog.Logger
	PortAllocator     *PortAllocator // nil = OS-assigned ports

	// Registrar, when non-nil, enables inbound REGISTER handling and AOR
	// resolution for outbound INVITEs.
	Registrar *Registrar

	// NonceTTL bounds the lifetime of an issued inbound-auth challenge nonce.
	// Zero falls back to the store default (60s).
	NonceTTL time.Duration

	// StrictMLineAnswer makes answers carry a port-0 placeholder for every
	// offered section we don't accept, per RFC 3264 §6.
	StrictMLineAnswer bool

	// TCPEnabled adds a TCP listener on BindPort alongside the UDP one. Needed
	// for inbound SIPREC: sipgo refuses to send a request over 1300 bytes on
	// UDP (RFC 3261 §18.1.1), and a recording INVITE always exceeds that.
	TCPEnabled bool
}

EngineConfig holds configuration for the SIP engine.

type ForkTarget added in v0.8.0

type ForkTarget struct {
	Socket    string // "ip:port"
	Transport string // "udp" | "tcp" | "tls"
}

ForkTarget identifies one branch of a parallel-forked INVITE: an explicit transport-layer destination (ip:port) that overrides the routing implied by the Request-URI's host. Used when dialing a registered AOR with one or more bound contacts; one ForkTarget per bound contact.

type IPIPTrunkView added in v0.9.0

type IPIPTrunkView struct {
	PeerURI string `json:"peer_uri,omitempty"`
}

IPIPTrunkView is the placeholder shape for the unimplemented ip_ip type.

type InboundCall

type InboundCall struct {
	Dialog    *sipgo.DialogServerSession
	From      string    // caller URI user part
	To        string    // callee URI user part
	RemoteSDP *SDPMedia // parsed offer SDP
	Request   *sip.Request

	// Body is the parsed INVITE body. Multipart offers (SIPREC) carry the
	// rs-metadata document alongside the SDP.
	Body *MessageBody

	// Session timer (RFC 4028) — populated when remote requests timers.
	SessionTimer *SessionTimerParams // nil when remote didn't request timers
}

InboundCall wraps a sipgo DialogServerSession with parsed SDP.

type InviteOptions

type InviteOptions struct {
	Codecs       []codec.CodecType                              // Override engine codecs for this call; nil = use engine default
	Headers      []sip.Header                                   // Extra SIP headers to include in the INVITE
	FromUser     string                                         // Override the user part of the From header (caller ID)
	FromHost     string                                         // Override the host part of the From/P-Asserted-Identity URI (e.g. a registered trunk's AOR realm); empty = engine publicHost
	OnEarlyMedia func(remoteSDP *SDPMedia, rtpSess *RTPSession) // Called on first 183 with SDP
	AuthUsername string                                         // SIP digest auth username (optional)
	AuthPassword string                                         // SIP digest auth password (optional)

	// ForkTargets, when non-empty, routes the INVITE to the listed transport
	// addresses. A single target overrides the destination for a single
	// outbound dialog; multiple targets parallel-fork the INVITE and race
	// for the first 2xx (RFC 3261 §16).
	ForkTargets []ForkTarget

	// RouteURI, when set, adds a single loose-route header pointing at the
	// given URI (typically a trunk's upstream proxy/registrar). The ";lr"
	// param is appended if not already present.
	RouteURI *sip.Uri

	// RTT (T.140 / RFC 4103) parameters. RTTEnabled offers m=text alongside
	// audio in the INVITE. RTTRedundancy controls the RFC 2198 RED depth
	// (0 = plain T.140, no RED).
	RTTEnabled    bool
	RTTRedundancy int

	// Streams, when it holds more than one entry, offers that many m=audio
	// sections. The first is the call's primary bidirectional audio; the rest
	// are additional streams (e.g. a translated feed). Empty or single-entry
	// offers emit exactly the SDP a single-stream call always has.
	Streams []OfferStream

	// BodyParts, when non-empty, sends the offer as a multipart/mixed body with
	// the SDP as its first part and these appended — how RFC 7866 carries the
	// rs-metadata document alongside the offer. Empty emits the plain
	// application/sdp body every other call uses.
	BodyParts []BodyPart
}

InviteOptions holds optional parameters for outbound INVITE.

type MLineSlot added in v0.12.0

type MLineSlot struct {
	Index int
	Media string   // "audio", "text", or a kind we only hold a place for
	Proto []string // echoed verbatim when the slot is rejected

	MID     string
	Label   string
	Content string
	Lang    string

	State SlotState

	// StreamID links an audio slot to the media stream carrying it. Empty for
	// tombstones and for slots we merely hold a position for.
	StreamID string

	Local  AudioStream       // what we advertise
	Remote RemoteAudioStream // what the peer last told us
}

MLineSlot is one position in a dialog's m-line vector.

func (*MLineSlot) Audio added in v0.12.0

func (s *MLineSlot) Audio() bool

Audio reports whether the slot is an audio section.

type MLineTable added in v0.12.0

type MLineTable struct {
	// contains filtered or unexported fields
}

MLineTable is a dialog's m-line vector. Slots are append-only for the life of the dialog: RFC 3264 §8 requires that the m-line count never decreases and that the i-th line always describes the same stream. Removal is a tombstone, never a deletion.

Not safe for concurrent use — the owning leg serializes access.

func (*MLineTable) ActiveAudio added in v0.12.0

func (t *MLineTable) ActiveAudio() []*MLineSlot

ActiveAudio returns the audio slots currently carrying media, in m-line order.

func (*MLineTable) ActiveAudioCount added in v0.12.0

func (t *MLineTable) ActiveAudioCount() int

ActiveAudioCount returns how many audio slots are active or awaiting an answer — the figure a per-dialog stream cap applies to.

func (*MLineTable) Append added in v0.12.0

func (t *MLineTable) Append(s MLineSlot) int

Append adds a slot at the next free position and returns its index. The slot's Index is overwritten to match.

func (*MLineTable) ByMID added in v0.12.0

func (t *MLineTable) ByMID(mid string) (*MLineSlot, bool)

ByMID returns the slot carrying the given a=mid value.

func (*MLineTable) ByStreamID added in v0.12.0

func (t *MLineTable) ByStreamID(id string) (*MLineSlot, bool)

ByStreamID returns the slot bound to the given media stream.

func (*MLineTable) Len added in v0.12.0

func (t *MLineTable) Len() int

Len returns the number of slots, i.e. the m-line count every offer and answer on this dialog must carry.

func (*MLineTable) LocalStreams added in v0.12.0

func (t *MLineTable) LocalStreams(held bool, skipMedia map[int]bool) []AudioStream

LocalStreams renders the table as the audio sections of an offer or answer: every slot in index order, tombstones as port 0. held applies the RFC 3264 hold transform to each active slot's desired direction.

Non-audio slots are returned as zero-port audio placeholders only when they were never ours to describe; callers that own an m=text section render it themselves and pass skipMedia to exclude it here.

func (*MLineTable) MintMID added in v0.12.0

func (t *MLineTable) MintMID() string

MintMID returns an a=mid value not yet used in this table.

func (*MLineTable) ReserveMID added in v0.12.0

func (t *MLineTable) ReserveMID(mid string)

ReserveMID records mid as used so MintMID never collides with it. Call it for every mid adopted from a peer's offer.

func (*MLineTable) Slot added in v0.12.0

func (t *MLineTable) Slot(i int) *MLineSlot

Slot returns the slot at index i, or nil when i is out of range.

func (*MLineTable) Slots added in v0.12.0

func (t *MLineTable) Slots() []MLineSlot

Slots returns the backing slice for iteration. Callers must not append to it.

func (*MLineTable) Tombstone added in v0.12.0

func (t *MLineTable) Tombstone(i int)

Tombstone disables the slot at index i permanently, keeping its position.

type MessageBody added in v0.12.0

type MessageBody struct {
	Raw       []byte
	Multipart bool
	Parts     []BodyPart
}

MessageBody is a parsed SIP message body.

func BodyOf added in v0.12.0

func BodyOf(msg BodyCarrier) (*MessageBody, error)

BodyOf parses the body of a SIP message using its Content-Type header.

func ParseMessageBody added in v0.12.0

func ParseMessageBody(contentType string, body []byte) (*MessageBody, error)

ParseMessageBody splits body according to contentType. A body that is not multipart is returned verbatim as a single part, so every existing single-part call site keeps identical bytes.

func (*MessageBody) Part added in v0.12.0

func (b *MessageBody) Part(contentType string) ([]byte, bool)

Part returns the body of the first part with the given content type.

func (*MessageBody) RSMetadata added in v0.12.0

func (b *MessageBody) RSMetadata() ([]byte, bool)

RSMetadata returns the SIPREC metadata document (RFC 7865), accepting both the registered and the legacy content type.

func (*MessageBody) SDP added in v0.12.0

func (b *MessageBody) SDP() ([]byte, bool)

SDP returns the session description. A single-part body is returned whatever its declared type, preserving the behaviour of peers that omit or misdeclare Content-Type on a plain SDP offer — except when it declares itself a type we know is not SDP.

RFC 7866 §9.1 lets an SRC send metadata with no SDP in an INVITE, an UPDATE, or a 200 to an offerless INVITE, and is explicit that "when a SIP message contains only an SDP offer or metadata, the multipart/mixed container is optional". So a bare rs-metadata body is a correct request, and taking it for an offer answers it 400 Bad SDP.

type OfferStream added in v0.12.0

type OfferStream struct {
	Direction string // "" means sendrecv
	Lang      string // BCP 47 tag for a=lang
	Content   string // a=content, e.g. "main" or "alt"
	Label     string // a=label
}

OfferStream describes one m=audio section to offer beyond the defaults.

type OutboundCall

type OutboundCall struct {
	Dialog    *sipgo.DialogClientSession
	RemoteSDP *SDPMedia
	RTPSess   *RTPSession

	// ExtraRTPSess holds the media sockets for audio sections beyond the first,
	// in m-line order. Nil for single-stream calls. OfferedStreams describes
	// every audio section offered, including the first, so the leg can zip the
	// peer's answer onto them positionally.
	ExtraRTPSess   []*RTPSession
	OfferedStreams []AudioStream

	// Optional RTT (T.140 / RFC 4103) media. Populated when the remote's
	// answer accepts the offered m=text section. Nil otherwise.
	TextRTPSess *RTPSession

	// Session timer (RFC 4028) — populated when remote's 200 OK includes timers.
	SessionTimer *SessionTimerParams // nil when remote didn't include timers
}

OutboundCall wraps a sipgo DialogClientSession with parsed answer SDP.

type OutboundRegistration added in v0.9.0

type OutboundRegistration struct {
	// contains filtered or unexported fields
}

OutboundRegistration is the sip_register Trunk implementation. One instance per trunk; safe for concurrent access.

func NewOutboundRegistration added in v0.9.0

func NewOutboundRegistration(engine *Engine, bus *events.Bus, log *slog.Logger, cfg OutboundRegistrationConfig, p OutboundRegistrationParams) *OutboundRegistration

NewOutboundRegistration constructs a trunk in the registering state. Call Start to launch the background lifecycle.

func (*OutboundRegistration) AOR added in v0.9.0

func (r *OutboundRegistration) AOR() string

func (*OutboundRegistration) AppID added in v0.9.0

func (r *OutboundRegistration) AppID() string

func (*OutboundRegistration) Credentials added in v0.9.0

func (r *OutboundRegistration) Credentials() (string, string)

Credentials returns the trunk's digest username/password. Used by the outbound INVITE path; never exposed over the API.

func (*OutboundRegistration) FromHost added in v0.12.0

func (r *OutboundRegistration) FromHost() string

FromHost returns the AOR realm host — the domain the upstream registrar authenticated us under. Used as the host part of the From and P-Asserted-Identity URIs on outbound INVITEs placed from this trunk, so the call claims the identity the trunk actually registered rather than the engine's own public host.

No lock: r.aor is assigned once in the constructor and never mutated, the same reason AOR() reads it lock-free.

func (*OutboundRegistration) ID added in v0.9.0

func (r *OutboundRegistration) ID() string

func (*OutboundRegistration) PeerSocket added in v0.9.0

func (r *OutboundRegistration) PeerSocket() (host string, port int, transport string)

func (*OutboundRegistration) RegistrarURI added in v0.9.0

func (r *OutboundRegistration) RegistrarURI() sip.Uri

RegistrarURI exposes the configured upstream registrar URI; used by the outbound INVITE path to attach a Route header.

func (*OutboundRegistration) Snapshot added in v0.9.0

func (r *OutboundRegistration) Snapshot() TrunkView

Snapshot returns the current TrunkView. Safe to call concurrently.

func (*OutboundRegistration) Start added in v0.9.0

func (r *OutboundRegistration) Start(ctx context.Context)

Start launches the background register-and-refresh loop. Calling Start more than once is a no-op.

func (*OutboundRegistration) Stop added in v0.9.0

Stop cancels the refresh loop and sends a final REGISTER with Expires: 0. Best-effort; honours ctx.

func (*OutboundRegistration) Type added in v0.9.0

func (r *OutboundRegistration) Type() TrunkType

type OutboundRegistrationConfig added in v0.9.0

type OutboundRegistrationConfig struct {
	DefaultExpiresSeconds int
	MinExpiresSeconds     int
	MaxExpiresSeconds     int
	RefreshRatio          float64
	FailureBackoffMax     time.Duration
}

OutboundRegistrationConfig holds tunables shared across every sip_register trunk.

type OutboundRegistrationParams added in v0.9.0

type OutboundRegistrationParams struct {
	ID                      string
	AppID                   string
	RegistrarURI            sip.Uri
	AOR                     sip.Uri
	Username                string
	Password                string
	ContactUser             string
	RequestedExpiresSeconds int
}

OutboundRegistrationParams is the per-trunk creation payload (the values that survive validation of a CreateTrunkRequest of type sip_register).

type PortAllocator

type PortAllocator struct {
	// contains filtered or unexported fields
}

PortAllocator manages a pool of UDP ports within a configured range. It is safe for concurrent use.

func NewPortAllocator

func NewPortAllocator(min, max int) (*PortAllocator, error)

NewPortAllocator creates a port allocator for the given range [min, max]. Returns nil if min and max are both 0 (use OS-assigned ports).

func (*PortAllocator) Allocate

func (pa *PortAllocator) Allocate() (int, error)

Allocate returns the next available port from the pool.

func (*PortAllocator) Range

func (pa *PortAllocator) Range() (int, int)

Range returns the configured min and max ports.

func (*PortAllocator) Release

func (pa *PortAllocator) Release(port int)

Release returns a port back to the pool.

type RTPSession

type RTPSession struct {
	// contains filtered or unexported fields
}

RTPSession manages a UDP socket for RTP send/receive. It implements symmetric RTP (RFC 4961): once an RTP packet is received, the remote address is latched to the source IP:port of that packet, overriding the SDP-provided address. This is essential for NAT traversal.

func NewRTPSession

func NewRTPSession() (*RTPSession, error)

NewRTPSession creates a new RTP session listening on a random UDP port. The socket is dual-stack on Linux (accepts both v4 and v6 when /proc/sys/net/ipv6/bindv6only is 0, which is the default).

func NewRTPSessionFromAllocator

func NewRTPSessionFromAllocator(alloc *PortAllocator) (*RTPSession, error)

NewRTPSessionFromAllocator creates an RTP session using a port from the allocator's pool. If alloc is nil, behaves like NewRTPSession (OS-assigned).

func NewRTPSessionOnPort

func NewRTPSessionOnPort(port int) (*RTPSession, error)

NewRTPSessionOnPort creates a new RTP session on a specific local port. Same dual-stack semantics as NewRTPSession.

func (*RTPSession) Close

func (s *RTPSession) Close() error

Close closes the UDP connection and releases the port back to the allocator. Idempotent: subsequent calls are no-ops. Multiple rollback paths in the engine ladder can each call Close on the same session.

func (*RTPSession) LocalPort

func (s *RTPSession) LocalPort() int

LocalPort returns the local UDP port this session is listening on.

func (*RTPSession) ReadRTP

func (s *RTPSession) ReadRTP() (*rtp.Packet, error)

ReadRTP reads and unmarshals an RTP packet from the UDP socket. Blocks until data arrives. Implements symmetric RTP: the remote address is latched to the source IP:port of each incoming RTP packet.

func (*RTPSession) RemoteAddr added in v0.12.0

func (s *RTPSession) RemoteAddr() *net.UDPAddr

RemoteAddr returns the address media is currently sent to, or nil before the first SetRemote. Reflects symmetric-RTP latching as well as SDP negotiation.

func (*RTPSession) SendKeepalive

func (s *RTPSession) SendKeepalive(payloadType uint8, count int)

SendKeepalive sends a small burst of silence RTP packets to the remote address. This is used immediately after SetRemote on outbound calls to punch through NAT devices (port-latching) before the leg's full media pipeline starts.

func (*RTPSession) SetReadDeadline

func (s *RTPSession) SetReadDeadline(t time.Time) error

SetReadDeadline sets a deadline on the underlying UDP socket for reads.

func (*RTPSession) SetRemote

func (s *RTPSession) SetRemote(ip string, port int) error

SetRemote sets the remote address for sending RTP packets. ip may be either an IPv4 or IPv6 literal (or a hostname); IPv6 literals are bracketed via net.JoinHostPort.

func (*RTPSession) WriteRTP

func (s *RTPSession) WriteRTP(pkt *rtp.Packet) error

WriteRTP marshals and sends an RTP packet to the remote address.

type RegisterAttempt added in v0.10.0

type RegisterAttempt struct {
	Request   *sip.Request
	AOR       string
	Contact   string
	Source    string
	Transport string
	UserAgent string
	CallID    string
	HasAuth   bool
}

RegisterAttempt describes an inbound REGISTER surfaced to the decision callback so the client can decide whether to challenge it (e.g. based on the source address).

type RegisterDecision added in v0.10.0

type RegisterDecision struct {
	Kind         RegisterDecisionKind
	Challenge    ChallengeParams // used when Kind == RegisterChallenge
	RejectCode   int             // used when Kind == RegisterReject; 0 => 403
	RejectReason string          // used when Kind == RegisterReject; "" => "Forbidden"
	// MaxExpires, when > 0, caps the granted binding TTL (seconds) for a REGISTER
	// this decision admits (accept, or the credentialed retry of a challenge). It
	// only ever shortens: the effective grant is min(ClampExpires(requested),
	// MaxExpires), still subject to the 60s floor. 0 leaves the registrar's normal
	// clamp in force.
	MaxExpires int
}

RegisterDecision is returned by the OnRegisterAttempt callback.

type RegisterDecisionKind added in v0.10.0

type RegisterDecisionKind int

RegisterDecisionKind is the action a VSI/REST client chooses for an inbound REGISTER it was consulted about.

const (
	RegisterAccept    RegisterDecisionKind = iota // bind and reply 200 OK
	RegisterChallenge                             // reply 401 with a digest challenge
	RegisterReject                                // reply with a non-2xx (default 403)
)

type Registrar added in v0.8.0

type Registrar struct {
	// contains filtered or unexported fields
}

Registrar is the in-memory AOR registry. Safe for concurrent use.

func NewRegistrar added in v0.8.0

func NewRegistrar(bus *events.Bus, log *slog.Logger, cfg RegistrarConfig) *Registrar

NewRegistrar constructs a registrar. bus may be nil (events are dropped).

func (*Registrar) Bind added in v0.8.0

func (r *Registrar) Bind(b Binding)

Bind inserts a new binding or refreshes an existing one (same AOR + Contact identity). When AllowMultipleContacts is false, any pre-existing bindings for the AOR are removed first (one event each, reason "replaced"). Emits sip.registration_active.

func (*Registrar) ClampExpires added in v0.8.0

func (r *Registrar) ClampExpires(requested int) int

ClampExpires clamps a requested expiry to [60, MaxExpiresSeconds]. A non-positive input is treated as the default.

func (*Registrar) Config added in v0.8.0

func (r *Registrar) Config() RegistrarConfig

Config returns the effective configuration (with defaults applied).

func (*Registrar) List added in v0.8.0

func (r *Registrar) List() []Binding

List returns every binding across all AORs. Order is not specified.

func (*Registrar) Lookup added in v0.8.0

func (r *Registrar) Lookup(aor string) (Binding, bool)

Lookup returns the most-recently-refreshed binding for an AOR. The second return value is false when no binding exists.

func (*Registrar) LookupAll added in v0.8.0

func (r *Registrar) LookupAll(aor string) []Binding

LookupAll returns every binding for an AOR, sorted most-recently-refreshed first.

func (*Registrar) Start added in v0.8.0

func (r *Registrar) Start(ctx context.Context)

Start launches the expiry sweeper. Returns immediately; the sweeper runs until ctx is cancelled.

func (*Registrar) UnbindAll added in v0.8.0

func (r *Registrar) UnbindAll(aor, reason string) int

UnbindAll removes every binding under the AOR. One event per binding. Returns the number of removed bindings.

func (*Registrar) UnbindContact added in v0.8.0

func (r *Registrar) UnbindContact(aor, contactURI, reason string) bool

UnbindContact removes a single contact under an AOR. Returns true if a binding was removed.

type RegistrarConfig added in v0.8.0

type RegistrarConfig struct {
	DefaultExpiresSeconds int
	MaxExpiresSeconds     int
	SweepInterval         time.Duration
	AllowMultipleContacts bool
}

RegistrarConfig holds tunables for the registrar.

type RemoteAudioStream added in v0.12.0

type RemoteAudioStream struct {
	Index int // m-line position within the SDP

	MID     string
	Label   string
	Content string
	Lang    string

	RemoteIP      string
	RemotePort    int
	AddressFamily string // "IP4" or "IP6"

	// Direction is the media-level attribute, falling back to the session-level
	// one. It stays empty when neither is present; use EffectiveDirection for
	// the RFC 8866 sendrecv default.
	Direction string
	Ptime     int
	RTCPMux   bool

	Codecs       []codec.CodecType
	CodecPTs     map[codec.CodecType]uint8
	CodecRates   map[codec.CodecType]int
	CodecFmtp    map[codec.CodecType]string
	DTMFEventPTs map[uint8]int
}

RemoteAudioStream holds the parsed parameters of one remote m=audio section. A zero RemotePort means the peer rejected or disabled the section.

func (*RemoteAudioStream) DTMFPTForRate added in v0.12.0

func (r *RemoteAudioStream) DTMFPTForRate(rate int) (uint8, bool)

DTMFPTForRate returns the telephone-event payload type advertised at the given clock rate, if any.

func (*RemoteAudioStream) EffectiveDirection added in v0.12.0

func (r *RemoteAudioStream) EffectiveDirection() string

EffectiveDirection resolves an absent direction attribute to the RFC 8866 default of sendrecv.

func (*RemoteAudioStream) PreferredDTMFEvent added in v0.12.0

func (r *RemoteAudioStream) PreferredDTMFEvent() (pt uint8, rate int, ok bool)

PreferredDTMFEvent returns the telephone-event PT and clock rate to mirror in an answer. When several were offered the lowest PT wins, for determinism.

type RemoteMLine added in v0.12.0

type RemoteMLine struct {
	Index   int
	Media   string   // "audio", "video", "text", "application", ...
	Proto   []string // must be echoed verbatim when rejecting the section
	Port    int
	MID     string
	Formats []string

	// AudioIdx indexes into SDPMedia.Audio, or -1 for non-audio sections.
	AudioIdx int
}

RemoteMLine records one m= section of any kind in offer order. Sections we don't handle are recorded too, so an answer can preserve the m-line count and ordering required by RFC 3264 §6.

type ReplacesParams added in v0.3.0

type ReplacesParams struct {
	CallID  string
	ToTag   string
	FromTag string
}

ReplacesParams carries the Replaces header dialog identity (RFC 3891).

func ParseReferTo added in v0.3.0

func ParseReferTo(value string) (string, *ReplacesParams, error)

ParseReferTo extracts the bare URI and (optional) Replaces from a Refer-To.

func (*ReplacesParams) String added in v0.3.0

func (p *ReplacesParams) String() string

String formats as the Replaces value embedded in a Refer-To URI.

type SDPConfig

type SDPConfig struct {
	LocalIP string
	RTPPort int
	Codecs  []codec.CodecType // Offered/supported codecs in preference order

	// Optional RTT (T.140 / RFC 4103) section. Set TextRTPPort != 0 to emit
	// an m=text line in offers/answers/re-INVITEs. TextT140PT and TextREDPT
	// are dynamic payload types; TextREDPT == 0 disables RFC 2198 redundancy.
	// RTTRedundancy is the number of t140/t140/.../t140 generations declared
	// in the RED fmtp.
	TextRTPPort   int
	TextT140PT    uint8
	TextREDPT     uint8
	RTTRedundancy int

	// AMRWBOctetAligned controls the AMR-WB fmtp emitted for an offer/answer:
	// true emits "octet-align=1", false emits no octet-align (RFC 4867 default,
	// bandwidth-efficient). On answers it must echo the peer's negotiated format.
	AMRWBOctetAligned bool

	// AMRWBModeSet, when non-empty (e.g. "0,1,2"), adds a "mode-set=..." AMR-WB
	// fmtp param. Used on answers to echo the peer's negotiated mode-set per
	// RFC 4867; left empty on offers (we accept all modes on receive).
	AMRWBModeSet string

	// AMRNBOctetAligned controls the AMR-NB fmtp emitted for an offer/answer:
	// true emits "octet-align=1", false emits no octet-align (RFC 4867 default,
	// bandwidth-efficient). On answers it must echo the peer's negotiated format.
	AMRNBOctetAligned bool

	// AMRNBModeSet, when non-empty (e.g. "0,4,7"), adds a "mode-set=..." AMR-NB
	// fmtp param. Used on answers to echo the peer's negotiated mode-set per
	// RFC 4867; left empty on offers (we accept all modes on receive).
	AMRNBModeSet string

	// DTMFPT, when non-zero, is the telephone-event (RFC 4733) PT to advertise
	// in the generated SDP. Zero defaults to 101. In answers callers should set
	// this from the remote offer to mirror the offerer's choice.
	DTMFPT uint8

	// DTMFClockRate, when non-zero, is the telephone-event clock rate (Hz) to
	// advertise. Zero defaults to TelephoneEventClockRate(selected codec). In
	// answers callers must set this from the remote offer so RFC 3264
	// offer/answer semantics hold — phones like Fanvil pin telephone-event at
	// 8 kHz regardless of audio codec, and unilaterally upgrading to 16 kHz
	// breaks their DTMF.
	DTMFClockRate int

	// Streams, when non-empty, replaces the single audio section the generators
	// would otherwise derive from RTPPort/Codecs/AMR*/DTMF*. Sections are
	// emitted in slice order; a zero Port rejects one per RFC 3264 §6. The
	// Text* fields still control the m=text section.
	Streams []AudioStream
}

SDPConfig holds local media parameters for SDP generation.

type SDPMedia

type SDPMedia struct {
	RemoteIP      string
	RemotePort    int
	AddressFamily string                     // "IP4" or "IP6" (from c= line); empty if not present
	Codecs        []codec.CodecType          // Codecs from m= line, in offer order
	CodecPTs      map[codec.CodecType]uint8  // Actual PT for each codec from remote SDP
	CodecRates    map[codec.CodecType]int    // Clock rate (Hz) for each codec, from a=rtpmap; falls back to codec default
	CodecFmtp     map[codec.CodecType]string // Raw a=fmtp params for each codec (e.g. AMR-WB "octet-align=1; mode-set=...")
	Ptime         int                        // ms, default 20
	Direction     string                     // "sendrecv", "sendonly", "recvonly", "inactive"; empty = sendrecv
	DTMFEventPTs  map[uint8]int              // telephone-event (RFC 4733) PT -> clock rate, as advertised by the remote

	// Audio holds every m=audio section in SDP order, including ones the peer
	// rejected with port 0. MLines holds every m= section of any kind, so an
	// answer can preserve the count and ordering RFC 3264 §6 requires.
	Audio  []RemoteAudioStream
	MLines []RemoteMLine

	// PrimaryAudio indexes the Audio entry the scalar fields above mirror: the
	// first section with a non-zero port, or -1 when there is none. The scalars
	// are written only by syncPrimary.
	PrimaryAudio int

	// Text (RTT, T.140 / RFC 4103). Non-nil when the remote SDP carried an
	// m=text line with a non-zero port. A port of zero (peer rejecting the
	// text section per RFC 3264) leaves this field nil.
	Text *SDPTextMedia
}

SDPMedia holds parsed remote media parameters.

func ParseSDP

func ParseSDP(raw []byte) (*SDPMedia, error)

ParseSDP parses a remote SDP body and extracts media parameters.

func ParseSDPMessage added in v0.12.0

func ParseSDPMessage(msg BodyCarrier) (*SDPMedia, error)

ParseSDPMessage parses the session description carried by a SIP message, transparently unwrapping a multipart body.

func (*SDPMedia) AudioByMID added in v0.12.0

func (m *SDPMedia) AudioByMID(mid string) (*RemoteAudioStream, bool)

AudioByMID returns the audio section carrying the given a=mid value.

func (*SDPMedia) DTMFPTForRate added in v0.9.0

func (m *SDPMedia) DTMFPTForRate(rate int) (uint8, bool)

DTMFPTForRate returns the remote telephone-event payload type advertised at the given clock rate, if any.

func (*SDPMedia) PreferredDTMFEvent added in v0.9.0

func (m *SDPMedia) PreferredDTMFEvent() (pt uint8, rate int, ok bool)

PreferredDTMFEvent returns the telephone-event PT and clock rate to mirror in an answer (RFC 3264 offer/answer). When the remote advertised multiple telephone-event lines, the lowest PT wins for determinism. Returns (0, 0, false) when no telephone-event was offered.

func (*SDPMedia) PrimaryStream added in v0.12.0

func (m *SDPMedia) PrimaryStream() *RemoteAudioStream

PrimaryStream returns the audio section the scalar fields mirror, or nil when every offered audio section was rejected.

type SDPTextMedia added in v0.6.0

type SDPTextMedia struct {
	RemoteIP   string
	RemotePort int
	T140PT     uint8 // 0 if no t140/1000 advertised
	REDPT      uint8 // 0 if no red/1000 advertised
	Direction  string
}

SDPTextMedia holds parsed remote RTT parameters.

type SIPRECSignals added in v0.12.0

type SIPRECSignals struct {
	Required    bool // Require/Proxy-Require: siprec
	FeatureTag  bool // Contact carries +sip.src
	HasMetadata bool // the body carries an rs-metadata part
}

SIPRECSignals describes how an INVITE announced itself as a recording session. Conformance varies between SBCs, so each signal is reported separately and the caller decides what to do with a partial claim.

func DetectSIPREC added in v0.12.0

func DetectSIPREC(call *InboundCall) SIPRECSignals

DetectSIPREC classifies an inbound INVITE against the three RFC 7866 signals.

func (SIPRECSignals) Claimed added in v0.12.0

func (s SIPRECSignals) Claimed() bool

Claimed reports whether the INVITE claims to be a recording session at all.

type SIPRegisterTrunkView added in v0.9.0

type SIPRegisterTrunkView struct {
	RegistrarURI            string `json:"registrar_uri"`
	AOR                     string `json:"aor"`
	Username                string `json:"username,omitempty"`
	ContactURI              string `json:"contact_uri,omitempty"`
	RequestedExpiresSeconds int    `json:"requested_expires_seconds"`
	GrantedExpiresSeconds   int    `json:"granted_expires_seconds,omitempty"`
	LastRegisteredAt        string `json:"last_registered_at,omitempty"`
	NextRefreshAt           string `json:"next_refresh_at,omitempty"`
	CallID                  string `json:"call_id,omitempty"`
	CSeq                    uint32 `json:"cseq,omitempty"`
	// SourceAddress is the host:port the registrar's most recent response
	// actually came from. Initially set from the configured RegistrarURI;
	// updated to the real transport address (which may differ from the URI
	// when DNS / load-balancing fronts the registrar) on each 2xx response.
	// Used as the key for tagging inbound INVITEs back to this trunk.
	SourceAddress string `json:"source_address,omitempty"`
}

SIPRegisterTrunkView holds the sip_register-specific runtime fields. Credentials (password) are never exposed.

type SessionTimerParams

type SessionTimerParams struct {
	Interval  uint32 // Session-Expires delta-seconds
	Refresher string // "uac" or "uas"
	MinSE     uint32 // Min-SE value (0 = not present)
}

SessionTimerParams holds parsed Session-Expires / Min-SE values from a SIP request or response.

type SlotAction added in v0.12.0

type SlotAction uint8

SlotAction is what an answer does with one offered m= section.

const (
	// SlotAccept negotiates the section and carries media on it.
	SlotAccept SlotAction = iota
	// SlotReject emits a port-0 placeholder so the answer keeps the offer's
	// m-line count and ordering.
	SlotReject
	// SlotSkip leaves the section to the caller (the m=text path).
	SlotSkip
	// SlotOmit drops the section from the answer entirely. Only produced when
	// StrictMLines is off, for backward compatibility.
	SlotOmit
)

type SlotPlan added in v0.12.0

type SlotPlan struct {
	Index  int
	Media  string
	Proto  []string
	Action SlotAction
	Reason string // why a section was rejected; "" when accepted

	// AudioIdx indexes into SDPMedia.Audio, or -1 for non-audio sections.
	AudioIdx int

	Codec     codec.CodecType
	PT        uint8
	Direction string // the direction our answer must carry

	MID     string
	Label   string
	Content string
	Lang    string
}

SlotPlan is the decision for one offered m= section.

func AcceptedAudio added in v0.12.0

func AcceptedAudio(plans []SlotPlan) []SlotPlan

AcceptedAudio returns the plans that negotiated an audio section.

func PlanAnswer added in v0.12.0

func PlanAnswer(offer *SDPMedia, opts AnswerOptions) []SlotPlan

PlanAnswer decides, for every m= section in an offer, what the answer does with it. It is pure: no ports are allocated and no media is touched, so the caller can reject a section it cannot materialize and still emit a conformant answer.

RFC 3264 §6 governs the shape: one answer section per offered section, in the same order, with port 0 for anything refused.

type SlotState added in v0.12.0

type SlotState uint8

SlotState is the negotiation state of one m-line slot.

const (
	// SlotPending is offered by us but not yet answered by the peer.
	SlotPending SlotState = iota
	// SlotActive is negotiated and carrying (or ready to carry) media.
	SlotActive
	// SlotTombstone is disabled forever: the slot keeps its position and is
	// re-emitted with port 0 in every subsequent offer (RFC 3264 §8.2).
	SlotTombstone
)

func (SlotState) String added in v0.12.0

func (s SlotState) String() string

type Trunk added in v0.9.0

type Trunk interface {
	ID() string
	Type() TrunkType
	// AOR returns the canonical identity used to match outbound POST /v1/legs
	// `from` against this trunk. Empty when the trunk type has no AOR concept.
	AOR() string
	// PeerSocket returns the upstream peer's transport address used to tag
	// inbound INVITEs delivered by this trunk. Returns empty host with port 0
	// when not yet known.
	PeerSocket() (host string, port int, transport string)
	AppID() string
	Snapshot() TrunkView
	// Start launches the background lifecycle (REGISTER + refresh for
	// sip_register). Returns immediately.
	Start(ctx context.Context)
	// Stop tears the trunk down (de-register for sip_register). Best-effort;
	// honours ctx for timeout.
	Stop(ctx context.Context) error
}

Trunk is the abstract resource managed by the TrunkManager. Each concrete type (sip_register, future ip_ip, ...) implements this interface; lookups over the manager are type-agnostic.

type TrunkManager added in v0.9.0

type TrunkManager struct {
	// contains filtered or unexported fields
}

TrunkManager is the concurrent registry of all SIP trunks (sip_register today; ip_ip etc. in the future). Lookups are type-agnostic and indexed by id, canonical AOR, and upstream peer socket.

func NewTrunkManager added in v0.9.0

func NewTrunkManager() *TrunkManager

func (*TrunkManager) Add added in v0.9.0

func (m *TrunkManager) Add(t Trunk)

Add inserts a trunk, indexing it by id, AOR (when non-empty), and peer socket (when known).

func (*TrunkManager) Get added in v0.9.0

func (m *TrunkManager) Get(id string) Trunk

Get returns the trunk with the given id, or nil.

func (*TrunkManager) List added in v0.9.0

func (m *TrunkManager) List() []Trunk

List returns every registered trunk in indeterminate order.

func (*TrunkManager) LookupByAORUser added in v0.9.0

func (m *TrunkManager) LookupByAORUser(user string) Trunk

LookupByAORUser returns a trunk whose AOR user-part matches the given user string. Falls back when the caller only supplied a bare user on `from` (e.g. POST /v1/legs with `from: "alice"`) — the engine's publicHost won't match the trunk's upstream domain. Returns nil if no match; if multiple trunks have the same user, returns one arbitrarily.

func (*TrunkManager) LookupByFromAOR added in v0.9.0

func (m *TrunkManager) LookupByFromAOR(aor string) Trunk

LookupByFromAOR returns the trunk whose canonical AOR matches the given URI (after canonicalisation), or nil. The input may be a raw SIP URI string ("sip:alice@host") or already-canonical form.

func (*TrunkManager) LookupByPeerSocket added in v0.9.0

func (m *TrunkManager) LookupByPeerSocket(host string, port int) Trunk

LookupByPeerSocket returns the trunk whose upstream peer matches the given transport address, or nil. Matches first on full host:port, then on host alone (for cases where the peer's ephemeral source port differs from the configured registrar port).

func (*TrunkManager) RefreshIndex added in v0.9.0

func (m *TrunkManager) RefreshIndex(id string)

RefreshIndex re-indexes the trunk under its current AOR and peer socket. Call this after a trunk's PeerSocket() value becomes known (e.g. after the first successful REGISTER reveals the registrar's actual transport addr).

func (*TrunkManager) Remove added in v0.9.0

func (m *TrunkManager) Remove(id string) Trunk

Remove removes a trunk by id (does not Stop it). Returns the removed trunk, or nil if absent.

func (*TrunkManager) Shutdown added in v0.9.0

func (m *TrunkManager) Shutdown(ctx context.Context)

Shutdown stops every trunk in parallel, honouring ctx. After return the manager is empty.

type TrunkStatus added in v0.9.0

type TrunkStatus string

TrunkStatus is the runtime state of a trunk's upstream connection.

const (
	TrunkStatusRegistering   TrunkStatus = "registering"
	TrunkStatusActive        TrunkStatus = "active"
	TrunkStatusFailed        TrunkStatus = "failed"
	TrunkStatusUnregistering TrunkStatus = "unregistering"
	TrunkStatusExpired       TrunkStatus = "expired"
)

type TrunkType added in v0.9.0

type TrunkType string

TrunkType discriminates the upstream connection style of a SIP trunk. Only TrunkTypeSIPRegister is implemented today; other values are reserved for the OpenAPI/AsyncAPI contract and rejected at request time.

const (
	// TrunkTypeSIPRegister: VoiceBlender acts as a UAC and REGISTERs to an
	// upstream registrar, refreshing periodically; calls in either direction
	// flow through that registered identity.
	TrunkTypeSIPRegister TrunkType = "sip_register"

	// TrunkTypeIPIP: reserved for static-IP peering (no REGISTER). Not yet
	// implemented; the trunks handler returns 501 when requested.
	TrunkTypeIPIP TrunkType = "ip_ip"
)

type TrunkView added in v0.9.0

type TrunkView struct {
	ID        string      `json:"id"`
	Type      TrunkType   `json:"type"`
	AppID     string      `json:"app_id,omitempty"`
	Status    TrunkStatus `json:"status"`
	LastError string      `json:"last_error,omitempty"`
	CreatedAt string      `json:"created_at"`

	SIPRegister *SIPRegisterTrunkView `json:"sip_register,omitempty"`
	IPIP        *IPIPTrunkView        `json:"ip_ip,omitempty"`
}

TrunkView is the JSON-friendly snapshot of a trunk's current state. Each type populates its own sub-struct; only one of `SIPRegister` / `IPIP` is non-nil per snapshot.

type WhatsAppInviteOptions added in v0.5.0

type WhatsAppInviteOptions struct {
	// FromNumber is the business phone number in E.164. Used as the From
	// URI user (with leading '+'). When DigestUsername is empty it also
	// serves as the digest auth username (with the '+' stripped, per Meta).
	FromNumber     string
	DigestUsername string // optional override; defaults to FromNumber without '+'
	Password       string
	SDPOffer       []byte
	Headers        []sip.Header
}

type WhatsAppOutboundCall added in v0.5.0

type WhatsAppOutboundCall struct {
	Dialog    *sipgo.DialogClientSession
	AnswerSDP []byte
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL