Administrator
Published on 2023-04-14 / 310 Visits
0
0

SIP 基本了解

基本了解

sip可以通过tcp或者udp,通过发送标准sip格式的字符串(编码成utf8)来实现通信,比如:

REGISTER sip:192.168.5.175 SIP/2.0
Via: SIP/2.0/UDP 192.168.5.176:5060;branch=z9hG4bK1735798886948
Max-Forwards: 70
From: <sip:1005@192.168.5.175>;tag=1735798886967
To: <sip:1005@192.168.5.175>
Call-ID: 1735798886951@192.168.5.175
CSeq: 1 REGISTER
Contact: <sip:1005@192.168.5.176:5060>
Expires: 3600
Content-Length: 0

上面这个格式成为消息头,还有消息体,即在消息头中指定类型和长度,然后空一个回车后的就是消息体,如呼叫请求中携带sdp信息:

INVITE sip:bob@example.com SIP/2.0
Via: SIP/2.0/UDP 192.168.1.100:5060;branch=z9hG4bK12345
Max-Forwards: 70
From: "Alice" <sip:alice@example.com>;tag=123456
To: "Bob" <sip:bob@example.com>
Call-ID: 123456789@192.168.1.100
CSeq: 1 INVITE
Contact: <sip:alice@192.168.1.100:5060>
Content-Type: application/sdp
Content-Length: 158

v=0
o=alice 2890844526 2890844526 IN IP4 192.168.1.100
s=Session SDP
c=IN IP4 192.168.1.100
t=0 0
m=audio 49170 RTP/AVP 0
a=rtpmap:0 PCMU/8000

参考:
SIP [RFC3261]

请求类型

1681437832180
1681437950698

响应类型

  • 100试呼叫(Trying)
  • 180振铃(Ringing)
  • 181呼叫正在前转(Call is Being Forwarded)
  • 200成功响应(OK)
  • 302临时迁移(Moved Temporarily)
  • 400错误请求(Bad Request)
  • 401未授权(Unauthorized)
  • 403禁止(Forbidden)
  • 404用户不存在(Not Found)
  • 408请求超时(Request Timeout)
  • 480暂时无人接听(Temporarily Unavailable)
  • 486线路忙(Busy Here)
  • 504服务器超时(Server Time-out)
  • 600全忙(Busy Everywhere)

参考:
wiki

消息示例

参考:
SIP常用消息实例参考

java注册代码参考

package org.example;

