Skip to content
LIVE FEED

BlogGiai đoạn 1 chi tiết: Kiến trúc lõi và cấu hình OpenSIPS — file opensips.cfg đầy đủ

Giai đoạn 1 — Kiến trúc lõi + Cấu hình OpenSIPS (Chi tiết đầy đủ)

Điều kiện tiên quyết: đã nắm chắc phần Giai đoạn 0 (transaction, dialog, method, response code). Tài liệu này giả định bạn hiểu vì sao ACK cho 2xx đi khác đường — nếu chưa chắc, quay lại đọc trước.

1. Kiến trúc runtime của OpenSIPS

1.1 Multi-process, không multi-thread

OpenSIPS khởi động và fork ra nhiều process con (số lượng cấu hình bằng children), mỗi process xử lý bản tin SIP độc lập — không dùng thread. Đây là lựa chọn thiết kế có chủ đích: tránh hoàn toàn các vấn đề đồng bộ hoá (race condition, lock contention) vốn là ác mộng của lập trình đa luồng, đổi lại phải giải quyết bài toán “làm sao các process con chia sẻ dữ liệu với nhau” — vì mỗi process là một không gian bộ nhớ riêng theo cơ chế fork() của Linux.

1.2 Shared Memory (SHM) và Private Memory (PKG)

Đây là khái niệm bạn phải hiểu trước khi viết bất kỳ dòng script nào có ý định lưu trạng thái:

Loại bộ nhớPhạm viDùng cho
PKG (Private/Package memory)Riêng của từng process, không process khác đọc đượcBiến $var(x) — chỉ tồn tại trong quá trình xử lý một bản tin, không cần chia sẻ
SHM (Shared memory)Toàn bộ process con đều đọc/ghi đượcBảng đăng ký usrloc (ai đang ở đâu), dialog đang active, cache module… — bất kỳ dữ liệu nào cần “process A ghi, process B đọc”

Hệ quả thực tế: nếu bạn dùng $var(x) = 5; trong route, giá trị đó biến mất ngay khi xử lý xong bản tin đó, và không process khác nhìn thấy được. Muốn lưu trạng thái sống lâu hơn một bản tin và chia sẻ giữa các process (ví dụ đếm số cuộc gọi đang active), bạn phải dùng cấu trúc dữ liệu do module cung cấp (usrloc, dialog, cachedb, htable) — chúng được cấp phát trên SHM. Đây là lý do vì sao OpenSIPS có hẳn module htable (hash table dùng SHM) cho các nhu cầu lưu trạng thái tuỳ biến.

2. Cấu trúc tổng thể file opensips.cfg

File cấu hình luôn có 3 phần theo đúng thứ tự:

####### Global Parameters #########
...
####### Modules Section ########
loadmodule "..."
modparam("...", "...", ...)
...
####### Routing Logic ########
route { ... }

2.1 Global Parameters thường dùng

Tham sốÝ nghĩa
log_levelMức độ chi tiết log (3 là mức thông thường cho production, cao hơn để debug)
log_stderroryes khi test để log ra console thay vì syslog
log_facilityFacility syslog (ví dụ LOG_LOCAL0)
childrenSố process con fork ra cho mỗi giao diện lắng nghe
listenĐịa chỉ/interface OpenSIPS lắng nghe (ví dụ udp:eth0:5060)
disable_tcpTắt TCP nếu chỉ cần UDP (giảm bề mặt tấn công cho lab đơn giản)
auto_aliasesTự nhận diện các địa chỉ/tên khác của chính server (nên tắt no trong production để tránh nhận diện sai do DNS)

2.2 Thứ tự thực thi các khối route — đây là phần hay bị hiểu sai

Bản tin SIP đến
      |
      v
  route { ... }   <-- LUÔN chạy đầu tiên cho mọi request mới
      |
      | (nếu forward đi và tạo branch mới khi forking)
      v
  branch_route[X] { ... }   <-- chạy cho MỖI branch trước khi gửi đi
      |
      | (khi response quay về)
      v
  onreply_route[X] { ... }  <-- chạy khi nhận response, TRƯỚC khi response được relay tiếp
      |
      | (nếu forward thất bại — timeout, 4xx-6xx từ tất cả nhánh)
      v
  failure_route[X] { ... }  <-- chạy khi cần quyết định: thử nhánh khác, hay trả lỗi về

