官方位址
原始程式檔列表
lib.c |
公用函式,都是用在網路位址處理 |
mcgroup.c |
處理 join/leace Multicast group |
ifvc.c |
建立及提供搜尋 interface vector 功能 |
config.c |
讀取 config 設定檔 |
udpsock.c |
建立 udp sock |
request.c |
處理 igmp request |
igmp.c |
收/送 igmp 封包 |
confread.c |
config 子集,讀檔相關功能 |
igmpproxy.c |
igmpproxy 主流程 |
syslog.c |
處理 log 訊息 |
mroute-api.c |
mroute api 相關程式 |
kern.c |
不確定,看起來是打包用的 |
rttable.c |
igmpproxy 內部使用的 routing table 以及跟 kernel 增減 routing 的程式 |
callout.c |
佇列處理 |
igmpproxy.h |
|
os-linux.h |
|
os-openbsd.h |
|
os-freebsd.h |
|
os-dragonfly.h |
|
os-netbsd.h |
|
igmpproxy 運作概說
multicast 的訂閱是由 igmp 來進行,但是 multi 的特性類似於 broadcast,所以並不能透過 route 或 nat 轉發,所以需要在 router/gateway 上提供 igmp proxy 服務,在 downstream 收到 request 的時候,從 upstream 轉發出去,同時要去通知 kernel 收到這個 group 的 multicast 要轉發給 downstream。因為 downstream 有可能不只一個 host 要訂閱某個 group,igmpproxy 不可以在確定某 host leave 的時候就直接切斷這個 group 的 multicast routing,所以需要維護一套自己要看的 routing table,在確定 downstream 都沒有人有訂閱這個 group 的時候才能切斷。
後面的程式解析只看 igmpproxy 的主要流程,也就是收到 igmp 封包之後開始解析,看看該程式是如何做出 igmpproxy 所需的功能。
acceptIgmp()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| // protocol == 0; kernel 發現新 source 的 group,所以要把他加入 routing 表
if (ip->ip_p == 0) {
checkVIF = getIfByIx( upStreamVif ); // 根據 src 位址找出 upstream vif
if(checkVIF == 0) { // 不是 upstream vif,跳開
else if(src == checkVIF->InAdr.s_addr) { // 根本就是自己發的,跳開
else if(!isAdressValidForIf(checkVIF, src)) { // 不在 upstream 的網段,跳開
}
activateRoute(dst, src); // 啟用 routing,然後跳開
}
// 後面會根據收到的 igmp 封包決定後續動作
switch (igmp->igmp_type) {
case IGMP_V1_MEMBERSHIP_REPORT:
case IGMP_V2_MEMBERSHIP_REPORT:
// 收到 igmp membership report
acceptGroupReport(src, group, igmp->igmp_type);
return;
case IGMP_V2_LEAVE_GROUP:
// 收到 igmp leave
acceptLeaveMessage(src, group);
return;
case IGMP_MEMBERSHIP_QUERY:
// 收到 igmp query,總覺得應該要回應這個 query 比較好,可是實際操作沒問題,所以就當作不需要就好
return;
default:
// 其他當作不認識
my_log(LOG_INFO, 0,
"ignoring unknown IGMP message type %x from %s to %s",
igmp->igmp_type, inetFmt(src, s1),
inetFmt(dst, s2));
return;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| if(!IN_MULTICAST( ntohl(group) )) {} // 如果 group 不在 multicast 位址範圍內,跳開
sourceVif = getIfByAddress( src ); // 根據 src 位址找出 vif
if(sourceVif == NULL) {} // 找不到 vif,跳開
if(sourceVif->InAdr.s_addr == src) {} // src 就是自己,跳開
if(sourceVif->state == IF_STATE_DOWNSTREAM) { // 是 downstream 的 vif 才處理
if(sourceVif->allowedgroups == NULL) { // 沒有設定白名單
insertRoute(group, sourceVif->index); // 加入路由後跳開
}
for(sn = sourceVif->allowedgroups; sn != NULL; sn = sn->next) {
if((group & sn->subnet_mask) == sn->subnet_addr) { // 在白名單裡面
insertRoute(group, sourceVif->index); // 加入路由
}
}
}
|
1
2
3
4
5
6
7
8
| if(!IN_MULTICAST( ntohl(group) )) {} // 如果 group 不在 multicast 位址範圍內,跳開
sourceVif = getIfByAddress( src ); // 根據 src 位址找出 vif
if(sourceVif == NULL) {} // 找不到 vif,跳開
// 這次不檢查是不是自己發的!?
if(sourceVif->state == IF_STATE_DOWNSTREAM) { // 是 downstream 的 vif 才處理
setRouteLastMemberMode(group); // 把 group 設定成沒有成員模式
sendGroupSpecificMemberQuery(gvDesc); // 送出 query,驗證是不是還有人要接收 group
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| croute = findRoute(group); // 根據 group 位址找 route 紀錄
if(croute==NULL) { // route 不存在
newroute = (struct RouteTable*)malloc(sizeof(struct RouteTable)); // 建立新的 route 紀錄
newroute->group = group;
newroute->upstrState = ROUTESTATE_NOTJOINED; // 新的 route 所以設定為 notjoined
BIT_ZERO(newroute->vifBits); // 清空要接收的 vif 表
croute = newroute; // croute 後面還會用到
if(ifx >= 0) {
BIT_SET(newroute->vifBits, ifx); // 設定接收的 vif
}
if(routing_table == NULL) {
// routing_table 還不存在,這個 newroute 就當頭
} else {
// routing_table 已經存在,找適當位置插入這一筆新的
}
} else { // route 已經存在
BIT_SET(croute->vifBits, ifx); // 更新要接收的 vif 表
if(croute->originAddr != 0) { // 有來源在播送給這個 group 的話
internUpdateKernelRoute(croute, 1) // 告訴 kernel 要把這個 group 轉送
}
}
if(croute->upstrState != ROUTESTATE_JOINED) { // 還沒對 upstream 送出 join
sendJoinLeaveUpstream(croute, 1); // 送出 join report
}
|
1
2
3
4
5
6
7
| croute = findRoute(group); // 根據 group 尋找 route
if(croute!=NULL) { // 有找到
if(croute->upstrState == ROUTESTATE_JOINED && conf->fastUpstreamLeave) { //已經送過 join 而且有設定 fast leave
sendJoinLeaveUpstream(croute, 0); // 送出 leave report
}
croute->upstrState = ROUTESTATE_CHECK_LAST_MEMBER; // 更改 upstream 狀態
}
|
1
2
3
4
5
6
7
8
9
| GroupVifDesc *gvDesc = (GroupVifDesc*) argument; // 把參數設定成 GroupVifDesc 型態
if(gvDesc->started) { // 已經啟用
if(!lastMemberGroupAge(gvDesc->group)) { // 最新一次的 member 檢查沒有得到 member 的回應
return;
} else {
gvDesc->started = 1; // 設定成啟動
}
sendIgmp(...); // 送出 membership query,詢問還有誰在接收這個 group
timer_setTimer(...); // 重設下次檢查時間
|
主程式流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| int main( int ArgCn, char *ArgVc[] ) {
for (int c; (c = getopt(ArgCn, ArgVc, "vdh")) != -1;) {
// 解析輸入參數
// d 將紀錄輸出到 stderr
// v 紀錄 info 等級
// vv 紀錄 debug 等級
// h 顯示語法說明
}
do { // 這個是主程式迴圈,還不是主迴圈,很有趣的寫法 :D,有什麼好處我不知道,請知道的人幫忙釋疑
if( ! loadConfig( configFilePath ) ) {} //讀取設定檔
if ( !igmpProxyInit() ) {} // 系統初始化
igmpProxyRun(); // 真正的主迴圈,開始收 igmp 封包跑 igmpproxy 流程
igmpProxyCleanUp(); // 清除
} while ( false );
}
|
設定檔
1
2
3
4
5
6
7
8
9
| int loadConfig(char *configFile) {
while ( token != NULL ) {
if(strcmp("phyint", token)==0) {
tmpPtr = parsePhyintToken(); // 解析 phy
}
else if(strcmp("quickleave", token)==0) {} // quickleave 標籤
... ...
}
}
|
PPP 連線的問題
先來看原本的程式 (ifvc.c)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| // Get the interface adress...
IfDescEp->InAdr = ((struct sockaddr_in *)&IfPt->ifr_addr)->sin_addr;
addr = IfDescEp->InAdr.s_addr;
memcpy( IfReq.ifr_name, IfDescEp->Name, sizeof( IfReq.ifr_name ) );
IfReq.ifr_addr.sa_family = AF_INET;
((struct sockaddr_in *)&IfReq.ifr_addr)->sin_addr.s_addr = addr;
// Get the subnet mask...
if (ioctl(Sock, SIOCGIFNETMASK, &IfReq ) < 0)
my_log(LOG_ERR, errno, "ioctl SIOCGIFNETMASK for %s", IfReq.ifr_name);
mask = ((struct sockaddr_in *)&IfReq.ifr_addr)->sin_addr.s_addr;
subnet = addr & mask;
/* get if flags
**
** typical flags:
** lo 0x0049 -> Running, Loopback, Up
** ethx 0x1043 -> Multicast, Running, Broadcast, Up
** ipppx 0x0091 -> NoArp, PointToPoint, Up
** grex 0x00C1 -> NoArp, Running, Up
** ipipx 0x00C1 -> NoArp, Running, Up
*/
if ( ioctl( Sock, SIOCGIFFLAGS, &IfReq ) < 0 )
my_log( LOG_ERR, errno, "ioctl SIOCGIFFLAGS" );
IfDescEp->Flags = IfReq.ifr_flags;
// Insert the verified subnet as an allowed net...
IfDescEp->allowednets = (struct SubnetList *)malloc(sizeof(struct SubnetList));
if(IfDescEp->allowednets == NULL) my_log(LOG_ERR, 0, "Out of memory !");
// Create the network address for the IF..
IfDescEp->allowednets->next = NULL;
IfDescEp->allowednets->subnet_mask = mask;
IfDescEp->allowednets->subnet_addr = subnet;
|
因為 igmp proxy 是利用 interface 的 ip/netmask 來判斷封包是否過濾,一般的 IPoE 使用上都沒什麼問題,但是遇到 PPP 連線時,對方傳過來的 mcast 封包 source 會是 P-t-P dstaddr,而不是我們這邊的 IP。這個情形會導致 igmpproxy 把 mcast 都丟掉,所以要在 line 28 那邊作一些小改變。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| // Get the interface adress...
IfDescEp->InAdr = ((struct sockaddr_in *)&IfPt->ifr_addr)->sin_addr;
addr = IfDescEp->InAdr.s_addr;
memcpy( IfReq.ifr_name, IfDescEp->Name, sizeof( IfReq.ifr_name ) );
IfReq.ifr_addr.sa_family = AF_INET;
((struct sockaddr_in *)&IfReq.ifr_addr)->sin_addr.s_addr = addr;
// Get the subnet mask...
if (ioctl(Sock, SIOCGIFNETMASK, &IfReq ) < 0)
my_log(LOG_ERR, errno, "ioctl SIOCGIFNETMASK for %s", IfReq.ifr_name);
mask = ((struct sockaddr_in *)&IfReq.ifr_addr)->sin_addr.s_addr;
subnet = addr & mask;
/* get if flags
**
** typical flags:
** lo 0x0049 -> Running, Loopback, Up
** ethx 0x1043 -> Multicast, Running, Broadcast, Up
** ipppx 0x0091 -> NoArp, PointToPoint, Up
** grex 0x00C1 -> NoArp, Running, Up
** ipipx 0x00C1 -> NoArp, Running, Up
*/
if ( ioctl( Sock, SIOCGIFFLAGS, &IfReq ) < 0 )
my_log( LOG_ERR, errno, "ioctl SIOCGIFFLAGS" );
IfDescEp->Flags = IfReq.ifr_flags;
// aimwang: when pppx get dstaddr for use
if (0x10d1 == IfDescEp->Flags)
{
if ( ioctl( Sock, SIOCGIFDSTADDR, &IfReq ) < 0 )
my_log(LOG_ERR, errno, "ioctl SIOCGIFDSTADDR for %s", IfReq.ifr_name);
addr = ((struct sockaddr_in *)&IfReq.ifr_dstaddr)->sin_addr.s_addr;
subnet = addr & mask;
}
// Insert the verified subnet as an allowed net...
IfDescEp->allowednets = (struct SubnetList *)malloc(sizeof(struct SubnetList));
if(IfDescEp->allowednets == NULL) my_log(LOG_ERR, 0, "Out of memory !");
// Create the network address for the IF..
IfDescEp->allowednets->next = NULL;
IfDescEp->allowednets->subnet_mask = mask;
IfDescEp->allowednets->subnet_addr = subnet;
|
Line 29-36 是根據 IfDescEp->Flags == 0x10d1 時,addr 改取 dstaddr 而不是 srcaddr,然後重算 subnet,再交給後面加入 allowednets。這樣就可以讓 PPPoE 也能享受 igmp proxy,提供用戶極致的影音服務。
4 則留言:
您好, 請教一下, 我目前在UPstream端收到igmp query後, proxy沒有自動送出 report, 以致於streaming在播一段時間後就被停止傳送了,
剛好就是在你寫的
case IGMP_MEMBERSHIP_QUERY:
// 收到 igmp query,總覺得應該要回應這個 query 比較好,可是實際操作沒問題,所以就當作不需要就好
return;
這個部份....
可以給我一點comment嗎, 謝謝
剛剛看最新的 code 仍然保持直接 return
https://github.com/pali/igmpproxy/blob/master/src/igmp.c
igmpproxy 主要的任務是替 downstream 轉發 MEMBERSHIP_REPORT 到 upstream 並建立 multicast route。
所以 igmpproxy 收到 downstream 的 MEMPERSHIP_REPORT 會轉發到 upstream。
我之前的測試環境在 upstream 的 switch 沒有啟用 igmpsnooping,media server 發出的 multicast 會一直發給每個 port,所以 igmpproxy 收到 MEMBERSHIP_QUERY 不處理也是能持續接收。
有一種可能是 igmpproxy 所在的主機發出 MEMBERSHIP_QUERY 的 interval 比 upstream 發出的間隔還要久,例如 upstream 可能 30s 就要一次,而 downstream 端可能 120s 才發一次,導致 downstream 回應 MEMBERSHIP_REPORT 的時效來不及向 upstream 反映。
可以考慮在 upstream 收到 MEMBERSIP_QUERY 的時候,呼叫 sendGeneralMembershipQuery() 向 downstream 發出,這樣 downstream 收到的 MEMBERSHIP_REPORT 應該就來得及回應給 upstream。
希望這方式能解決您的問題
我已經改好一版可以 forward MembershipQuery 的程式,請參考
https://github.com/aimwang/igmpproxy
https://github.com/pali/igmpproxy/pull/48
送出提交申請後,專案主持人提示 membership report 應該是由 kernel 回覆。
參考下面這篇文章,有很詳細的說明當 kernel 收到 igmp 封包的處置。
https://www.twblogs.net/a/5c1155b4bd9eee5e4183ad8e
我查看目前專案使用的 4.4.60 有兩處會送出 membership report
1. static void igmp_timer_expire(unsigned long data)
這個是在計時器到期時會送出
2. static void ip_mc_rejoin_groups(struct in_device *in_dev)
這個則是由 bonding 送出的 event 觸發執行。drivers/net/bonding/bond_main.c
static void bond_resend_igmp_join_requests_delayed(struct work_struct *work)
檢查已經註冊的 multicast 清單,並送出 event 要求 rejoin。
收到 membership query
igmp_rcv() 呼叫 int igmp_heard_query()
igmp_heard_query() 換檢查要不要重設計時器,if (changed) 呼叫 igmp_mod_timer()
igmp_mod_timer() 新的到期時間比舊的還短,就把舊的計時器刪除,建立新的計時器,所以新的計時器到期就會觸發上述 1. 的 callback function。
張貼留言