Cisco UC Manager

[연재] SIP Normalization 의 이해 - 상. SIP Normalization Scripts 와 LUA

라인하트 2015. 9. 16. 17:30


글 싣는 순서

상. SIP Normalization 과 LUA 

중. 루아 스크립팅을 도와주는 도구들
하. 복잡한 스크립트 만들기   

 



시작하며
CUCM 8.5 버전부터 SIP Normalization을 지원하였지만, 엔지니어들이 LUA (루아) 스크립트에 대한 이해가 부족하여 거의 사용하지 못했습니다. 저도 
DX80 <--> CUCM <--> MCU 5300 시리즈간에 TLS / SRTP 연동을 하다보니 어쩔 수 없이 LUA 스크립트와 OpenSSL을 자세하게 공부하게 되었습니다. 이 연재가 완료되는 대로 영상회의 단말과 VCS 등에 대한 암호화를 따로 정리하도록 하겠습니다.  


 

SIP Transparency & Normalization 개요 
CUCM은 SIP 메세지를 조작할 수 있도록 다음 두 가지 기능을 지원합니다.

  • Normalization 
    네트워크로 수신 또는 송신되는 SIP 메세지의 헤더 및 내용을 변경합니다. SIP 및 SDP 헤더의 추가, 변경 및 삭제가 가능하며, 헤더 별 상세 내용의 변경도 가능합니다. 


  • Transparency 
    SIP 메세지가 하나의 Call Leg에서 다른 Call Leg로 변경없이 통과되도록 합니다. CUCM은 B2BUA이므로 SIP 메세지가 변경되지만 그대로 통과되도록 합니다.

Transparency 보다 SIP Normalization 구현을 중심으로 설명하겠습니다. 




LUA의 개요

루아 (LUA)는 달을 의미하는 포르투칼어로 가볍고 손쉽게 프로그래밍을 할 수 있도록 설계되었습니다. LUA는 누구나 목적에 상관없이 사용할 수 있으며, GNU 라이센스도 요구하지 않습니다. 루아는 프로그래머가 아닌 사람들이 사용하기 쉬운 파이썬 수준의 언어라고도 합니다. 





시스코 CUCM은 LUA를 SIP Normalization을 위한 Script 언어로 사용하고, 간단한 SIP Normalization을 위해 필요한 부분만을 중심으로 설명합니다.  개인적으로 공부해 보니 SIP Normalization 을 위한 부분만 공부한다고 하면 3시간 정도면 충분했습니다. 




SIP 헤더 변경을 위한 기본적인 LUA 구문의 이해

LUA는 프로그래머가 아닌 엔지니어도 쉽게 이해할 수 있는 스크립팅 언어이지만, 몇 가지 데이타 형식과 구문은 배워야 합니다. 처음부터 프로그래밍의 ABC 부터 시작하면 잠을 자기 시작하는 것이 엔지니어이므로 실제로 바로 써 먹을 수 있는 부분 부터 시작해 보겠습니다. ^^ 


기본 구문은 아래와 같습니다.  


M = {} 

function M.inbound_INVITE(msg) 

msg:removeHeader("Cisco-Guid") 

end

return M


LUA 스크립트는 세 부분으로 구성됩니다. 

  • Module Initialization 
    첫 라인에서 M이라 불리는 내용이 없는 루아 테이블을 생성하고, 모듈을 시작합니다. 

  • Message Handler Definitions
    메세지 핸들러를 정의하여 메세지를 분석하거나 변경합니다.    

  • Returning the Modules
    모듈의 값을 되돌려 주면서 종료합니다.  


