Skip to content
LIVE FEED

BlogGiai đoạn 2 chi tiết: NAT/Media với RTPengine, Dispatcher, Drouting và B2BUA thực chiến

Giai đoạn 2 — Định tuyến nâng cao, NAT/Media, và B2BUA (Chi tiết đầy đủ)

Điều kiện tiên quyết: đã hoàn thành Giai đoạn 1 — hiểu route/failure_route/onreply_route, đã có registrar+proxy chạy được với REGISTER/INVITE/BYE/CANCEL cơ bản. Tài liệu này mở rộng trực tiếp trên file cấu hình của Giai đoạn 1.

1. NAT — vấn đề thật sự là gì

Phần lớn thiết bị VoIP nằm sau NAT (router gia đình, firewall doanh nghiệp). NAT gây ra hai vấn đề tách biệt mà người mới hay gộp làm một:

Loại NATVấn đềAi xử lý
Signaling NATContact header trong REGISTER/INVITE chứa địa chỉ IP riêng (192.168.x.x) — không ai bên ngoài gọi lại đượcOpenSIPS (module nathelper)
Media NATSDP chứa IP riêng cho RTP — hai bên không thể gửi audio trực tiếp cho nhau qua NATMedia relay riêng (RTPengine) — OpenSIPS không tự xử lý được media

1.1 Phát hiện NAT

loadmodule "nathelper.so"
modparam("nathelper", "received_avp", "$avp(received)")

Trong route, phát hiện UA có nằm sau NAT hay không bằng cách so sánh IP nguồn thật của gói tin ($si) với IP ghi trong Contact:

if (nat_uac_test(3)) {
    setflag(NAT_FLAG);
    fix_nated_register();   # sửa lại Contact trong REGISTER cho đúng IP/port công khai
}

fix_nated_register() dùng cho REGISTER; với INVITE/response dùng fix_nated_contact() tương ứng. Core function force_rport() bắt buộc server gửi response về đúng port nguồn thay vì port ghi trong Via — luôn nên gọi khi phát hiện NAT.

1.2 Media NAT — bắt buộc phải có RTPengine

RTPengine là một daemon chạy độc lập (không phải module chạy trong tiến trình OpenSIPS) — OpenSIPS chỉ gửi lệnh điều khiển qua một control socket, còn RTPengine mới là thứ thực sự nhận/gửi lại luồng RTP giữa hai bên.

loadmodule "rtpengine.so"
modparam("rtpengine", "rtpengine_sock", "udp:127.0.0.1:2223")

Ba hàm cốt lõi và đúng vị trí gọi trong flow:

route {
    ...
    if (is_method("INVITE") && !has_totag()) {
        rtpengine_offer();      # gọi TRƯỚC khi relay INVITE đi — sửa SDP offer
        ...
        route(RELAY);
    }
}

onreply_route {
    if (t_check_status("200") && $rm=="INVITE") {
        rtpengine_answer();     # gọi khi response 200 OK quay về — sửa SDP answer
    }
}

route {
    if (has_totag() && is_method("BYE")) {
        rtpengine_delete();     # giải phóng phiên RTP khi cuộc gọi kết thúc
    }
}

Ghi nhớ nguyên tắc: offer sửa lúc đi, answer sửa lúc về, delete lúc BYE — đúng với mô hình offer/answer của SDP đã học ở Giai đoạn 0.

2. TLS cho signaling, SRTP cho media

2.1 TLS

loadmodule "tls_mgm.so"
listen=tls:eth0:5061
modparam("tls_mgm", "certificate", "/etc/opensips/tls/cert.pem")
modparam("tls_mgm", "private_key", "/etc/opensips/tls/key.pem")

Softphone kết nối qua sips: URI hoặc transport=tls thay vì UDP thường — bảo vệ nội dung SIP header (bao gồm cả các trường P-header từ Giai đoạn 0 nếu tích hợp IMS) khỏi bị nghe lén trên đường truyền.

