2018年9月18日 星期二

openwrt中使用ubus實現進程通信的原理

From:http://blog.csdn.net/jasonchen_gbd/article/details/45627967
ubus為openwrt平台開發中的進程間通信提供了一個通用的框架。它讓進程間通信的實現變得非常簡單,並且ubus具有很強的可移植性,可以很方便的移植到其他linux平台上使用。本文描述了ubus的實現原理和整體框架。
ubus源碼可通過Git庫git://nbd.name/luci2/ubus.git獲得,其依賴的ubox庫的git庫:git://nbd.name/luci2/ubox.git。

1. ubus的實現框架

ubus實現的基礎是unix socket,即本地socket,它相對於用於網絡通信的inet socket更高效,更具可靠性。unix socket客戶端和服務器的實現方式和網絡socket類似,讀者如果還不太熟悉可查閱相關資料。

我們知道實現一個簡單的unix socket服務器和客戶端需要做如下工作:
  1. 建立一個socket server端,綁定到一個本地socket文件,並監聽clients的連接。
  2. 建立一個或多個socket client端,連接server。
  3. client和server相互發送消息。
  4. client或server收到對方消息後,針對具體消息進行相應處理。
ubus同樣實現了上述組件,並對socket連接以及消息傳輸和處理進行了封裝:
  • 1.  ubus提供了一個socket server:ubusd因此開發者不需要自己實現server端。
  • 2.  ubus提供了創建socket client端的接口,並且提供了三種現成的客戶端供用戶直接使用:
1) 為shell腳本提供的client端。
2) 為lua腳本提供的client接口。
3) 為C語言提供的client接口。
可見ubus對shell和lua增加了支持,後面會介紹這些客戶端的用法。
  • 3.  ubus對client和server之間通信的消息格式進行了定義:client和server都必須將消息封裝成json消息格式
  • 4.  ubus對client端的消息處理抽像出“對象(object)”和“方法(method)”的概念。一個對像中包含多個方法,client需要向server註冊收到特定json消息時的處理方法。對象和方法都有自己的名字,發送請求方只需在消息中指定要調用的對象和方法的名字即可。
使用ubus時需要引用一些動態庫,主要包括:
  •  libubus.so:ubus向外部提供的編程接口,例如創建socket,進行監聽和連接,發送消息等接口函數。
  •  libubox.so:ubus向外部提供的編程接口,例如等待和讀取消息。
  •  libblobmsg.so,libjson.so:提供了封裝和解析json數據的接口,編程時不需要直接使用libjson.so,而是使用libblobmsg.so提供的更靈活的接口函數。
ubus中各組件的關係如下圖所示:

使用ubus進行進程間通信不需要編寫大量代碼,只需按照固定模式調用ubus提供的API即可。在ubus源碼中examples目錄下有一些例子可以參考。

2. ubus的實現原理

下面以一個例子說明ubus的工作原理:
下圖中,client2試圖通過ubus修改ip地址,而修改ip地址的函數在client1中定義。
client2進行請求的整個過程為:
1. client1向ubusd註冊了兩個對象:“interface”和“dotalk”,其中“interface”對像中註冊了兩個method:“getlanip”和“setlanip”,對應的處理函數分別為func1()和func2 ()。“dotalk”對像中註冊了兩個method:“sayhi”和“saybye”,對應的處理函數分別為func3()和func4()。
2. 接著創建一個client2用來與client1通信,注意,兩個client之間不能直接通信,需要經ubusd(server)中轉。
3. client2就是在前面講到的shell/lua/C客戶端。假設這裡使用shell客戶端,在終端輸入以下命令:
ubus call interface setlanip '{“ip”:“10.0.0.1”, “mask”:24}'
ubus的call命令帶三個參數:請求的對象名,需要調用的方法名,要傳給方法的參數。
4. 消息發到server後,server根據對象名找到應該將請求轉發給client1,然後將消息發送到client1,client1進而調用func2()接受參數並處理,如果處理完成後需要回复client2,則發送回复消息。