import java.io.IOException;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class SimpleSipClient {

    private static final String SIP_VERSION = "SIP/2.0";
    private static final String USERNAME = "1005";
    private static final String PASSWORD = "1234";
    private static final String DOMAIN = "192.168.5.175";
    private static final String PROXY = "192.168.5.175";
    private static final int PROXY_PORT = 5060;

    private DatagramSocket socket;
    private InetAddress proxyAddress;
    private int localPort;

    public SimpleSipClient() throws SocketException, UnknownHostException {
        socket = new DatagramSocket(5060); // 系统自动分配端口
        localPort = socket.getLocalPort(); // 获取实际绑定的端口
        System.out.println("Bound to local port: " + localPort);
        proxyAddress = InetAddress.getByName(PROXY);
    }

    public void sendSipMessage(String message) throws IOException {
        byte[] buffer = message.getBytes(StandardCharsets.UTF_8);
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length, proxyAddress, PROXY_PORT);
        socket.send(packet);
        System.out.println("Sent SIP message:\n" + message);
    }

    public String receiveSipMessage() throws IOException {
        byte[] buffer = new byte[4096];
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
        socket.receive(packet);
        String receivedMessage = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.UTF_8);
        System.out.println("Received SIP message:\n" + receivedMessage);
        return receivedMessage;
    }

    public void register() throws IOException, NoSuchAlgorithmException {
        String viaBranch = "z9hG4bK" + System.currentTimeMillis();
        String callId = System.currentTimeMillis() + "@" + DOMAIN;

        // 第一次注册请求(不带鉴权信息)
        String registerMessage = "REGISTER sip:" + DOMAIN + " " + SIP_VERSION + "\r\n" +
                "Via: SIP/2.0/UDP " + InetAddress.getLocalHost().getHostAddress() + ":" + localPort + ";branch=" + viaBranch + "\r\n" +
                "Max-Forwards: 70\r\n" +
                "From: <sip:" + USERNAME + "@" + DOMAIN + ">;tag=" + System.currentTimeMillis() + "\r\n" +
                "To: <sip:" + USERNAME + "@" + DOMAIN + ">\r\n" +
                "Call-ID: " + callId + "\r\n" +
                "CSeq: 1 REGISTER\r\n" +
                "Contact: <sip:" + USERNAME + "@" + InetAddress.getLocalHost().getHostAddress() + ":" + localPort + ">\r\n" +
                "Expires: 3600\r\n" +
                "Content-Length: 0\r\n\r\n";

        sendSipMessage(registerMessage);

        // 接收响应
        String response = receiveSipMessage();

        // 如果收到 401 Unauthorized,提取鉴权信息并重新注册
        if (response.contains("401 Unauthorized")) {
            String realm = extractHeaderValue(response, "Digest realm");
            String nonce = extractHeaderValue(response, "nonce");

            // 打印调试信息
            System.out.println("Realm: " + realm);
            System.out.println("Nonce: " + nonce);

            // 生成鉴权响应
            String authResponse = calculateAuthResponse(USERNAME, PASSWORD, realm, nonce, "REGISTER", "sip:" + DOMAIN);

            // 打印调试信息
            System.out.println("HA1: " + md5(USERNAME + ":" + realm + ":" + PASSWORD));
            System.out.println("HA2: " + md5("REGISTER:sip:" + DOMAIN));
            System.out.println("Auth Response: " + authResponse);

            // 第二次注册请求(带鉴权信息)
            registerMessage = "REGISTER sip:" + DOMAIN + " " + SIP_VERSION + "\r\n" +
                    "Via: SIP/2.0/UDP " + InetAddress.getLocalHost().getHostAddress() + ":" + localPort + ";branch=" + viaBranch + "\r\n" +
                    "Max-Forwards: 70\r\n" +
                    "From: <sip:" + USERNAME + "@" + DOMAIN + ">;tag=" + System.currentTimeMillis() + "\r\n" +
                    "To: <sip:" + USERNAME + "@" + DOMAIN + ">\r\n" +
                    "Call-ID: " + callId + "\r\n" +
                    "CSeq: 2 REGISTER\r\n" +
                    "Contact: <sip:" + USERNAME + "@" + InetAddress.getLocalHost().getHostAddress() + ":" + localPort + ">\r\n" +
                    "Authorization: Digest username=\"" + USERNAME + "\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", uri=\"sip:" + DOMAIN + "\", response=\"" + authResponse + "\"\r\n" +
                    "Expires: 3600\r\n" +
                    "Content-Length: 0\r\n\r\n";

            sendSipMessage(registerMessage);
            receiveSipMessage(); // 等待最终响应
        }
    }

    private String extractHeaderValue(String response, String headerName) {
        String[] lines = response.split("\r\n");
        for (String line : lines) {
            if (line.startsWith("WWW-Authenticate")) {
                String[] key = line.replaceAll("WWW-Authenticate: ","").split(", ");
                for (String s : key){
                    if (s.startsWith(headerName)){
                        String t = s.split("=")[1];
                        return t.substring(1,t.length()-1);
                    }
                }
            }
        }
        return null;
    }

    private String calculateAuthResponse(String username, String password, String realm, String nonce, String method, String uri) throws NoSuchAlgorithmException {
        String ha1 = md5(username + ":" + realm + ":" + password);
        String ha2 = md5(method + ":" + uri);
        return md5(ha1 + ":" + nonce + ":" + ha2);
    }

    private String md5(String input) throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("MD5");
        byte[] hash = md.digest(input.getBytes(StandardCharsets.UTF_8));
        return toHexString(hash);
    }

    private String toHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        try {
            SimpleSipClient client = new SimpleSipClient();
            client.register(); // 注册到SIP服务器
            Thread.sleep(1000); // 等待注册完成
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

会议相关

参考:
SIP的视频会议系统结构和原理介绍【详解】
SIP视频会议框架与信令控制流程

过程分析

以下是A,B注册到代理服务器,然后A呼叫B,B接听,B挂断的过程。
A的ip为192.168.1.18,用户名101;B的ip为192.168.1.31,用户名100;代理服务器的ip为192.168.1.175。
注册成功的过程,看抓包得发两次REGISTER两次,第一次不携带鉴权信息,第二次携带鉴权信息。
通过观察,REGISTER会定时发送,不是说只发开始的一次而已。

一. A,B注册到代理服务器

说明

进行注册时,SIP 协议(RFC 3261)明确规定了鉴权流程:

  1. 客户端发送请求(不带鉴权信息或者错误的鉴权信息)。
  2. 服务器返回 401 Unauthorized(其中WWW-Authorization字段包含 realm 和 nonce)。
  3. 客户端重新发送请求(携带带鉴权信息Authorization字段,鉴权信息由密码计算而出,并且其中携带第2部分中的realm和nonce字段内容)。
  4. 服务器验证鉴权信息并返回最终响应200 OK。

可以发现,如果sip服务器启用了鉴权机制,成功注册需要发送两次REGISTER请求。

图解

未命名文件 (3)

抓包详解(B客户端)

  1. 两次注册,两次应答
    1681439419080
  2. 客户端第一次注册(无鉴权信息不存在Authorization字段或存在Authorization字段,但鉴权信息有误)
    1681439665149
  3. 代理服务器第一次应答(失败401)
    1681439693771
  4. 客户端第二次注册(有鉴权信息)
    1681439718533
  5. 代理服务器第二次应答(成功200)
    1681439750465

二次验证的作用

(1) 安全性
防止重放攻击:
服务器在第一次响应中返回一个随机生成的 nonce 值。客户端需要使用这个 nonce 计算鉴权响应(response)。由于 nonce 是随机且一次性的,攻击者无法通过截获的鉴权信息重放请求。
动态鉴权:
每次鉴权时,服务器可以生成不同的 nonce,确保每次鉴权过程都是独立的,增强了安全性。

(2) 灵活性
支持多种鉴权方式:
服务器可以根据需要选择不同的鉴权方式(如 Digest、AKAv1-MD5 等),并在第一次响应中告知客户端,客户端使用对应的计算方式进行密码计算。
减少不必要的计算:
如果客户端直接发送鉴权信息,但服务器不需要鉴权,会导致不必要的计算和资源浪费。

二. A呼叫B

说明

与注册类似,在SIP呼叫流程中,如果服务器要求认证,但客户端在第一次请求中没有提供有效的nonce,服务器会返回一个401 Unauthorized或407 Proxy Authentication Required响应。这个响应中的WWW-Authorization或Rroxy-Authorization字段会包含nonce值,客户端需要根据这个nonce重新计算认证信息(如response),然后重新发送请求。

通过观察抓包可以知道,A呼叫B时,我们可知INVITE请求头和请求行携带的号码和IP信息:

From字段规律:通过观察A呼叫B的整个过程(包括B接听),所有来回的通信中携带的信息都为<sip:101@192.168.1.175>
To字段规律:通过观察A呼叫B的整个过程(包括B接听),所有来回的通信中携带的信息都为<sip:100@192.168.1.175>
说明From字段和To字段并不是按某个节点的理解,而是按最开始的INVITE携带到整个完整的处理周期,且完全一样这两个值。
挂断类似上面的,按挂断的第一包BYE的信息为主。

Contact存在INVITE包,且携带的信息按各自发起的。
Via字段,携带的ip,则按照A-代理服务器之间的指令都以A的ip,代理服务器-B之间的指令都以代理服务器的为主。

图解

未命名文件 (4)

抓包详解

A开始呼叫B,A与代理服务器通信
  1. 两次呼叫,两次应答
    1681441006160-1681441050578
  2. 客户端第一次呼叫(无鉴权信息)
    1681441211865
    1681441279323
  3. 代理服务器第一次应答(失败401)
    1681441303401
  4. 客户端响应ACK给代理服务器
    1681441425993
  5. 客户端第二次呼叫(有鉴权信息)

在此阶段发送sdp信息,包含呼叫方A需要收流的端口

1681441493125
1681442028809
5. 代理服务器第二次应答(成功100)
1681441527179

代理服务器与B通信
  1. 一次呼叫一次应答一次振铃
    1681441809379
  2. 呼叫

在此阶段发送sdp信息,包含被呼叫方B需要推流的端口

1681441934140
1681441985886
2. 应答
1681441958748
3. 振铃
1681441969605

代理服务器与A通信
  1. 振铃
    1681442390418

三. B开始接听

图解

未命名文件 (1)

抓包详解

  1. 总流程,共4包数据
    1681442676340
  2. 接听,B→Server

在此阶段发送sdp信息,包含被呼叫方B需要收流的端口

1681442742036
1681442761333
3. 应答,Server→B
1681442779356
5. 回复,Server→A

在此阶段发送sdp信息,包含呼叫方A需要推流的端口

1681442798467
1681442810027
7. 应答,A→Server
1681442830953

四. B开始挂断

图解

未命名文件 (2)

抓包详解

  1. 11
    1681442891152
  2. 挂断
    1681442925231
  3. 应答
    1681442939487-1681442944824
  4. 挂断
    1681442974149
  5. 应答
    1681442991677

Comment