2.2 SRTP

OpenSIPS tự nó không mã hoá RTP — việc chuyển đổi RTP ⇄ SRTP giao cho RTPengine xử lý bằng flag truyền vào rtpengine_offer():

rtpengine_offer("SRTP-accept SRTP-offer");

Đây chính là lúc RTPengine đóng vai trò transcoding bảo mật: một bên nói RTP thường, một bên nói SRTP, RTPengine dịch giữa hai bên.

3. Dispatcher — cân bằng tải nhiều gateway

3.1 Cấu hình

loadmodule "dispatcher.so"
modparam("dispatcher", "list_file", "/etc/opensips/dispatcher.list")
modparam("dispatcher", "ds_ping_interval", 30)
modparam("dispatcher", "ds_ping_method", "OPTIONS")

File dispatcher.list (mỗi dòng: setid ip:port):

1 sip:192.168.10.10:5060
1 sip:192.168.10.11:5060

3.2 Dùng trong script

route {
    ...
    if (is_method("INVITE") && !has_totag()) {
        if (!ds_select_dst("1", "4")) {   # set 1, thuật toán 4 = round-robin theo Call-ID
            sl_send_reply("503", "No Gateway Available");
            exit;
        }
        route(RELAY);
    }
}

failure_route[GW_FAILOVER] {
    if (t_check_status("408|5[0-9][0-9]")) {
        if (ds_next_dst()) {     # thử gateway tiếp theo trong cùng set
            route(RELAY);
            exit;
        }
    }
}

ds_ping_interval + ds_ping_method cho OpenSIPS tự động OPTIONS-probe từng gateway định kỳ — gateway không trả lời sẽ tự động bị đánh dấu INACTIVE và bị loại khỏi vòng chọn cho tới khi hồi phục.

4. Drouting (Dynamic Routing) — định tuyến theo prefix/LCR

Khi có nhiều nhà cung cấp PSTN với biểu giá khác nhau theo đầu số, dispatcher (chỉ biết load-balance phẳng) không đủ — cần drouting để chọn gateway theo tiền tố số bị gọi (Least Cost Routing).

loadmodule "drouting.so"
modparam("drouting", "db_url", "mysql://opensips:opensipsrw@localhost/opensips")

Cấu hình chính nằm trong 2 bảng: dr_gateways (danh sách gateway) và dr_rules (ánh xạ prefix → nhóm gateway, có ưu tiên). Trong script:

if (!do_routing("0")) {   # "0" là group id định nghĩa trong dr_rules
    sl_send_reply("404", "No Route");
    exit;
}

Ở mức Giai đoạn 2 chỉ cần nắm khái niệm và biết khi nào dùng drouting thay vì dispatcher (drouting khi cần định tuyến theo số, dispatcher khi chỉ cần load-balance phẳng một nhóm gateway ngang hàng) — đi sâu cấu hình LCR thật thuộc phạm vi vận hành production (Giai đoạn 3).

5. B2BUA thực chiến — kịch bản ẩn danh người gọi

Nhắc lại kiến trúc đã học: b2b_entities (tầng UAS/UAC thấp) + b2b_logic (tầng kịch bản). Đây là ví dụ minh hoạ khi nào bắt buộc dùng B2BUA thay vì chỉ sửa header trong proxy: nếu chỉ đổi From header ở chế độ proxy, phía callee vẫn có thể nhìn thấy identity thật qua P-Asserted-Identity hoặc qua chính dialog gốc; B2BUA tạo hẳn hai Call-ID độc lập nên identity thật của Leg A không bao giờ lộ sang Leg B.

loadmodule "b2b_entities.so"
loadmodule "b2b_logic.so"
modparam("b2b_logic", "script_req_route", "B2B_REQUEST")
modparam("b2b_logic", "script_reply_route", "B2B_REPLY")