각 라인별로 자세히 살펴보겠습니다. 

  • M = {} 
    M은 메세지 핸들러 (Message Handler)를 사용하기 위한 한 단위의 모듈을 의미합니다. { } 은테이블을 생성하기 위해 사용합니다. 예를 들면, t = { 1,2,3,4,5} 는 t라고 불리는 테이블에 데이터가 1,2,3,4,5 라는 데이터가 있다는 의미입니다.  

    따라서, 시스코 SIP Normalization을 위한 LUA 환경에서는 메세지 핸들러들을 정의하고 식별하기 위해 생성한 테이블 "M"을 사용합니다. 


  • function M.inbound_INVITE(msg)
     "inbound_INVITE" 메세지 핸들러는 수신되는 INVITE 메쏘드를 조작한다는 의미입니다. "msg"는 스크립트가 적용되는 SIP 메세지를 나타냅니다. "function M.inbound_INVITE(msg)" 의 의미는 수신되는 SIP INVITE의 메세지는 msg라는 변수에 저장된다는 것입니다.  


  • msg:removeHeader("Cisco-Guid") 
    "removeHeader(header)" 라는 SIP Message API를 호출합니다. msg:removeHeader("Cisco-Guid")는 msg 변수 안에 "Cisco-Guid"라는 헤더를 삭제한다는 의미입니다.  

  • end
    함수를 닫습니다. 


  • return M
    변경된 메세지를 루나 테이블로 되돌려 줍니다.  


기본 구문은 단순합니다. M 테이블을 생성하고 함수 및 API를 적용한 후 결과값을 M으로 다시 돌려주는 과정입니다.   


시스코 루아 환경에서 M= { } 라고 선언하면 M이라 불리우는 빈 테이블이 만들어지고, SIP 메세지 핸들러를 호출합니다. 수집되는 SIP 메세지 중에서 조작할 필요가 있는 SIP 메세지를 선택하기 위한 메세지 핸들러는 다음과 같이 결정됩니다. 


  • SIP Request (요청)을 선택하기 위한 구문 - M.<direction>_<SIP method>(msg)
    <direction>은  "inbound"와 "outbound" 중에서 선택하고, <SIP method>는 INVITE, ACK, UPDATE, BYE 등으로 SIP Request line에 표시된 메쏘드 이름입니다. 

    예를들어 발신하는 INVITE 메세지의 내용을 변경하기 위해서는 function M.outbound_INVITE(msg) 라고 선언하고, 수신하는 UPDATE 메세지의 내용을
     변경하기 위해서는 function M.inbound_INVITE(msg) 라고 선언합니다. 

  • SIP Response (응답)을 선택하기 위한 구문 - M.<direction>_<response code>_<method>
    <direction>은 
    "inbound"와 "outbound" 중에서 선택하고, <response code>는 183, 200 등으로 표시되는 세자리 숫자이며, <method>는 어떤 요청에 대한 응답인지를 알기 위해 선언합니다. 

    예를 들어 INVITE를 송신한 후에 수신되는 200 OK 응답에 대한 메세지를 변경하기 위해서는 M.inbound_200_INVITE(msg)라고 선언하며, 수신되는 INVITE에 대한 200 OK 발신 메세지에 대한 메세지를 변경하기 위해서는 M.outbound_200_INVITE 입니다. 


  • 모든 요청을 선택하기 위한 구문 (Wild Card)
    모든 요청에 적용할 경우 M.inbound_ANY(msg) 또는 M.outbound_ANY(msg) 사용합니다. 방향은 Wild Card를 사용할 수 없습니다.


  • 모든 응답을 선택하기 위한 구문 (Wild Card)
    모든 응답에 적용할 경우 M.inbound_ANY_ANY(msg) 또는 M.outbound_ANY_ANY(msg)를 사용합니다. 방향은 Wild Card를  사용할 수 없습니다. 

    <Response Code> 부분은 ANY대신 1XX 또는 4XX등으로 X를 이용하면서 구체적인 메쏘드 응답을 지정할 수 있습니다. 예를 들면, M.inbound_18X_INVITE (msg) 또는 M.outbound_ANY_INVITE (msg)등이 가능합니다. 그러나 M.inbound_18X_ANY (msg) 와 같은 구문은 유효하지 않습니다. 


  

여기서 메세지 핸들러에 대한 구문은 case-seneitive 합니다. 대소문자 구분을 잘못하면 동작하지 않습니다. 미세하게 겹쳐지는 메세지 핸들러의 경우에는 logest-match를 기준으로 합니다. 예를들면, M.inbound_183_INVITE(msg)와 M.inbound_ANY_ANY 가 있을 경우 전자의 구문이 실행됩니다.  




LUA 스크립트 작성을 위한 필수 참조 자료
루아 스크립트를 작성할 때 기본 구문에 따라 작성한 후에 SIP 메세지를 원하는 대로 조작하기 위해서는 다양한 API가 필요합니다. SIP Normalization 을 위한 LUA API를 모아놓은 "
  Developer Guide for SIP Transparency and Normalization " 자료를 참조하면 좋은 스크립트를 만들 수 있습니다.  