Ngoài ra còn event_route[...] — không nằm trong luồng xử lý bản tin SIP, mà chạy khi có sự kiện nội bộ (lệnh MI được gọi, module clusterer báo trạng thái node thay đổi, timer nội bộ…).

Cách khai báo route phụ và gọi lại:

route[RELAY] {
    if (!t_relay()) {
        sl_reply_error();
    }
}

route {
    ...
    route(RELAY);   # gọi route phụ tên RELAY
}

3. Pseudo-variables thường dùng — bảng tra cứu nhanh

Pseudo-variableÝ nghĩa
$var(x)Biến tạm PKG, sống trong phạm vi xử lý 1 bản tin
$avp(x)Attribute-Value Pair, có thể sống lâu hơn qua các route con trong cùng transaction, dùng để truyền dữ liệu giữa các route
$ruRequest-URI đầy đủ
$rUPhần user trong Request-URI (số bị gọi)
$rdPhần domain trong Request-URI
$fu / $fUFrom URI đầy đủ / phần user trong From (người gọi)
$tu / $tUTo URI đầy đủ / phần user trong To
$ciCall-ID
$ctNội dung header Contact
$siSource IP của gói tin đến
$spSource port của gói tin đến
$rmTên method của request hiện tại

Ghi nhớ: $var/$avp là công cụ thao tác logic tạm thời trong khi xử lý một bản tin — không phải nơi lưu trạng thái lâu dài (xem lại mục 1.2).

4. Module lõi bắt buộc — giải thích + modparam mẫu

tm (Transaction Module)

Nền tảng của toàn bộ xử lý có trạng thái: quản lý transaction INVITE và non-INVITE (xem lại Giai đoạn 0 mục 3). Hầu hết logic forward request có trạng thái đều thông qua t_relay() do module này cung cấp.

loadmodule "tm.so"

sl (Stateless Reply)

Trả lời nhanh mà không cần tạo transaction — dùng cho các trường hợp không cần theo dõi trạng thái (ví dụ trả 200 OK cho OPTIONS keepalive, hoặc trả lỗi trước khi transaction được tạo).

loadmodule "sl.so"

rr (Record-Route)

Chèn chính OpenSIPS vào route set của dialog để các request trong-dialog sau này (BYE, re-INVITE) đi qua lại đúng OpenSIPS.

loadmodule "rr.so"
modparam("rr", "append_fromtag", 1)

append_fromtag nên bật nếu bạn dùng module acc với detect_direction — giúp phân biệt đúng chiều gọi đi/gọi đến của request trong-dialog.

maxfwd

Xử lý header Max-Forwards — chống loop vô hạn (xem Giai đoạn 0 mục 2.3).

loadmodule "maxfwd.so"

usrloc + registrar

Cặp module xử lý REGISTER — usrloc lưu trữ bảng AOR→Contact, registrar cung cấp hàm save()/lookup() dùng trong script.

loadmodule "usrloc.so"
modparam("usrloc", "db_mode", 0)   # 0 = chỉ lưu trong RAM, mất khi restart (đủ cho lab)
                                     # 2 = write-through DB, dùng khi cần persistence qua restart

loadmodule "registrar.so"

auth + auth_db

Xác thực digest — auth cung cấp core logic, auth_db lấy mật khẩu từ database (bảng subscriber).

loadmodule "auth.so"
loadmodule "auth_db.so"
modparam("auth_db", "calculate_ha1", yes)
modparam("auth_db", "password_column", "password")
modparam("auth_db", "db_url", "mysql://opensips:opensipsrw@localhost/opensips")

dialog

Theo dõi vòng đời dialog (từ INVITE tới BYE) — cần thiết để biết chính xác khi nào một cuộc gọi thật sự kết thúc, phục vụ tính cước theo thời lượng và các thao tác quản lý cuộc gọi sau này (Giai đoạn 2, 3).

loadmodule "dialog.so"
modparam("dialog", "dlg_flag", 4)

acc

Ghi CDR. Phiên bản hiện tại dùng hàm do_accounting(type, [flags], [table]) gọi tường minh trong script thay vì cơ chế flag tự động kiểu cũ.

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

permissions

Kiểm soát ai được phép gọi đi đâu (dùng nhiều hơn ở Giai đoạn 2 khi có nhiều gateway) — nêu ở đây để bạn biết vị trí của nó trong bức tranh tổng thể, chưa cần cấu hình sâu ngay.

5. Chuẩn bị database (MySQL) cho lab