route {
    ...
    if (is_method("INVITE") && !has_totag() && $ua =~ "anonymous-out") {
        # UE yêu cầu ẩn danh (ví dụ qua header Privacy: id, hoặc quy ước prefix riêng)
        b2b_init_request("anon_call");
        exit;
    }
}

route[B2B_REQUEST] {
    # Route riêng dành cho request thuộc phiên B2BUA — KHÔNG chạy qua route{} thông thường
    if ($b2b_logic.entity(0) == $null) {
        # tạo Leg B (UAC) với From đã bị thay bằng danh tính ẩn danh
        $var(anon_from) = "sip:anonymous@" + $fd;
        b2b_client_new("$var(anon_from)", $rU@$rd, "", "", "B2B_REPLY");
    }
    b2b_pass_request();
}

route[B2B_REPLY] {
    b2b_handle_reply();
}

Lưu ý quan trọng: đây là mã minh hoạ để thấy đúng luồng gọi hàm (b2b_init_request → route riêng → b2b_client_new/b2b_pass_request/b2b_handle_reply) — cú pháp tham số chính xác của từng hàm lệch giữa các phiên bản OpenSIPS khá nhiều (module b2b_logic đã trải qua vài lần tái cấu trúc API). Trước khi chạy thật, đối chiếu README đúng version đang cài, đừng copy nguyên khối trên vào production.

6. Presence — SUBSCRIBE/NOTIFY

loadmodule "presence.so"
modparam("presence", "db_url", "mysql://opensips:opensipsrw@localhost/opensips")

Về bản chất, module này tự xử lý toàn bộ vòng đời SUBSCRIBE/NOTIFY (lưu watcher, gửi NOTIFY khi presentity đổi trạng thái) — trong route bạn chỉ cần đảm bảo SUBSCRIBE/PUBLISH được định tuyến tới đúng chỗ, không cần tự viết logic:

if (is_method("PUBLISH") || is_method("SUBSCRIBE")) {
    route(RELAY);   # để module presence tự xử lý phía sau
    exit;
}

7. Dialplan — chuẩn hoá số điện thoại

loadmodule "dialplan.so"
modparam("dialplan", "db_url", "mysql://opensips:opensipsrw@localhost/opensips")

Bảng dialplan_rules ánh xạ pattern → replacement bằng regex. Ví dụ chuẩn hoá số nội địa Việt Nam về dạng E.164: | dpid | match_exp | subst_exp | |—|—|—| | 1 | ^0([0-9]{9})$ | \+84\1 |

Gọi trong script trước khi định tuyến ra ngoài:

if (!dp_translate("1", "$rU")) {
    sl_send_reply("404", "Invalid Number Format");
    exit;
}

8. File cấu hình mở rộng — nối tiếp Giai đoạn 1

Thêm vào file opensips.cfg của Giai đoạn 1 (giữ nguyên phần REGISTER/dialog/acc đã có), chèn thêm đoạn xử lý cho INVITE đi ra ngoài qua gateway có NAT + RTPengine + dispatcher:

# --- Thêm vào phần Modules Section ---
loadmodule "nathelper.so"
modparam("nathelper", "received_avp", "$avp(received)")

loadmodule "rtpengine.so"
modparam("rtpengine", "rtpengine_sock", "udp:127.0.0.1:2223")

loadmodule "dispatcher.so"
modparam("dispatcher", "list_file", "/etc/opensips/dispatcher.list")
modparam("dispatcher", "ds_ping_interval", 30)

# --- Trong route{}, sau bước "REGISTER" và trước bước "INVITE mới" của Giai đoạn 1 ---
if (is_method("REGISTER")) {
    if (nat_uac_test(3)) {
        fix_nated_register();
    }
    ... # phần auth + save() giữ nguyên như Giai đoạn 1
}

