among my many other incredibly autistic hobbies, I have been known to enjoy toying with telephony solutions - usually software-based ones. I wanted to get a physical PBX to play with, though, partly as a way to hook up some modems, and partly just because I like having blinkenlights in my home server rack.
I ended up getting an Avaya IP Office IP500v2 control unit for dirt cheap, with a System SD that had a decent set of licenses on it, including some third-party SIP endpoint licenses. I didn't have any Avaya phones, but I did have a few Yealinks and a Cisco CP-8831-3PCC, so that IP Office box has been the main thing running my home telephony.
a few weeks ago I found a bulk lot of Polycom VVX200/300 phones (also for dirt cheap, I do love a bargain), and so I hooked a couple of them up to the IP Office, only to discover that the way these Polycom phones handle auto-answer (for intercom/paging over SIP) is very much incompatible with all of the available options for auto-answer on third-party SIP endpoints in Avaya IP Office.
for auto-answer to work, these Polycom phones expect an SIP Alert-Info header in a specific format - something like info=Ring Answer;delay=0 (to ring exactly once, then answer on speakerphone), or info=Answer Mute;delay=0 (to silently answer on speakerphone, with the device's microphone muted). the Avaya IP Office has two options for 3rd party endpoint auto-answer - either Call-Info: <sip:...>; answer-after=0, or an RFC 5373 compliant Answer-Mode header.
a friend (hi Brendan!) jokingly suggested I set up a SIP proxy server to rewrite the auto-answer headers on the fly. that sounded like it'd work, so I started playing with Kamailio. and, as it turns out, rewriting the headers on the INVITE request is both (a) pretty damn easy in Kamailio; and (b) exactly what I needed.
and thus, here's my working kamailio.cfg:
#!KAMAILIO
#!define FLT_NATS 5
#!define FLB_NATB 6
debug=2
memdbg=5
memlog=5
log_facility=LOG_LOCAL0
log_prefix="[$ci] "
fork=yes
children=8
disable_tcp=yes
port=5060
loadmodule "kex.so"
loadmodule "corex.so"
loadmodule "xlog.so"
loadmodule "ctl.so"
loadmodule "cfg_rpc.so"
loadmodule "debugger.so"
loadmodule "tm.so"
loadmodule "tmx.so"
loadmodule "sl.so"
loadmodule "rr.so"
loadmodule "pv.so"
loadmodule "dlgs.so"
loadmodule "htable.so"
loadmodule "sanity.so"
loadmodule "maxfwd.so"
loadmodule "textops.so"
loadmodule "textopsx.so"
loadmodule "usrloc.so"
loadmodule "registrar.so"
loadmodule "siputils.so"
loadmodule "nathelper.so"
loadmodule "rtpengine.so"
modparam("htable", "htable", "client_ua=>size=4")
modparam("tm", "failure_reply_mode", 3)
modparam("tm", "fr_timer", 30000)
modparam("tm", "fr_inv_timer", 120000)
modparam("rr", "enable_full_lr", 1)
modparam("rr", "append_fromtag", 0)
modparam("dlgs", "timer_interval", 10)
modparam("dlgs", "init_lifetime", 180)
modparam("dlgs", "active_lifetime", 7200)
modparam("dlgs", "finish_lifetime", 10)
modparam("registrar", "method_filtering", 1)
modparam("registrar", "max_expires", 3600)
modparam("registrar", "gruu_enabled", 0)
modparam("registrar", "use_path", 1)
modparam("registrar", "path_mode", 0)
modparam("nathelper|registrar", "received_avp", "$avp(RECEIVED)")
modparam("rtpengine", "rtpengine_sock", "udp:127.0.0.1:2223")
route {
route(REQINIT);
route(NATDETECT);
if (is_method("CANCEL")) {
dlgs_update();
if (t_check_trans()) {
route(RELAY);
}
exit;
}
if (!is_method("ACK")) {
if (t_precheck_trans()) {
t_check_trans();
exit;
}
t_check_trans();
}
route(WITHINDLG);
# after this point we are handling direct (non-dialog) requests only
xinfo("request: $hdr(CSeq) from $fu ($si:$sp) to $ru");
route(REQ_MANGLE);
remove_hf("Route");
if (is_method("INVITE|SUBSCRIBE|REFER")) {
record_route();
}
if (is_method("INVITE")) {
dlgs_init("$fu", "$tu", "srcip=$si");
}
route(REGISTRAR);
route(LOCATION);
}
route[REQINIT] {
set_reply_no_connect();
force_rport();
# silently drop scanners
if ($ua =~ "friendly|scanner|sipcli|sipvicious|VaxSIPUserAgent|pplsip") {
exit;
}
# filter too old messages
if (!mf_process_maxfwd_header("10")) {
xlog("Too many hops on request from $si:$sp\n");
sl_send_reply("483", "Too Many Hops");
break;
}
# drop malformed SIP messages
if (!sanity_check("17895", "7")) {
xlog("Malformed SIP request from $si:$sp\n");
exit;
}
# directly respond to keepalives
if (is_method("OPTIONS") && uri == myself && $rU == $null) {
sl_send_reply("200", "Keepalive");
exit;
}
return;
}
route[NATDETECT] {
#!ifdef USE_NAT
if (nat_uac_test("19")) {
if (is_method("REGISTER")) {
fix_nated_register();
} else {
if (is_first_hop()) {
set_contact_alias();
}
}
setflag(FLT_NATS);
}
#!endif
return;
}
route[NATMANAGE] {
#!ifdef USE_NAT
if (is_request()) {
if (has_totag()) {
if (check_route_param("nat=yes")) {
setbflag(FLB_NATB);
}
}
}
if (!(isflagset(FLT_NATS) || isbflagset(FLB_NATB))) {
return;
}
if (is_request()) {
if (!has_totag()) {
if (t_is_branch_route()) {
add_rr_param(";nat=yes");
}
}
}
if (is_reply()) {
if (isbflagset(FLB_NATB)) {
if (is_first_hop()) {
set_contact_alias();
}
}
}
if (isbflagset(FLB_NATB)) {
# no connect message in a dialog involving NAT traversal
if (is_request()) {
if (has_totag()) {
set_forward_no_connect();
}
}
}
#!endif
return;
}
route[REGISTRAR] {
if (!is_method("REGISTER")) {
return;
}
#!ifdef USE_NAT
if (isflagset(FLT_NATS)) {
setbflag(FLB_NATB);
}
#!endif
if (!save("location", "6")) {
send_reply_error();
}
xinfo("register: $fu User-Agent is $ua");
$sht(client_ua=>$fU) = $ua;
route(RELAY);
exit;
}
route[LOCATION] {
lookup("location");
route(RELAY);
exit;
}
route[WITHINDLG] {
if (!has_totag()) {
return;
}
if (loose_route()) {
dlgs_update();
#!ifdef USE_NAT
if (!isdsturiset()) {
handle_ruri_alias();
}
#!endif
if (is_method("NOTIFY|REFER")) {
record_route();
} else if (is_method("ACK")) {
route(NATMANAGE);
}
route(RELAY);
exit;
}
if (is_method("ACK")) {
if (t_check_trans()) {
route(RELAY);
}
exit;
}
sl_send_reply("404", "Not here");
exit;
}
route[RELAY] {
if (has_body("application/sdp")) {
rtpengine_manage("RTP codec-mask-all codec-transcode-opus codec-transcode-PCMA codec-transcode-speex SIP-source-address replace-origin replace-session-connection");
}
if (is_method("INVITE|BYE|SUBSCRIBE|UPDATE")) {
if (!t_is_set("branch_route")) {
t_on_branch("MANAGE_BRANCH");
}
}
if (is_method("INVITE|SUBSCRIBE|UPDATE")) {
if (!t_is_set("onreply_route")) {
t_on_reply("MANAGE_REPLY");
}
}
if (is_method("INVITE")) {
if (!t_is_set("failure_route")) {
t_on_failure("MANAGE_FAILURE");
}
}
if (!t_relay()) {
send_reply_error();
};
exit;
}
# SIP request mangling
route[REQ_MANGLE] {
route(REQ_MANGLE_IPO);
return;
}
# Avaya IP Office compat
route[REQ_MANGLE_IPO] {
if (is_method("INVITE")) {
# transform RFC5373 auto-answer into a more broadly compatible header set
if (remove_hf_match("Answer-Mode", "re", "^Auto")) {
remove_hf("Call-Info");
remove_hf("Alert-Info");
if (str_ifind($sht(client_ua=>$tU), "polycom")) {
xinfo("REQ_MANGLE_IPO: rewriting auto-answer for Poly UC towards $tu");
append_hf("Alert-Info: info=Ring Answer;delay=0\r\n");
} else {
xinfo("REQ_MANGLE_IPO: rewriting auto-answer towards $tu ($sht(client_ua=>$tU))");
append_hf("Call-Info: <$tu>; answer-after=0\r\n");
append_hf("Alert-Info: auto answer\r\n");
}
}
}
msg_apply_changes();
return;
}
branch_route[MANAGE_BRANCH] {
xdbg("new branch [$T_branch_idx] to $ru\n");
route(NATMANAGE);
return;
}
reply_route {
if (!sanity_check("17604", "6")) {
xlog("Malformed SIP response from $si:$sp\n");
drop;
}
return;
}
onreply_route[MANAGE_REPLY] {
xdbg("got reply from $si:$sp -> $tu");
if (status =~ "[12][0-9][0-9]") {
route(NATMANAGE);
}
if (has_body("application/sdp")) {
rtpengine_manage();
}
return;
}
failure_route[MANAGE_FAILURE] {
route(NATMANAGE);
if (has_body("application/sdp")) {
rtpengine_manage();
}
if (t_is_canceled()) {
exit;
}
return;
}note that this config does not do any authentication, flood protection, ratelimiting, or any of that stuff that you'd actually want in a production deployment. this is running inside my home network, so I don't really care about that.
it also assumes that you set the 3rd party endpoint auto-answer for the extension you're logging into with the Polycom phones to RFC5373 mode.
I only looked at the Kamailio docs just enough to get this to work - I am by no means an expert at configuring it - so there's a very large chance that parts of that config aren't actually necessary. but it works, and that's all that matters for me right now :P