接下來介紹一下上述過程中,ubus內部的處理機制,雖然使用ubus進行進程間通信不需要關注這些實現細節,但有助於加深對ubus實現原理的理解。
下圖中,client1註冊對象和方法,其實可認為是服務提供端,只不過對於ubusd來講是一個socket client。client2去調用client1註冊的方法。

3. ubus的應用場景和局限性

ubus可用於兩個進程之間的通信,並以類似json格式進行數據交互。ubus的常見場景為:
  • “客戶端--服務器”形式的交互,即進程A註冊一系列的服務,進程B去調用這些服務。
  • ubus支持以“訂閱-- 通知”的方式進行進程通信,即進程A提供訂閱服務,其他進程可以選擇訂閱或退訂該服務,進程A可以向所有訂閱者發送消息。
由於ubus實現方式的限制,在一些場景中不適宜使用ubus:
  1. ubus用於少量數據的傳輸,如果數據量很大或是數據交互很頻繁,則不宜用ubus。經過測試,當ubus一次傳輸數據量超過60KB,就不能正常工作了。
  2. ubus對多線程支持的不好,例如在多個線程中去請求同一個服務,就有可能出現不可預知的結果。
  3. 不建議遞歸調用ubus,例如進程A去調用進程B的服務,而B的該服務需要調用進程C的服務,之後C將結果返回給B,然後B將結果返回給A。如果不得不這樣做,需要在調用過程中避免全局變量的重用問題。

4. ubus源碼簡析

下面介紹一下ubusd和ubus client工作時的代碼流程,這里為了便於理解,只介紹大致的流程,欲了解詳細的實現請讀者自行閱讀源碼。

4.1 ubusd工作流程

ubusd 的初始化所做的工作如下:
1. epoll_create(32)創建出一個poll_fd。
2.創建一個UDP unix socket,並添加到poll_fd的監聽隊列。
3.進行epoll_wait()等待消息。收到消息後的處理函數定義如下:
[cpp]  view plaincopy 
  1. static struct  uloop_fd server_fd = {   
  2. .cb = server_cb,  
  3. };  
即調用server_cb()函數。
4. server_cb()函數中的工作為:
(1)進行accept(),接受client連接,並為該連接生成一個client_fd。
(2)為client分配一個client id,用於ubusd區分不同的client。
(3)向client發送一個HELLO消息作為連接建立的標誌。
(4)將client_fd添加到poll_fd的監聽隊列中,用於監聽client發過來的消息,消息處理函數為client_cb()。
也就是說ubusd監聽兩種消息,一種是新client的連接請求,一種是現有的每個client發過來的數據。
當ubusd收到一個client的數據後,調用client_cb()函數的處理過程:
1.先檢查一下是否有需要向這個client回复的數據(可能是上一次請求沒處理完),如果有,先發送這些遺留數據。
2.讀取socket上的數據,根據消息類型(數據中都指定了消息類型的)調用相應的處理函數,消息類型和處理函數定義如下:
[cpp]  view plaincopy 
  1. static const  ubus_cmd_cb handlers[__UBUS_MSG_LAST] = {   
  2. [UBUS_MSG_PING] = ubusd_send_pong,  
  3. [UBUS_MSG_ADD_OBJECT] = ubusd_handle_add_object,  
  4. [UBUS_MSG_REMOVE_OBJECT] = ubusd_handle_remove_object,  
  5. [UBUS_MSG_LOOKUP] = ubusd_handle_lookup,  
  6. [UBUS_MSG_INVOKE] = ubusd_handle_invoke,  
  7. [UBUS_MSG_STATUS] = ubusd_handle_response,  
  8. [UBUS_MSG_DATA] = ubusd_handle_response,  
  9. [UBUS_MSG_SUBSCRIBE] = ubusd_handle_add_watch,  
  10. [UBUS_MSG_UNSUBSCRIBE] = ubusd_handle_remove_watch,  
  11. [UBUS_MSG_NOTIFY] = ubusd_handle_notify,  
  12. };  
例如,如果收到invoke消息,就調用ubusd_handle_invoke()函數處理。
這些處理函數可能是ubusd處理完後需要回發給client數據,或者是將消息轉發給另一個client(如果發送請求的client需要和另一個client進行通信)。
3.處理完成後,向client發送處理結果,例如UBUS_STATUS_OK。(注意,client發送數據是UBUS_MSG_DATA類型的)