if (is_method("INVITE") && !has_totag()) {
    record_route();
    create_dialog();

    if (nat_uac_test(3)) {
        setflag(NAT_FLAG);
        fix_nated_contact();
    }
    rtpengine_offer();

    if (!lookup("location")) {
        # không tìm thấy user nội bộ -> thử định tuyến ra gateway PSTN qua dispatcher
        if (!ds_select_dst("1", "4")) {
            sl_send_reply("404", "Not Found");
            exit;
        }
    }

    do_accounting("log");
    route(RELAY);
}

# --- Thêm onreply_route ---
onreply_route {
    if (t_check_status("200") && $rm=="INVITE") {
        rtpengine_answer();
    }
}

# --- Trong nhánh BYE (has_totag + loose_route) đã có ở Giai đoạn 1, thêm ---
if (is_method("BYE")) {
    rtpengine_delete();
    do_accounting("log");
}

# --- Thêm failure_route để failover dispatcher ---
failure_route[GW_FAILOVER] {
    if (t_check_status("408|5[0-9][0-9]")) {
        if (ds_next_dst()) {
            route(RELAY);
            exit;
        }
    }
    sl_send_reply("503", "Service Unavailable");
}

9. Kiểm thử

  1. Test NAT: đặt một softphone sau NAT thật (mạng 4G di động hoặc router có NAT), một softphone trên mạng LAN cùng OpenSIPS — gọi hai chiều, kiểm tra RTP có tới được cả hai phía không.
  2. Test RTPengine: dùng lệnh quản trị RTPengine tương ứng để xem session đang active; kiểm tra log OpenSIPS xem rtpengine_offer()/rtpengine_answer() có trả lỗi không.
  3. Test dispatcher failover: tắt một gateway trong dispatcher.list, gọi ra ngoài, xác nhận cuộc gọi tự chuyển sang gateway còn lại mà người dùng không nhận ra.
  4. Test B2BUA anonymize: bắt gói Wireshark ở cả hai phía Leg A và Leg B, xác nhận Call-ID khác nhau hoàn toàn và From header ở Leg B không còn identity thật.

10. Lỗi thường gặp

Triệu chứngNguyên nhân hay gặp
Gọi được, nghe tiếng nhưng chỉ một chiều (one-way audio)Thiếu rtpengine_answer() trong onreply_route, hoặc gọi rtpengine_offer()/answer() sai thứ tự
Cuộc gọi qua NAT bị rớt sau ~30-60 giâyThiếu cơ chế keepalive từ registrar cho client sau NAT — cần cấu hình thêm NAT ping (module nathelper có chức năng ping định kỳ tới client đã đăng ký)
Dispatcher không tự động failover dù gateway đã chếtQuên gắn failure_route vào transaction trước khi t_relay(), hoặc ds_ping_interval chưa đủ thời gian phát hiện gateway chết
B2BUA tạo cuộc gọi nhưng không bao giờ nhận replyThiếu khai báo đúng script_reply_route trong modparam, hoặc route reply đặt sai tên không khớp
RTPengine daemon không nhận lệnh từ OpenSIPSSai địa chỉ/port trong rtpengine_sock, hoặc RTPengine daemon chưa chạy/chưa mở đúng control socket

11. Tự kiểm tra trước khi qua Giai đoạn 3

  • Giải thích được vì sao NAT tầng signaling và NAT tầng media phải xử lý bởi hai thành phần khác nhau (OpenSIPS vs RTPengine).
  • Vẽ lại đúng thứ tự gọi offer/answer/delete của RTPengine gắn với ba thời điểm nào trong call flow.
  • Phân biệt được khi nào dùng dispatcher, khi nào dùng drouting.
  • Giải thích được vì sao ẩn danh người gọi bằng B2BUA an toàn hơn hẳn so với chỉ sửa From header ở chế độ proxy thuần.

Nếu trả lời trôi chảy cả 4 câu, bạn sẵn sàng cho Giai đoạn 3 (HA/clustering, performance tuning, bảo mật vận hành production).

Contact
Facebook
TOP