Developer Guide for SIP Transparency and Normalization.pdf


이 자료에서는 정의된 다수의 API 세트를 소개하고 있으니 참조하시기 바랍니다.

  • SIP Messages APIs
  • SDP APIs
  • SIP Pass Though APIs
  • SIP Utility APIs
  • SIP URI APIs
  • Trace APIs
  • Scrip Parameters APIs 

가장 많이 사용하는 API 세트는 SIP Messages 세트이니 스크립트를 만들기 전에 한번즈음 살펴보시면 큰 도움이 됩니다.  



자주 쓰는 SIP 헤더 값 변경 시나리오 - Contact 헤더의 IP 주소를 도메인 네임으로 변경하기
가장 간단하면서 가장 많이 사용하는 예제를 만들어 보겠습니다. 아래 예제는 CUCM에서 발신하는 INVITE SIP Request의 Contact 헤더가 IP로 된 것을 도메인 네임으로 변경하는 합니다. 


M= { }

function M.outbound_INVITE(msg)

local cont = msg:getHeader("Contact")

local changed = string.gsub(cont,"@192.168.0.4","@cucmk.cisco.lab")

msg:modifyHeader("Contact",changed)

end

return M


프로그래밍을 조금이라도 해보신 분들은 대충 이해하리라 짐작됩니다. 잘 모르는 분들을 위해 function 구문안의 내용에 대해 살펴보겠습니다. 

    • function M.outbound_INVITE(msg)
      CUCM에서 발신되는 INVITE 메세지 핸들러를 호출합니다. 이 스크립트가 적용되는 트렁크로 발신되는 모든 SIP INVITE 요청은 기본적으로 검사하게 됩니다. SIP INVITE 요청내의 모든 내용이 msg라는 변수에 포함됩니다.

      여기서 M.outbound_INVITE 메세지핸들러에 의한 msg 변수의 값은 다음과 같이 INVITE 요청의 모든 내용입니다. 

      INVITE sip:8001;reg=40040001@192.168.0.133:5061;transport=tls SIP/2.0 

      Via: SIP/2.0/TLS 192.168.0.21:5061;egress-zone=DefaultSubZone;branch=z9hG4bKd417f2f1292f544d7bdaedf71d24041f7918.6ea5b8737a7cc73deb7c67816a7c5125;proxy-call-id=3940a92a-ef67-4fde-beda-97f1208d9cc0;rport "

      Via: SIP/2.0/TLS 192.168.0.4:5061;branch=z9hG4bK1bd6384ba952;received=192.168.0.4;ingress-zone=cucm 

      Call-ID: a5b8ce80-5f61b29a-1bd6-400a8c0@192.168.0.4 

      CSeq: 101 INVITE 

      Remote-Party-ID: <sip:1009@cisco.lab>;party=calling;screen=yes;privacy=off 

      Contact: <sip:1009@192.168.0.4:5061;transport=tls>;video;audio;bfcp 

      From: <sip:1009@cisco.lab>;tag=16128~80f34be1-735a-462d-8134-f35ad4f0eaf0-30250889 

      To: <sip:8001@vcs.cisco.lab> 

      Max-Forwards: 68 

      Record-Route: <sip:proxy-call-id=3940a92a-ef67-4fde-beda-97f1208d9cc0@192.168.0.21:5061;transport=tls;lr> 

      Record-Route: <sip:proxy-call-id=3940a92a-ef67-4fde-beda-97f1208d9cc0@192.168.0.21:5061;transport=tls;lr>

      Allow: INVITE,OPTIONS,INFO,BYE,CANCEL,ACK,PRACK,UPDATE,REFER,SUBSCRIBE,NOTIFY 

      User-Agent: Cisco-CUCM10.5 

      Expires: 180

      Date: Mon, 14 Sep 2015 11:42:18 GMT 

      Supported: timer,resource-priority,replaces,X-cisco-srtp-fallback,X-cisco-original-called 

      Session-Expires: 1800 

      Min-SE: 1800 

      P-Asserted-Identity: <sip:1009@cisco.lab> 

      Allow-Events: presence,kpml

      X-TAATag: d6eb0e0e-b575-471d-8f31-26facb91c7f6 

      Call-Info: <urn:x-cisco-remotecc:callinfo>; security= Unknown; gci= 1-13036; isVoip, <sip:192.168.0.4:5061>;method="NOTIFY;Event=telephone-event;Duration=500" 

      Call-Info: <urn:x-cisco-remotecc:callinfo>;x-cisco-video-traffic-class=DESKTOP;x-cisco-qos-tcl=true

      Cisco-Guid: 2780352128-0000065536-0000000029-0067152064 

      Content-Length: 0 

       

    • local cont = msg:getHeader("Contact")
      "local" 은 지역변수로 현재의 메세지 핸들러 내에서만 유효한 변수입니다. 즉, 다른 메세지 핸들러에서 local을 사용하면 전혀 다른 변수라는 의미입니다. 지역변수의 반대는 전역변수로 전체 스크립트 내에서 유일한 변수입니다. "local cont" 는 "cont" 라는 지역변수를 하나 선언하고, cont는 "=" 이하에서 결정된 값을 나타내게 됩니다.

      getHeader(header-name) API는 헤더의 내용을 값으로 돌려줍니다. msg:getHeader("Contact") 의 의미는 위의 "msg" 변수의 내용 중에 "Contact" 헤더의 값을 돌려달라는 의미입니다. 그 값은 "<sip:1009@192.168.0.4:5061;transport=tls>;video;audio;bfcp" 입니다. 

      이제 이 라인의 의미는 "cont = <sip:1009@192.168.0.4:5061;transport=tls>;video;audio;bfcp" 입니다. "cont"변수에 저장된 값에서 필요한 내용을 변경할 것입니다.



    • local changed = string.gsub(cont,"@192.168.0.4","@cucmk.cisco.lab")
      string. 으로 시작하는 함수는 문자열에서 특정값을 선택하여 변환하기 위해 사용합니다.  여기서 사용된 string.gsub(s,i,j) 는 문자열 s 에서 i 값을 j값으로 변경하기 위해 사용합니다. 

      string.gsub(cont,"@192.168.0.4","@cucmk.cisco.lab") 는  전 줄에서 정의한 "cont"의 문자열에서 "@192.168.0.4"라는 문자열이 있다면 "@cucmk.cisco.lab"으로 변환하라는 의미입니다. 따라서 "<sip:1009@192.168.0.4:5061;transport=tls>;video;audio;bfcp"라는 값은 <sip:1009@cucmk.cisco.lab:5061;transport=tls>;video;audio;bfcp"으로 변경되어 지역변수 "changed"에 저장됩니다.

    • msg:modifyHeader("Contact",changed)
      처음 보았던 removeHeader API와 비슷한 모양새를 가진 modifyHeader(header-name, header-value) 는 헤더 값을 변경하는 API입니다. modifyHeader("Contact",changed) 로 적용하면 Contact 헤더에는 changed 지역변수의 값이 적용되어 IP 주소가 아닌 도메인 주소로 변경됩니다. 

      "Contact : <sip:1009@cucmk.cisco.lab:5061;transport=tls>;video;audio;bfcp" 이 값은 다시 msg의 값으로 전환됩니다. 

    • return M
      이제 M 테이블로 변경값이 리턴됩니다. 


      INVITE sip:8001;reg=40040001@192.168.0.133:5061;transport=tls SIP/2.0 

      Via: SIP/2.0/TLS 192.168.0.21:5061;egress-zone=DefaultSubZone;branch=z9hG4bKd417f2f1292f544d7bdaedf71d24041f7918.6ea5b8737a7cc73deb7c67816a7c5125;proxy-call-id=3940a92a-ef67-4fde-beda-97f1208d9cc0;rport "

      Via: SIP/2.0/TLS 192.168.0.4:5061;branch=z9hG4bK1bd6384ba952;received=192.168.0.4;ingress-zone=cucm 

      Call-ID: a5b8ce80-5f61b29a-1bd6-400a8c0@192.168.0.4 

      CSeq: 101 INVITE 

      Remote-Party-ID: <sip:1009@cisco.lab>;party=calling;screen=yes;privacy=off 

      Contact: <sip:1009@cucmk.cisco.lab:5061;transport=tls>;video;audio;bfcp 

      From: <sip:1009@cisco.lab>;tag=16128~80f34be1-735a-462d-8134-f35ad4f0eaf0-30250889 

      To: <sip:8001@vcs.cisco.lab> 

      Max-Forwards: 68 

      Record-Route: <sip:proxy-call-id=3940a92a-ef67-4fde-beda-97f1208d9cc0@192.168.0.21:5061;transport=tls;lr> 

      Record-Route: <sip:proxy-call-id=3940a92a-ef67-4fde-beda-97f1208d9cc0@192.168.0.21:5061;transport=tls;lr>

      Allow: INVITE,OPTIONS,INFO,BYE,CANCEL,ACK,PRACK,UPDATE,REFER,SUBSCRIBE,NOTIFY 

      User-Agent: Cisco-CUCM10.5 

      Expires: 180

      Date: Mon, 14 Sep 2015 11:42:18 GMT 

      Supported: timer,resource-priority,replaces,X-cisco-srtp-fallback,X-cisco-original-called 

      Session-Expires: 1800 

      Min-SE: 1800 

      P-Asserted-Identity: <sip:1009@cisco.lab> 

      Allow-Events: presence,kpml

      X-TAATag: d6eb0e0e-b575-471d-8f31-26facb91c7f6 

      Call-Info: <urn:x-cisco-remotecc:callinfo>; security= Unknown; gci= 1-13036; isVoip, <sip:192.168.0.4:5061>;method="NOTIFY;Event=telephone-event;Duration=500" 

      Call-Info: <urn:x-cisco-remotecc:callinfo>;x-cisco-video-traffic-class=DESKTOP;x-cisco-qos-tcl=true

      Cisco-Guid: 2780352128-0000065536-0000000029-0067152064 

      Content-Length: 0 

       