4.2 client的工作流程

ubus call obj method的工作流程:
1.創建一個unix socket(UDP)連接ubusd,並接收到server發過來的HELLO消息。
2. ubus call命令由ubus_cli_call()函數進行處理,先向ubusd發送lookup消息請求obj的id。然後向ubusd發送invoke消息來調用obj的method方法。
3.創建epoll_fd並將client的fd添加到監聽列表中等待消息。
4. client收到消息後的處理函數為ubus_handle_data(),其中UBUS_MSG_DATA類型的數據receive_call_result_data()函數協助解析。
被call的client的工作流程:
和ubus客戶端的流程相似,只是變成了接受請求並調用處理函數

SYN cookies 機制下連接的建立

From: http://blog.csdn.net/justlinux2010/article/details/12619761 


在正常情況下,服務器端接收到客戶端發送的SYN包,會分配一個
連接請求塊(即request_sock結構),用於保存連接請求信息,並且發送SYN+ACK包給客戶端,然後將連接請求塊添加到半連接隊列中。客戶端接收到SYN+ACK包後,會發送ACK包對服務器端的包進行確認。服務器端收到客戶端的確認後,根據保存的連接信息,構建一個新的連接,放到監聽套接字的連接隊列中,等待用戶層accept連接。這是正常的情況,但是在並發過高或者遭受SYN flood攻擊的情況下,半連接隊列的槽位數量很快就會耗盡,會導致丟棄新的連接請求,SYN cookies技術可以使服務器在半連接隊列已滿的情況下仍能處理新的SYN請求
      如果開啟了SYN cookies選項,在半連接隊列滿時,SYN cookies並不丟棄SYN請求,而是將源目的IP、源目的端口號、接收到的客戶端初始序列號以及其他一些安全數值等信息進行hash運算,並加密後得到服務器端的初始序列號,稱之為cookie服務器端在發送初始序列號為cookie的SYN+ACK包後,會將分配的連接請求塊釋放。如果接收到客戶端的ACK包,服務器端將客戶端的ACK序列號減1得到的值,與上述要素hash運算得到的值比較,如果相等,直接完成三次握手,構建新的連接。YN cookies機制的核心就是避免攻擊造成的大量構造無用的連接請求塊,導致內存耗盡,而無法處理正常的連接請求
      啟用SYN cookies是通過在啟動環境中設置以下命令完成:
echo 1 > /proc/sys/net/ipv4/tcp_syncookies
      注意,開啟該機制並不意味著所有的連接都是使用SYN cookies機制來完成連接的建立只有在半連接隊列已滿情況下才會觸發 SYN cookies機制由於SYN cookies機制嚴重違背TCP協議不允許使用TCP擴展,可能對某些服務造成嚴重的性能影響(如SMTP轉發),對於防禦SYN flood攻擊的確有效。對於沒有受到攻擊的高負載服務器,不要開啟此選項,可以通過修改tcp_max_syn_backlogtcp_synack_retriestcp_abort_on_overflow系統參數來調節。
      下面來看看內核中是怎麼通過SYN cookie機制來完成連接的建立。
      客戶端的連接請求由tcp_v4_conn_request()函數處理。tcp_v4_conn_request()中有一個局部變量want_cookie,用來標識是否使用SYN cookies機制。want_cookie的初始值為0,如果半連接隊列已滿,並且開啟了tcp_syncookies系統參數,則將其值設置為1,如下所示:
int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
{
#ifdef CONFIG_SYN_COOKIES
    int want_cookie = 0;
#else
#define want_cookie 0 /* Argh, why doesn't gcc optimize this :( */
#endif 
... 
    /* TW buckets are converted to open requests without
     * limitations, they conserve resources and peer is
     * evidently real one.
     */
      if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
#ifdef CONFIG_SYN_COOKIES
             if (sysctl_tcp_syncookies) {
            want_cookie = 1;
        } else
#endif       
        goto drop;
    } 