# cài opensips-cli nếu chưa có (đi kèm gói opensips-cli hoặc script db riêng)
opensips-cli -x database create

Lệnh trên tạo database opensips cùng các bảng cần thiết (subscriber, acc, missed_calls, location nếu dùng db_mode > 0…). Sau đó thêm một subscriber test:

INSERT INTO subscriber (username, domain, password, ha1, ha1b)
VALUES ('alice', 'example.com', 'alicepass',
        MD5('alice:example.com:alicepass'),
        MD5('alice:example.com:alicepass'));

(Cách tính ha1/ha1b chính xác phụ thuộc cấu hình calculate_ha1 — nếu để yes, OpenSIPS tự tính từ password plaintext, bạn không cần tự MD5 tay; ví dụ trên chỉ minh hoạ ý nghĩa của cột.)

6. File cấu hình đầy đủ — dự án tay Giai đoạn 1

Ghép toàn bộ lại thành một opensips.cfg hoàn chỉnh, xử lý đúng: REGISTER (có auth qua MySQL), INVITE/ACK/BYE/CANCEL (có record-route + dialog + acc), OPTIONS (keepalive stateless).

####### Global Parameters #########
log_level=3
log_stderror=yes
log_facility=LOG_LOCAL0
children=4
disable_tcp=yes
auto_aliases=no
listen=udp:eth0:5060

####### Modules Section ########
loadmodule "tm.so"
loadmodule "sl.so"
loadmodule "rr.so"
modparam("rr", "append_fromtag", 1)

loadmodule "maxfwd.so"

loadmodule "usrloc.so"
modparam("usrloc", "db_mode", 0)

loadmodule "registrar.so"

loadmodule "auth.so"
loadmodule "auth_db.so"
modparam("auth_db", "calculate_ha1", yes)
modparam("auth_db", "password_column", "password")
modparam("auth_db", "db_url", "mysql://opensips:opensipsrw@localhost/opensips")

loadmodule "dialog.so"
modparam("dialog", "dlg_flag", 4)

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

####### Routing Logic ########

route {
    # 1. Chống loop vô hạn — luôn kiểm tra đầu tiên
    if (!mf_process_maxfwd_header(10)) {
        sl_send_reply("483", "Too Many Hops");
        exit;
    }

    # 2. Request trong-dialog (có To-tag) — BYE, ACK 2xx, re-INVITE...
    if (has_totag()) {
        if (loose_route()) {
            if (is_method("BYE")) {
                do_accounting("log");   # ghi CDR khi cuộc gọi kết thúc
            }
            route(RELAY);
            exit;
        } else {
            # ACK cho 2xx không có route set hợp lệ vẫn phải cố relay theo transaction
            if (is_method("ACK")) {
                if (t_check_trans()) {
                    t_relay();
                }
                exit;
            }
            sl_send_reply("404", "Not Here");
            exit;
        }
    }

    # 3. CANCEL cho INVITE đang treo
    if (is_method("CANCEL")) {
        if (t_check_trans()) {
            t_relay();
        }
        exit;
    }

    # 4. OPTIONS dùng làm keepalive — trả lời thẳng, không cần transaction
    if (is_method("OPTIONS") && $rU==$null) {
        sl_send_reply("200", "OK");
        exit;
    }

    # 5. REGISTER — xác thực rồi lưu vị trí
    if (is_method("REGISTER")) {
        if (!www_authorize("", "subscriber")) {
            www_challenge("", "0");
            exit;
        }
        if (!save("location")) {
            sl_reply_error();
        }
        exit;
    }

    # 6. INVITE mới — chèn Record-Route để nằm trong đường đi của dialog
    if (is_method("INVITE")) {
        record_route();
        create_dialog();
    }

    # 7. Tìm nơi chuyển tiếp dựa trên bảng đăng ký
    if (!lookup("location")) {
        sl_send_reply("404", "Not Found");
        exit;
    }

    if (is_method("INVITE")) {
        do_accounting("log");   # ghi log lúc khởi tạo cuộc gọi (tuỳ chọn, có thể chỉ ghi lúc BYE)
    }

    route(RELAY);
}

route[RELAY] {
    if (!t_relay()) {
        sl_reply_error();
    }
    exit;
}