이 스크립트를 CUCM에 적용하여 테스트해 보겠습니다.   



CUCM에 LUA 스크립트 등록하기
CUCM의 메뉴바에서 "Device >> Device Settings >> SIP Normalization Script"를 선택합니다. 




"Find" 버튼을 선택하면 사전 설정된 스크립트를 확인할 수 있습니다. "Add New" 버튼을 선택하여 새로운 스크립트를 등록합니다. 

 



"Name" 필드에는 스크립트의 이름을 넣고, "Content" 항목에 직접 작성한 스크립트를 복사해서 넣습니다. 





CUCM Trunk에 적용하기
CUCM 트렁크 설정에서 "SIP Information" 박스에서 Normalization Script 부분에서 방금 생성한 SIP Normalization Script 를 선택합니다. 이 트렁크를 이용하는 모든 SIP 메세지는 이 스크립트에 적용을 받게 됩니다.  




스크립트 적용을 위해 "Reset" 버튼을 클릭한 후에 새로운 창이 뜨면 "Reset" 버튼을 클릭합니다. 






마치며
LUA 스크립트를 이용하여 간단하게 스크립트를 작성하고, CUCM에 적용하는 것까지 살펴보았습니다. 실제 스크립트를 짜다보면 글자가 틀리거나 구문이 틀리는 경우가 자주 있지만, CUCM은 구문이 틀리더라도 오류를 발생시키지 않습니다. 따라서, CUCM에 입력하기 전에 완벽하게 작성을 해야 합니다. 
다음 글에서는 오류없이 구문을 작성할 수 있는 다양한 툴과 방법에 대해 살펴보겠습니다. 


개인적으로 LUA를 대충 공부하고 적용하는 데 2-3시간 정도면 충분햇습니다. 마음이 급하다고 무조건 따라만 하지말고 차근차근 읽어보면서 구문을 이해한 후에 적용하셔야 실력도 늘고 장애처리를 하는 수고도 덜 수 있습니다. 



참고 자료 : 위키백과의 루아(LUA) 



라인하트 유씨누스(UCnus) (CCIEV #18487)  --------------------------------------
ucwana@gmail.com (라인하트의 구글 이메일) 
http://twitter.com/nexpertnet (넥스퍼트 블로그의 트위터, 최신 업데이트 정보 및 공지 사항) 
http://groups.google.com/group/cciev (시스코 UC를 공부하는 사람들이 모인 구글 구룹스) 
http://groups.google.com/group/ucforum (UC를 공부하는 사람들이 모인 구글 구룹스) 
세상을 이롭게 하는 기술을 지향합니다. ________________________________________________________