... 
drop:
    return 0;
}
      如果沒有開啟SYN cookies機制,在半連接隊列滿時,會跳轉到drop處,返回0 。在調用tcp_v4_conn_request()的tcp_rcv_state_process()中會直接釋放SKB包。
      我們前面提到過,造成半連接隊列滿有兩種情況(不考慮半連接隊列很小的情況),一種是負載過高,正常的連接數過多;另一種是SYN flood攻擊。如果是第一種情況,此時是否繼續構建連接,則要取決於連接隊列的情況及半連接隊列的重傳情況,如下所示:
if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1)
        goto drop;
      sk_acceptq_is_full()函數很好理解,根據字面意思就可以看出,該函數是檢查連接隊列是否已滿。inet_csk_reqsk_queue_young()函數返回的是半連接隊列中未重傳過SYN+ACK段的連接請求塊數量。如果連接隊列已滿並且半連接隊列中的連接請求塊中未重傳的數量大於1,則會跳轉到drop處,丟棄SYN包。如果半連接隊列中未重傳的請求塊數量大於1,則表示未來可能有2個完成的連接,這些新完成的連接要放到連接隊列中,但此時連接隊列已滿。如果在接收到三次握手中最後的ACK後連接隊列中沒有空閒的位置,會忽略接收到的ACK包,連接建立會推遲,所以此時最好丟掉部分新的連接請求,空出資源以完成正在進行的連接建立過程。還要注意,這個判斷並沒有考慮半連接隊列是否已滿的問題。從這裡可以看出,即使開啟了SYN cookies機制並不意味著一定可以完成連接的建立
      如果可以繼續連接的建立,調用inet_reqsk_alloc()分配連接請求塊,如下所示:
req = inet_reqsk_alloc(&tcp_request_sock_ops);
    if (!req)
        goto drop;
      看到這裡可能就有人疑惑,既然開啟了SYN cookies機制,仍然分配連接請求塊,那和正常的連接構建也沒有什麼區別了。這里之所以要分配連接請求塊是用於發送SYN+ACK包給客戶端,發送後會釋放掉,並不會加入到半連接隊列中。
      接下來就是計算cookie的值,由cookie_v4_init_sequence()函數完成,如下所示:
if (want_cookie) {
#ifdef CONFIG_SYN_COOKIES
        syn_flood_warning(skb);
        req->cookie_ts = tmp_opt.tstamp_ok;
#endif
        isn = cookie_v4_init_sequence(sk, skb, &req->mss);
    }
      計算得到的cookie 值會保存在連接請求塊tcp_request_sock 結構的snt_isn 成員中,接著會調用__tcp_v4_send_synack() 函數發送SYN+ACK 包,然後釋放前面分配的連接請求塊,如下所示:
if (__tcp_v4_send_synack(sk, req, dst) || want_cookie)
        goto drop_and_free;
      在服務器端發送完SYN+ACK包後,我們看到在服務器端沒有保存任何關於這個未完成連接的信息,所以在接收到客戶端的ACK包後,只能根據前面發送的SYN+ACK包中的cookie值來決定是否繼續構建連接。
      我們接下來看接收到ACK包後的處理情況。ACK包在tcp_v4_do_rcv()函數中調用的tcp_v4_hnd_req()中處理,如下所示:
static struct sock *tcp_v4_hnd_req(struct sock *sk, struct sk_buff *skb)
{
    ... 
#ifdef CONFIG_SYN_COOKIES
    if (!th->rst && !th->syn && th->ack)
        sk = cookie_v4_check(sk, skb, &(IPCB(skb)->opt));
#endif
    return sk;
}
      由於在服務器端沒有保存未完成連接的信息,所以在半連接隊列或ehash 散列表中都不會找到對應的sock 結構。如果開啟了SYN cookies 機制,則會檢查接收到的數據包是否是ACK 包,如果是,在cookie_v4_check() 中會調用cookie_check() 函數檢查ACK 包中的cookie 值是否有效。如果有效,則會分配request_sock 結構,並根據ACK 包初始化相應的成員,開始構建描述連接的sock 結構。創建過程和正常的連接創建過程一樣。
      問題就出在TCP連接的三次握手中,假設一個用戶向服務器發送了SYN報文後突然死機或掉線,那麼服務器在發出SYN,ACK應答報文後,是無法收到客戶端的ACK報文的(第三次握手無法完成),這種情況下服務器端一般會重試(再次發送SYN,ACK給客戶端),並在等待一段時間後丟棄這個未完成的連接,這段時間的長度我們稱為SYN Timeout一般來說這個時間是分鐘的數量級(大約為30秒~ 2分鐘);一個用戶出現異常導致服務器的一個線程等待1分鐘並不是什麼很大的問題,但如果有一個惡意的攻擊者大量模擬這種情況,服務器端將為了維護一個非常大的半連接列表而消耗非常多的資源-- 數以萬計的半連接,即使是簡單的保存並遍歷也會消耗非常多的CPU時間和內存,何況還要不斷對這個列表中的IP進行SYN,ACK重試實際上如果服務器的TCP/IP棧不夠強大,最後的結果往往是堆棧溢出崩潰--即使服務器端的系統足夠強大,服務器端也將忙於處理攻擊者偽造的TCP連接請求而無暇理睬客戶的正常請求(畢竟客戶端的正常請求比率非常之小),此時從正常客戶的角度看來,服務器失去響應,這種情況我們稱作:服務器端受到了SYN Flood攻擊(SYN洪水攻擊)。 
從防禦角度來說,有幾種簡單的解決方法,
  • 第一種是縮短SYN Timeout時間由於SYN Flood攻擊的效果取決於服務器上保持的SYN半連接數,而SYN半連接數= SYN攻擊頻度* SYN Timeout ,所以通過縮短從接收到SYN報文,到確定該報文無效並丟棄該連接的時間,可以成倍的降低服務器的負荷。例如設置為20秒以下(過低的SYN Timeout設置可能會影響客戶的正常訪問)。
  • 第二種方法是設置SYN Cookie就是給每一個請求連接的IP地址分配一個Cookie ,如果短時間內連續收到來自某個IP的重複SYN報文,就認定是受到了攻擊,以後從這個IP地址來的包會被一概丟棄。
      可是,上述的兩種方法只能對付比較原始的SYN Flood攻擊,縮短SYN Timeout時間僅在對方攻擊頻度不高的情況下生效;而SYN Cookie更依賴於對方使用真實的IP地址。如果攻擊者以數万/秒的速度發送SYN報文,同時利用SOCK_RAW隨機改寫IP報文中的源地址,以上的方法將毫無用武之地。
      一般來說,如果一個系統(或主機)負荷突然升高甚至失去響應,使用netstat命令能看到大量SYN_RCVD的半連接(數量>500或占總連接數的10%以上),可以認定,這個系統(或主機)遭到了SYN Flood攻擊。
      遭到SYN Flood攻擊後,首先要做的是取證,通過netstat –natp >result.txt記錄目前所有TCP連接狀態是必要的,如果有嗅探器,或者tcpdump之類的工具,記錄TCP SYN報文的所有細節也有助於以後追查和防禦,需要記錄的字段有:源地址、IP首部中的標識、TCP首部中的序列號、TTL值等,這些信息雖然很可能是攻擊者偽造的,但是用來分析攻擊者的心理狀態和攻擊程序也不無幫助。特別是TTL值,如果大量的攻擊包似乎來自不同的IP但是TTL值卻相同,我們往往能推斷出攻擊者與我們之間的路由器距離,至少也可以通過過濾特定TTL值的報文降低被攻擊系統的負荷(在這種情況下TTL值與攻擊報文不同的用戶就可以恢復正常訪問)
到目前為止,能夠有效防範SYN Flood攻擊的手段並不多,而SYN Cookie就是其中最著名的一種。

syn flood攻擊代碼:
http://www.cnblogs.com/rollenholt/articles/2590970.html 
http://rshell.blog.163.com/blog/static/416191702007923999492/ 
https://github.com/polyrabbit/ tcp-syn-flood

How to use simple speedtest in RaspberryPi CLI

  pi@ChunchaiRPI2:/tmp $  wget -O speedtest-cli https://raw.githubusercontent.com/sivel/speedtest-cli/master/speedtest.py --2023-06-26 10:4...