基本了解
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]
请求类型
响应类型
- 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)明确规定了鉴权流程:
- 客户端发送请求(不带鉴权信息或者错误的鉴权信息)。
- 服务器返回 401 Unauthorized(其中WWW-Authorization字段包含 realm 和 nonce)。
- 客户端重新发送请求(携带带鉴权信息Authorization字段,鉴权信息由密码计算而出,并且其中携带第2部分中的realm和nonce字段内容)。
- 服务器验证鉴权信息并返回最终响应200 OK。
可以发现,如果sip服务器启用了鉴权机制,成功注册需要发送两次REGISTER请求。
图解
抓包详解(B客户端)
- 两次注册,两次应答
- 客户端第一次注册(无鉴权信息不存在Authorization字段或存在Authorization字段,但鉴权信息有误)
- 代理服务器第一次应答(失败401)
- 客户端第二次注册(有鉴权信息)
- 代理服务器第二次应答(成功200)
二次验证的作用
(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之间的指令都以代理服务器的为主。
图解
抓包详解
A开始呼叫B,A与代理服务器通信
- 两次呼叫,两次应答
- 客户端第一次呼叫(无鉴权信息)
- 代理服务器第一次应答(失败401)
- 客户端响应ACK给代理服务器
- 客户端第二次呼叫(有鉴权信息)
在此阶段发送sdp信息,包含呼叫方A需要收流的端口
5. 代理服务器第二次应答(成功100)
代理服务器与B通信
- 一次呼叫一次应答一次振铃
- 呼叫
在此阶段发送sdp信息,包含被呼叫方B需要推流的端口
2. 应答
3. 振铃
代理服务器与A通信
- 振铃
三. B开始接听
图解
抓包详解
- 总流程,共4包数据
- 接听,B→Server
在此阶段发送sdp信息,包含被呼叫方B需要收流的端口
3. 应答,Server→B
5. 回复,Server→A
在此阶段发送sdp信息,包含呼叫方A需要推流的端口
7. 应答,A→Server
四. B开始挂断
图解
抓包详解
- 11
- 挂断
- 应答
- 挂断
- 应答