Lưu ý quan trọng về độ chính xác cú pháp: tên tham số modparam và tín hiệu hàm có thể lệch nhẹ giữa các phiên bản OpenSIPS (ví dụ acc module đã đổi từ cơ chế flag tự động sang do_accounting() tường minh trong các bản gần đây). Trước khi chạy thật, luôn đối chiếu với README của từng module đúng phiên bản bạn cài (opensips -V để xem version, sau đó tra https://opensips.org/docs/modules/<version>/<module>.html).

7. Giải thích luồng xử lý — nối lại với kiến thức Giai đoạn 0

  • Bước 2 (has_totag() + loose_route()) chính là nơi áp dụng khái niệm dialog-forming response đã học: chỉ request nào thuộc một dialog đã tồn tại (có To-tag) mới đi vào nhánh này.
  • Bước 2 nhánh ACK xử lý đúng điều đã nhấn mạnh ở Giai đoạn 0 mục 3.3: ACK cho 2xx đi theo route set riêng (qua loose_route()), không dùng lại transaction INVITE gốc — đây là lý do route xử lý ACK tách biệt hẳn logic INVITE.
  • Bước 3 minh hoạ đúng định nghĩa CANCEL ở Giai đoạn 0: chỉ có ý nghĩa khi transaction INVITE gốc còn tồn tại (t_check_trans()), nếu không transaction đã kết thúc thì CANCEL bị bỏ qua.
  • Bước 6 (record_route()) là hành động khiến OpenSIPS xuất hiện trong Record-Route header của response, từ đó BYE sau này biết đường quay lại qua OpenSIPS — nối trực tiếp với khái niệm Route set trong RFC 3261.

8. Kiểm thử

  1. Kiểm tra cú pháp trước khi chạy thật:
    opensips -C -f /etc/opensips/opensips.cfg
    
  2. Chạy ở chế độ foreground để xem log trực tiếp khi debug:
    opensips -f /etc/opensips/opensips.cfg -D -E
    
  3. Test REGISTER nhanh bằng sipsak (không cần softphone):
    sipsak -U -C sip:alice@example.com -s sip:alice@your-server-ip -a alicepass
    
  4. Test bằng 2 softphone thật (Zoiper/Linphone), gọi giữa hai máy đã đăng ký, quan sát Wireshark như đã luyện ở Giai đoạn 0 — lần này đối chiếu xem OpenSIPS có xuất hiện đúng trong Via/Record-Route hay không.
  5. Kiểm tra bảng đăng ký hiện tại qua giao diện quản trị (MI):
    opensips-cli -x ul_dump
    

9. Lỗi thường gặp của người mới

Triệu chứngNguyên nhân hay gặp
REGISTER luôn trả 401 dù mật khẩu đúngcalculate_ha1 không khớp với cách bạn lưu password trong DB — kiểm tra lại cột password có đang lưu plaintext hay đã là HA1
Gọi được nhưng BYE không tới nơiThiếu record_route() ở nhánh INVITE, hoặc quên xử lý loose_route() ở nhánh có-totag
CDR trong bảng acc bị thiếu hoặc trùng lặpGọi do_accounting() sai chỗ (ví dụ gọi cả ở bước khởi tạo lẫn bước BYE mà không phân biệt loại sự kiện)
ACK cho 2xx bị rơi, cuộc gọi treo im lặng không có RTPNhầm lẫn xử lý ACK 2xx giống ACK non-2xx — quên rằng ACK 2xx phải qua loose_route(), không tự động qua tm
OPTIONS keepalive giữa 2 OpenSIPS bị relay đi lung tung thay vì trả lời thẳngThiếu điều kiện $rU==$null hoặc thiếu kiểm tra OPTIONS gửi đến chính server trước khi quyết định trả lời trực tiếp

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

  • Giải thích được vì sao $var(x) không dùng để đếm số cuộc gọi đang active toàn hệ thống — phải dùng cấu trúc nào thay thế?
  • Vẽ lại được thứ tự chạy của route/failure_route/branch_route/onreply_route bằng trí nhớ.
  • Chỉ ra chính xác dòng nào trong file cấu hình mẫu xử lý đúng hành vi ACK-cho-2xx đã học ở Giai đoạn 0.
  • Tự thêm được một nhánh xử lý cho method MESSAGE (trả lời thẳng 202 Accepted) mà không cần xem lại tài liệu.

Nếu cả 4 câu trên đều trả lời trôi chảy, bạn đã sẵn sàng cho Giai đoạn 2 (NAT, RTPengine, dispatcher, B2BUA thực chiến).

Contact
Facebook
TOP