2021年10月3日 星期日

經營者的九大口訣

一直向前,不要回頭。

二話不說,即知即行。

三分熱度,無法達標。

四荒八極,躲到虛擬。

五體投地,自尊不計。

六問三推,藉口一堆。

七老八十,終身學習。

八方風雨,也要前進。

九五之尊,抱負崇高。

2021年6月30日 星期三

XMPP for TR069

 $1 前言

CWMP (TR-069) 的基本原則之一是 CPE Client永遠是聯線的發起者,但ACS依然有許多 TR-069 連接請求,CPE收到之後,開始會話。這通常在ACS有事的時候,必須立即聯繫 CPE 時使用,例如ACS系統想要將一個新的服務,導入到設備上的時候,通常要主動發起連線。

TR069標準文件中的Annex K中,定義了讓ACS使用XMPP來啟動與Device連線的方法。

$1.1 STUN的使用

在最初設計CWMP的時候,主要執行於客戶端的Gateway。由於這些設備可直接在 Internet 上擁有合法的的IP地址,因此如果讓設備提供一個公開可訪問的 URL,ACS便可以輕易的發出請求(Request),如此便輕易的將TR-069 連接請求傳送到網路設備。

基本的連接請求是通過HTTP GET 方法對CPE 指定的URL 完成的(這個URL 需要在每個Inform 訊息(Message)中發送給ACS,以便ACS 始終知道當前的URL 是什麼)。成功後,設備使用Inform 方法的事件列表中的6 CONNECTION REQUEST事件啟動 CWMP 會話。

然而,隨著越來越多的設備可以通過 TR-069 進行管理,位於Gateway後面的裝置越來越多,這些裝置要透過NAT Mapping,才能提供一個可以連接URL。

TR-069 的 Annex G文件中,定義了一種使用 STUN (Session Traversal Utilities for NAT)協議的方法,以允許這些NAT之後的設備,通過網關接受 HTTP GET 請求。

但STUN 並不是最好的解決方案。最大的問題是STUN使用 UDP,ACS並不能保證它的Request可以到達Device。

$1.2使用來XMPP 來繞過 NAT

隨著更新協議的出現,Broadband Forum的TR-069 開發人員開始使用(XMPP: eXtensible Messaging and Presence Protocol ) 建立一個 Message Bus,以允許發生連接請求可以隨時發生。並在Annex K of TR-069 Amendment 5中進行了完整的定義。

要使用此功能,CPE必須支援CWMP Data Model 的 XMPPBasic:1 和 XMPPConnReq:1 profile。

詳細的定義請參考以下連結:https://cwmp-data-models.broadband-forum.org/tr-157-1-8-0.html#R.TR-069a5

在解釋如何工作之前,先要對XMP的工作與訊息傳遞格式,作一些基礎性的說明。

$2. XMPP名詞解釋

$2.1 JabberID : XMPP的識別位址寫法

簡稱JID(Jabber Identifier or JabberID)。JabberID由三個主要元素構成。node、domain、resource。格式與e-Mail類似,為 user@domain/resource

常見樣子如下: alex@example.com/home 

下面是一個例子:t004@of3/663f243

格式

意義

說明

t004

node

node就是帳戶名稱。在Query的時候,也可以是聊天室。

Of3

Domain  

domainJabberID中大部分指的是伺服器名稱,有時我們也會寫成短名稱如of3.xmpp.tti.tv只寫 of3

663f243

Resource

JabberID中的resource部分,大部分時候它代表XMPP Client端連線的識別,每個連線的client會賦予唯一的resource作為識別。


$2.1 XML stream 與 XML stanza。

由stream內包含不同作用的stanzas區塊,不同區塊標籤,有不同功能作用。Stanzas 區塊有 <presence />、<message />、<iq />或 三個區塊,麻煩參考一下的例子:


在XMPP的Client <-> Server, Server<-> Server之間,透過傳送與接收不同的XMPP stanzas,達成各種不同任務。

以上圖為例,t002用戶,建立了一個XMPP stream,送出<stream:stream>標籤。

第一個標籤<iq>,要求t002的roster (聯絡人或中文翻為好友清單)。
接著傳送<presence>,告知伺服器他目前是online,且是avaiable。
接著傳送<message>,給t004送了一封短訊。
然後再以<presence>,知會伺服器,t002已經unavaiable。
最後以<stream:stream>結束關閉這段通信。

下面簡單的看一下這三個XML stanza。

三個XMPP stanzas有一些共通屬性元素。

id:<presence/>、<message/>、<iq/>都可以有這個屬性,但通常多見於<iq/>,因為對於<iq />而言,它是必要屬性。而其他為非必要屬性。其最大用處在於辨識傳送接收雙方stanza的對應,以免答非所問。
from:訊息發送來源,他的屬性(Type)是由SERVER端提供。
to:  訊息發送目標
type:在三個不同的stanzas中,會有不同值。

$2.1.1 <iq />

<iq />在 http://xmpp.org/rfcs/rfc6120.html#stanzas-semantics-iq 8.2.3 有詳細值列表定義。Iq是Info/Query的縮寫,包含 from/to/id/type。

<iq /> 表示Info/Query,使用<iq />必然是有問有答,這可以為XMPP通信提供一個比較可靠的請求與回應機制,類似於GET/POST/PUT方法。IQ只能包含一個payload。

IQ中包含有id Type,用於識別伺服器發回的回應。

type

說明

get

用於請求資訊,類似於HTTP Get

set

提供資訊或請求,類似於HTTP POST/PUT

result

回應請求,類似於HTTP 200

error

錯誤資訊



面有一個例子,發送取得Contact List 的需求:
<iq from=alice@wonderland.lit/pda id="rr82a1z7" to=alice@wonderland.lit type="get">
<query xmlns="jabber:iq:roster"/>
</iq>
XMPP伺服器返回Contact List

<iq from=alice@wonderland.lit id="rr82a1z7" to=alice@wonderland.lit/pda type="result">
    <query xmlns="jabber:iq:roster">
        <item jid="whiterabbit@wonderland.lit"/>
        <item jid="lory@wonderland.lit"/>
        <item jid="mouse@wonderland.lit"/>
        <item jid="sister@realworld.lit"/>
    </query>
</iq>            

Client新增一個連絡人jid="madhatter@wonderland.lit"

<iq from=alice@wonderland.lit/pda id="ru761vd7" to=alice@wonderland.lit type="set">
    <query xmlns="jabber:iq:roster">
        <item jid="madhatter@wonderland.lit"/>
    </query>
</iq>

伺服器回應說OK

<iq from=alice@wonderland.lit id="ru761vd7" to=alice@wonderland.lit/pda type="result"/>

$2.1.2 <presence />

<presence />值定義在 http://xmpp.org/rfcs/rfc3921.html 的 2.2.1 有詳細定義。

Type= Unavailable/subscribe/subscribed/unsubscribe/unsubscribed/probe/error

<presence />是用來說明Device的可訪問性。用戶發出<presence />,表明自己上線。但是我們也不用擔心任何人都可以看到自己的線上狀態,除非我們訂閱了該使用者的狀態,訂閱之後,使用者的狀態資訊會自動發送到訂閱者處。
<presence /> 也提供subscribe/subscribed/unsubscribe/unsubscribed。意思是訂閱或是不要訂閱。

當USER設定自己是<presence />之後,如果其他使用者訂閱(subscribe)了該使用者的狀態,使用者的上線或離線狀態資訊,會自動發送到訂閱者處。

<iq type=’get’>
       <query xmlns=’jabber:iq:roster’ />  //Get Client的Contact List(術語就是roster)
</iq>
<presence/>  //通知伺服器,Client已線上並隨時可以接收訊息

在一個XMPP連線之中,一開始大部分用上述的stanza來開頭,這時候<iq /> 發送之後,系統會回應一堆的JID Contact List,以及Client與這些JID的訂閱關係。接下來Client發送presence節,通知自己線上,以及已經得到連絡人的狀態資訊。

$2.1.3 < message />

<message />值定義在 http://xmpp.org/rfcs/rfc3921.html 的 2.1.1 有詳細值列表定義。type包含 chat/error/groupchat/headline/normal

用於從一個Client向另外一個Client發送消息,並可以傳輸任何類型的結構化資訊(XML),不保證傳輸可靠性。

CPE 必須能夠與 XMPP Server建立安全,且經過身份驗證的連接。
由於CPE保持與XMPP Server的連接,XMPP Server可以從ACS隨時發送一組來自與”允許地址”、未經請求的訊息(Message)給XMPP Server,XMPP Server 再發送給CPE。

Annex K之中,定義了要使用XMPP ( RFC 6120)發送的一組基於XMPP 的ConnectionRequests (TR069 3.2.2 節)的特定訊息(Message)。流程如下:




逐步說明如下

(1) ACS 與 XMPP Server 建立連接

前面已經大致上描述了下圖的架構:

ACS 想要用HTTP 發ConnectionRequests給CPE,但是通常CPE都在Gateway的後面,被NAT或防火牆給擋住了。為了解決這個問題,TR-069 Annex K 定義了一種通過 XMPP Server執行連接請求的方法。

ACS作為系統端服務,自己當然要先與XMPP Server隨時保持連線。第一步就ACS與XMPP Server要建立連線。

ACS通過配置XMPP.Connection object與ManagementServer Object 的ConnReqXMPPConnection 參數,來連接XMPP SERVER,提供CPE的XMPP連線。
XMPP.Connection object,可選擇性的使用 ManagementServer Object 的ConnReq AllowedJabberIDs參數,其中明訂Jabber ID列表,來表中的CPE可以發動XMPP連接請求。

當XMPP連接請求發送到CPE時,XMPP IQ Stanza將包含一個”from”地址,其中包含發起實體的Jabber ID。

此參數是一個逗號分隔的字符串列表(最多32個項目,最大項目長度256)。每個條目代表一個Jabber ID或地址,允許發起XMPP連接請求。

每個 Jabber ID 都可以是”Full JID” (包含以下格式的local-part @ domain-part / resource-part)或”Bare JID” (local-part@domain-part)。


具例如下:如果ConnReqAllowedJabberID包含”ACS1@tr069.example.com, ACS2@tr-069.example.com/resource1”,將允許以下JID傳入:

“ACS1@tr069.example.com/resource1”
“ACS1@tr069.example.com/resource2”
“ACS2@tr069.example.com/resource1”

並且不允許以下JID傳入:

“ACS2@tr069.example.com/resource2”
“ACS@tr069.example.com/resource”

如果此參數為空字串,則允許所有Jabber ID連接。並且XMPP連接請求不會因”from”地址而被視為無效。詳細的物件資訊可參考一下連結:

https://cwmp-data-models.broadband-forum.org/tr-157-1-8-0.html#D.TR-157:1.ManagementServer.

(2) ACS通過配置XMPP使能在Device中使用XMPP

基本上一定是CPE對ACS來啟動連接一個連線作業。這算是一個標準的啟動程序:
CPE -> ACS : Inform (0 BOOTSTRAP or 1 BOOT)
ACS -> CPE:InformResponse
CPE -> ACS:Empty HTTP POST
ACS -> CPE:SetParameterValues
CPE -> ACS:SetParameterValuesResponse
ACS -> CPE:HTTP204

這時候由於是CPE主動找ACS,所以完全沒有NAT與防火牆的問題。這時候ACS告訴CPE如何連接XMPP Server的資訊。

ACS要將以下資訊通知CPE,這上圖中是使用SetParameterValues,但也可以使用AddObject:
- 使用XMPP.Connection Object的ManagementServer參數通知CPE。
- 選擇性的使用ManagementServer Object來設定SERVER接受的Jabber ID。

當然如果CPE在預設值中,先設定了以上的XMPP連接值,則可以省略此步驟。

A.下圖是描述ACS與CPE之間使用AddObject來傳送:

ACS想要對CPE啟動XMPP Connection Request,ACS首先在CPE’s XMPP interface object中建構一個新的XMPP.Connection object。ParameterKey為ACS提供了一種可靠且可擴展的方法來處理接下ACS所做的更新。這裡的ParameterKey 值,在接下的動作,例如SetParameterValues、AddObject或DeleteObject等都要使用這個唯一ParameterKey參數值作為識別。CPE在Factoory Reset時,ParameterKey的值必須設置為空字符串。

B. 除了用AddObject之外,也可以用SetParameterValues。


這裡是透過Device的data model 中ManagementServer object 的ConnReqXMPPConnection parameter來告訴CPE需要連線的Path Name。位於<Value>當中的 Device.XMPP.Connection.1

透過這個CWMP呼叫,CPE就知道XMPP Server的路徑在哪裡了。接下來ACS還要告訴Devices連線用的 JabberID。



利用以上這個SetParameterValues,Devices就知道自己的

JabberID = acs@provider.com/resource1

Question: 如何知道是 “resource1” ?

ACS可以在ConnReqAllowedJabberID 參數中,設定允許連接的 JabberID。ACS在之後的XMPP message中使用的”from” ID必須與這些JabberID之一匹配才能被CPE接受。

如果CPE已經被Connection Requests設定了一個 XMPP Server,則可以省略此步驟。

(3) Establish Connection to XMPP Server

ACS與XMPP Server建立一個Secure XML Stream。CPE與XMPP Server也建立一個Secure XML Stream。接下來Device連接到XMPP Server,可以用前面得到的JabberID來建立與ACS Server連接。




CPE會在ConReqJabberID參數中,列出了它的Jabber ID,並且必須確保與 XMPP.Connection object中的JabberID參數匹配。此參數預設為主動通知 (Active Notification),因此ACS可以在不同的session中,學習它或使用GetParameterValues 請求它。

當連線建立之後,ACS可以使用 XMPP 與CPE 進行”Chat”,並向CPE發送 XMPP連接請求。

此 XMPP 連接請求包含一個 XMPP IQ stanza ,其中包含分配給 CPE 的連接請求用戶名和連接請求密碼的元素。

一旦發送,CPE 用一個空的 XMPP IQ stanza 確認請求。

一旦成功,CPE 就開始與當前在 ManagementServer.URL 參數中分配的 ACS 的 CWMP 會話。CWMP 然後照常繼續。

如果出現問題,或者 CPE 正在處理類似於 HTTP 連接請求中描述的過多請求,CPE 可以發送帶有適當錯誤類型元素的 IQ 節。

如果 XMPP 連接請求不能被認證,CPE 使用“未授權”錯誤代碼。

4. XMPP IQ (Connect Request):ACS -> XMPP Server

每當ACS希望與CPE建立連接時,它可以發送XMPP連接請求(包含連接請求訊息(Message)的XMPP IQ節到其XMPP Server,指定與連接請求需要的 CPE 匹配的”to”地址和一個與允許的Jabber ID之一匹配的”發件人”地址 - 有關詳細信息,請參閱K.2.2.2)到XMPP Server。



5. XMPP IQ (Connect Request):XMPP Server-> Client (Device)

XMPP Server轉發給Client

6. XMPP OQ (Connection Request ACK) : Client(Device) -> XMPP Server

7. XMPP OQ (Connection Request ACK) : XMPP Server -> ACS

XMPP Server將 XMPP IQ 節發送到適當的設備。

8. Inform (6 COOECTION REQUEST)

當XMPP完成到上一步(7)時,CPE便像原先一樣,通過 HTTP 發起與 ACS 的 CWMP 對話,Device向其 anagementServer.URL 參數中指定的 ACS 發出通知請求。由於ACS通過XMPP向CPE傳遞連接請求,因此通過NAT的時候,無需擔心網路地址轉換。



注:XMPP 連接請求機制通常被描述為使用單個 XMPP Server,但根據部署可能有多個 XMPP Server。一些示例是部署具有多個集群在一起的 XMPP Server的單個域,或者部署多個域,其中每個域由一組 XMPP Server組成,並且它們都可以相互通信。

ACS先與XMPP Server建立連接,然後用 TR-069 通知 CPE 可以使用此 XMPP Server來進行其連接請求。然後,CPE 與XMPP Server建立安全連接。從那時起,ACS 可以通過 XMPP Server向 CPE 的Jabber ID 傳送連接請求。此連接請求只是一個 XMPP IQ Stanza ,其中包含一個到其 XMPP Server的連接請求訊息(Message),指定與需要發送連接請求的 CPE 匹配的“to”地址和與允許的 Jabber ID 之一匹配的“from”地址。






2021年5月3日 星期一

Opensync 管理器 (Managers) 簡介

Opensync 管理器 (Managers) 簡介

OpenSync由十九個獨立的程式組成,每個程式有其獨立的功能。由於功能上的相關性,可以分為以下四組,並且依照順序啟動。當然如果熟悉了之後,可以有選擇地不要啟動列為”Optional”的管理程式。這取決於完整系統定義的功能。

下面就是這些程式的簡單說明,之後每一個Manager會有專章進行討論:

1. OpenSync 的基礎程式

1-a. Diagnostics Manager (DM)

DM (診斷經理)是第一個需要啟動的程式。它負責啟動其餘的OpenSync管理器,並有選擇性監控它們。這裡說的選擇性是DM有兩個程式介面,可以在啟動與停機時,呼叫廠商掛上的監控程式。

DM除了啟動其他管理程式外,如果這些管理程式掛了,他還會產生 Crash Report,裡面會記錄當機時間、程式的PID、當掉的理由等等;這些報告會透過MQTT送到雲端的系統中。除了本機之外,DM也可以收集網路節點的當機資訊(例如何時當機、當機頻率等等)。

1-b. Connection Manager (CM(2))

CM2負責建立主幹(Backhaul)連接,並通過IPv4或IPv6保持與雲端管理系統的連接。CM使用以下步驟來與雲端管理系統連接:

1. 上行網路(Uplink)的選擇,依照設備類型(Gateway or Extender)有不同的方法):

Gateway: 對於網關設備,上行鏈路始終是電纜端口(以太網,光纖,同軸電纜等)。在這種情況下,不使用特殊邏輯,僅監視與控制器的連接。

Extender: 在這種情況下,CM要選擇適當的上行鏈路端口,也許是有線端口(RJ45)或Wi-Fi Backhaul。

2.從AWLAN_Node OVS表中的redirector_addr得到 host name的IP地址。

3.建立與 redirector的連接。

4.通過 redirector 得到雲端管理系統的IP地址。

5.將CM連接到雲端管理系統。

6.監視鏈路穩定性 (會不會一直重複發生斷線的狀況)。

c. Captive Portal Manager (CPM)

這個功能叫做Guest Network,就是家裡面如果有客人來,例如客人、佣人、褓姆臨時要上網,你不想把家裡密碼給客人使用,就可以用這個功能。

Captive Portal是一個啟動頁面,會在訪客用戶設備上自動打開,提示他們通過開放的訪客Wi-Fi登錄,登錄成功之後就可以上網。由於此功能在應用程程層次上執行,因此節點(Node)和控制器(Controller)必須包括在較低層上建立適當連接方法。此節點獲得用戶憑據後,即可使用AAA Server來做客戶端管理。

CPM在OpenSync設備上扮演代理服務的角色,該服務可對在Wi-Fi網路切割獨立的部分,對第三方(來賓)的客戶端裝置上進行身份驗證。來賓客戶端設備通過外部Captive Portal服務器進行身份驗證。

CPM具有以下角色:

− 識別需要進行身份驗證的流量。

− 將流量路由到代理服務。

− 將代理服務從收到的HTTP port 80流量傳遞到雲端系統中運行的UAM (Universal Access Method) / NAS服務。

如果對於這裡運作有疑問,下面連接有不錯的範例與解釋:

https://help.keenetic.com/hc/en-us/articles/360000501439-Captive-portal

CPM 預設是關閉的。

1-d. WAN Orchestrator (WANO)

WAN Orchestrator(WANO)負責裝置的Internet連線。WANO啟用設備WAN Port,並啟動必要的服務以啟用Internet連接。

以下是WANO連接網路的步驟:

(1). 建立連接界面:WANO支持以下接口:以太網、VLAN和PPPoE。

(2). 端口角色檢測和WAN探測:

a. WANO自動配置Ethernet Port,並設定Port角色,這裡會啟用自動檢測功能,找出以下三種角色的其中之一:WAN 連接、Ethernet 主幹、Ethernet的延伸。

b. 在WAN設置之前,啟動保全機制,並啟用防火牆規則。

C. 在可用的WAN接口上,啟動的WANO probing (探針):

-  在設備上沒有對WAN進行的設定的話,WANO會預設為DHCP探測。
-  如果裝置設定為靜態IP地址,WANO探測已知的互聯網主機。
-  WAN設定為PPPoE或VLAN。

(3).網路配置:WANO負責在設備上進行WAN後續設定。

2.基本的網路架構設定

2-a. Wireless Manager (WM2)

WM2負責Wi-Fi子系統(例如VAP (Virtual Access Point)、SSID /密碼、主幹(Backhaul)、Channel等)的設置和配置。WM2還負責監視Wi-Fi狀態和客戶端連接/斷開連接。

一開始啟動的時候,WM2會讀取當前設備的配置,並更新相關的Config和State表。雲端管理系統使用型號名稱的無線頻率與介面接口,來正確設置無線裝置的參數。WM2具有以下角色:

  • 在以下表中的資訊不匹配時,呼叫Target API來進行設備設定:
    • Wifi_Radio_Config與Wifi_Radio_State
    • Wifi_VIF_Config與Wifi_VIF_State

  • 通過Wifi_Associated_Clients Table,報告已連接的Client與其Metadata。
  • 每當STA VIF 連接狀態改變的時候,更新Wifi_Master_State表中的port_active欄位,並通知CM。這功能用於Wi-Fi Extender的啟動和上行網路連接設定。

2-b. Network Manager (NM)

NM負責管理所有與網路相關的配置和網路狀態報告,這是功能比較多的Manager。主要功能包含:

    • 管理IPv4 / IPv6地址(Static、DHCP)
    • 創建和終結網絡接口(Wi-Fi接口除外,請參閱WM(Wireless Manager))
    • 配置網路接口的參數(MTU、Up / Down State)
    • 管理DNS和DNSMASQ服務
    • 管理防火牆規則(filtering, port forwarding)
    • 啟動/停止各種網路服務 (UPnP、DHCP Server and Client)
    • 管理GRE隧道(GRE Tunnels)
    • 管理橋接的接口(Bridge Interface)
    • 關聯Client端的DHCP fingerprint reporting
    • 管理DHCP保留的IP

NM可以管理OVS 橋接器、傳統橋接器 (缺少HomePass™之類的功能)、網絡接口及其配置、Wi-Fi接口 (Access Point 和 Station)。

Cloud schema是使用擴充的OVSDB schema,該schema已經提供了配置OVS橋接器的所有必要方法。但有時Bridge裝置的上傳介面是Wi-Fi,但如果不與Wi-Fi層進行互動,則無法完全配置Wi-Fi接口。NM在Wi-Fi管理部分必須要與WM偕同一致以提供完整的功能。

2-c. Netfilter Manager (NFM)

Netfilter 一個User Space的Linux公用程式,使用iptable來控制封包與流向(Data Flow)。NFM藉由 Netfilter 提供以下功能:

− 封包過濾(Packet filtering),防火牆功能(Firewalling)
− 封包的紀錄(Packet logging)
− 封包的排隊(Packet queueing)
− 封包的處理)Packets mangling) (例如依照封包的標頭和標記做一些特別的處理)。
− 網路和端口地址轉換(Network and port address translation)
− 讀取、解析NFLOG數據包,然後用JSON格式,透過MQTT發送給雲端系統。

2-d. QoS Manager - QoSM

QoS 設定網路封包的處理先後順序,例如語音、視訊要有優先通過,以維持比較好的用戶體驗。這些設定包含:

− Queue的先後順序(Queue priorities)。
− 保留給Queue的最大頻寬(Maximum queue bandwidth)。
− 封包的優先順序(Packet prioritization)。 

3. Optional, needed for additional features

3-a. Queue Manager (QM)

QM負責緩衝來自SM、BM和FCM的MQTT訊息。即使Up Link連接斷開(例如主幹網路瞬時故障、ISP服務中斷等等),也嘗試不丟棄任何訊息。訊息使用Google的protocol buffers進行編碼。報告內的多條訊息將合併為一條訊息。 其他管理器使用Unix Socket與QM進行通信。
Google的protocol buffers 請參考一下連結:
https://developers.google.com/protocol-buffers/docs/proto

3-b. Statistics Manager (SM)

統計管理器(SM)負責處理所有有關於無線網路的統計請求(Request),並將結果發送到雲端管理系統。當MQTT用於數據平面時,配置通過OVSDB完成。
以下用一個收集客戶端的頻率為例,來說明說明收集和發送統計信息的過程(不是真實的統計數據,僅是用來舉例說明用的):
雲端管理系統可以設置了一個計時器,以10秒的頻率,收集一次客戶端的頻率信息,另一個計時器用於每120秒發送一次客戶端報告。如果發生Timeout,便呼叫Target Layer(API) 測量客戶端裝置的頻率。
Target Layer(API) 也許會呼叫Kernel API讀取相關數值。Target Layer(API)還負責將其測量的數據資料結構轉換為SM期望的格式(格式定義於OpenSync的Data Pipeline(簡寫為DPP) Library中)。
各種量測值會存儲到報告中,也會與其他量測的數據合併在一起,並可進行進一步處理和操作。當報告的計時器timeout時,該報告被發送到QM (Queue Manager),QM 將來自不同管理器的報告合併為一個報告,透過MQTT發送到雲端管理平台。

3-c. Band steering Manager (BM)

負責根據每個客戶端的配置、Wi-Fi信號/雜訊的強度和雲端管理系統提供的觸發條件(Trigger),將Wi-Fi裝置引導到不同的頻段(2.4GHz or 5GHz稱之為band steering)或不同的AP(稱之為Client Steering)。這是Smart Mesh之中最基本的功能。

3-d. Log Manager (LM – deprecated, included as a part of PM plugin)

Log Manager負責根據Cloud請求(logpull)收集和上載裝置的日誌和系統信息,並依照日誌的嚴重性設定處理的模組。

3-e. Platform Manager (PM)

PM負責:
- 設備GUI和雲端管理系統之間的暱稱同步。
- 雲端管理系統通知設備凍結(Parental Control功能)(OpenFlow must disabled)
- 通過調節風扇轉速來進行設備的熱度管理
- 設備的LED管理。

3-f. Exchange Manager (XM)

Exchange manager 主要執行OVSDB 與其他管理系統之中交換資料(例如與 TR-069/369 之間)。基本上這是一個雙向的同步功能(e.g., OVSDB <-> TR-181)。

3-g. Upgrade Manager (UM)


收到來自雲端管理系統的觸發後,UM確保設備韌體升級過程完成。 雲端管理系統為所選客戶位置上的所有節點精心安排了韌體升級過程。
雲端管理系統使用以下命令啟動更新:
  • - 可用的韌體image URL
  • - 雲端管理系統期望韌體下載的下載時間(例如半夜三點開始下載升級)。
  • - 韌體密碼(如果韌體image已加密) (可選)
每次韌體image下載成功之後,設備都會通過OVSDB AWLAN_Node Table的upgrade_status欄位報告成功。此時,雲端管理系統將更新升級計時器。計時器更新之後,設備開始升級韌體過程。
如果升級成功,則設備將通過upgrade_status 欄位再次報告成功。雲端管理系統隨即進行設備重啟。

3-h. LTE Manager (LTEM)

LTE管理器(LTEM)負責在檢測到Internet中斷後,將網路流量從WAN切換到LTE備份。LTEM會監視WAN Link是否發生L2或L3中斷,並在發生WAN中斷時向LTE接口添加預設路由。當WAN接口恢復聯機時,LTEM刪除LTE路由,網路流量再次回到WAN接口。
LTEM還負責在網路切換的時候,為LTE接口添加和刪除DNS Entry。

4. OpenFlow management and configuration

4-a. FlowService Manager (FSM)


4-b. OpenFlow Manager (OM)


4-c. FlowCollection Manager (FCM) 

2021年5月1日 星期六

Open vSwitch簡介

由於想做Opensync,所以決定由基本的核心開始研讀,這核心就是Open vSwitch (簡寫為OVS)。

如果說硬體的Ethernet Switch大家都懂,OVS就是軟體的交換器(switch),所以所有硬體交換器的功能,OVS都有支援。

那有甚麼好處? 那就是說Switch功能可以不斷更新。因為硬體買了就買了,如果有新的功能出來,就只能丟掉,買新的硬體。但是軟體Switch只要更新軟體,便有新功能了。

此外目前在雲端的環境之中,VM的使用是常見的,雖然作業系統早就可以虛擬化了,但是實體網路介面還是有許多介面,因此OVS也可以輕易地提供許多有彈性地應用。下面這張由Wiki抄過來的圖,可以極大化的說明OVS的功能。


但雖然OVS是設計給分散式網路使用,但是更多的應用是利用OVS來在一個機器上建立一堆虛擬機器,然後便可以透過OVS這虛擬的交換器,用標準的網路協定來進行溝通。

下面列一下OVS的功能:

  • 使用通用路由封裝(GRE: Generic Routing Encapsulation)協定,在虛擬機之中的端口之間,建立通道,支援NetFlow、sFlow、IP流信息導出(IPFIX: IP Flow Information Export)、交換端口分析器(SPAN: Switched Port Analyzer )、遠程交換端口分析器(RSPAN: Remote Switched Port Analyzer )等功能。
  • 支援LACP( Link Aggregation Control Protocol, IEEE 802.1AX -2008):就是Switch可以把兩個Ethernet Port與電腦的兩片網卡,分別連接起來,當成一條Ethernet來使用。
  • 支援802.1q VLAN網路,同時支援穿透功能(Trucking)。
  • 支持IGMP 1、2和3,進行multicast snooping。
  • 支援  Shortest Path Bridging Media Access Control (SPBM)及Link Layer Discovery Protocol (LLDP)的相關基本支援。
  • 支持Bidirectional Forwarding Detection (BFD)和802.1ag Link監視。
  • 支持Spanning Tree Protocol(STP, IEEE 802.1D -1998) 和 Rapid Spanning Tree Protocol (RSTP,IEEE 802.1D-2004)。
  • 針對不同應用程序、使用者或數據流的QoS Control。
  • 支援分層公平服務曲線(HFSC: hierarchical fair-service curve)的排隊規則(qdisc )
  • 虛擬機接口上提供流量監管(Traffic policing)。
  • 網卡(NIC)的綁定,通過MAC地址、活動備份和Layer 4 Hash進行負載平衡。Layer 4 Hash也許有些同事沒有聽說過,基本上負載平衡是用兩條網路一起送封包,來加快速度。但是如何決定哪個封包走哪條路? 就有下面的方法:


上圖當中,利用網路封包之中的 Ethernet Header + IP + L4 (Port #)來做出Hash Key,Switch便用Hash結果來決定封包要走哪一條網路介面。


  • 支援OpenFlow通信協議,包括各種與虛擬化相關的擴充。
  • 支援IPv6。
  • 支持多種隧道協議,包括GRE、虛擬可擴展LAN (VXLAN)、無狀態傳輸隧道(STT)和Geneve,並額外支持IPsec上的分層
  • 遠程配置協議,具有針對C和Python編程語言的現有綁定
  • 數據包轉發引擎在內核空間或用戶空間中的實現,通過在不離開內核空間的情況下處理大多數轉發數據包以及使用多線程內核空間和用戶空間組件,提供了更大的靈活性並提供了性能改進。
  • 具有流緩存引擎的多表轉發管道
  • 轉發層抽象化界面(Forwarding layer abstraction),使其更容易將Open vSwitch移植到新的軟件和硬件平台。

下面是 OVS 的系統架構圖,接下來會逐一說明圖中各模組的功能。在此架構圖中,我們看到三個主要的橘色的方塊,這是主要的三個元件: Datapath、vswitchd、ovsdb。分別說明如下:

  • Vswitchd: 這是一隻Linux daemon (常駐程式),他是OVS的主要模組,用以實現交換器的功能。由這架構可以了解,想要好好執行OVS,必須要有比較強力的CPU。
  • Datapath:這是一個以Driver方式存在的Linux Kernel模組,主要用以接收來自網卡的封包。這是因為Switch原先的在網路協定堆疊之中,層次比較低。所以如果要拿出來處理,需要放一個Kernel Module到網卡的上層,才方便將封包送到User Space中的ovs-vswitchd 來處理。上圖中有一個有趣詞 promiscuous mode,大家也許沒有聽過,他中文可以直譯為”混雜模式”。這是指一台電腦的網卡接收所有經過它的封包,而不論其目的地址是否是本機。一般而言,網卡都工作在非混雜模式下,此時網卡只接受來自網路卡的目的地址指向自己的封包。當網卡工作在混雜模式下時,網卡將來自Etehrnet網路中的所有封包,全部接收下來,並交給上面的Datapath Module。有一個著名的封包分析程式 sniffer,就是典型使用”混雜模式”來工作的程式。
  • Ovsdb: 這是一個Local Database,用以搭配 vswitchd,提供資料的暫存與設定。Ovsdb 也提供許多擴充的功能,可以存放系統的資料。

下面依照圖中數字,說明一下資料流程:

(1) 網路卡在混雜模式下,讀取所有來自Etehrnet的封包,送給 Datapath Module。

(2) Datapath Module檢查封包,將需要處理的封包送交 vswicthd deamon處理。

(3) OpenFlow 是數據鏈路層協定(Data Link Layer Protocol),主要控制Switch或Router的轉發原則(forwarding plane),藉此設定網路封包所走的網路路徑。OVS做為一個軟體Switch,vswitchd 同樣支援OpenFlow Protocol,用來設定路由。

(4) 發現需要處理的封包,vswitchd 會更新Flow table。

(5) 如果封包不需要處理,vswitchd 通知 Datapath。

(6) 考慮到如果每一次封包的傳導都要詢問一次 OpenFlow 將導致網路的延遲與沒有效率,已經得知的規則將記錄在 flow table 中,供之後的封包使用。Flow Table每次更新都會通知 Datapath Module。

(7) Datapath 可以根據更新的 flow table來傳導封包。

其他還有一些ovs的管理介面,有以下幾隻程式:

ovs-dpctl: 用來配置 Datapath (OVS Kernel 模組)。

ovs-ofctl: 查詢和控制 OpenFlow 的規則 (policy)。

ovsdb-tool: ovsdb 的設定工具。

ovs-vsctl: 查詢和更新 vswitchd 的配置。

上圖之中,還有一個方塊是sFlow,這是一個網路常常使用的流量分析工具,通常在L2 Switch上面會有SFlow Agent,就可以讓sFlow collectors 讀取資料,然後來做一些分析。OVS上面也有支援 sFlow agent的功能。詳細資料可以參考一下的連結:

https://docs.openvswitch.org/en/latest/howto/sflow/



2021年4月24日 星期六

使用 libexpat 來解析XML文件

expat 是使用C語言所寫的XML解析器,採用Stream-oriented的方式解析XML 文件,在我使用的Ubunto環境下,可以用一下方式安裝套件:

# sudo apt-get install expat

expat 大約有二十多個供使用的Library Call,在此只介紹要用的幾個基本函數。

程式編譯後,連結的程式館是 -lexpat ,程式一開始要加入以下的 include file:

#include "expat.h"

之後在調用之前,要做以下宣告與初始化動作:

XML_Parser XML_ParserCreate(const XML_Char *encoding)

  XML_ParserCreate()會建立一個 XML_Parser 的資料結構然後回傳給一個變數,之後所有的操作都要帶上這個變數。他的參數 encoding 是一個字串說明這個XML要處理的編碼方式。目前已經知道的有以下四種方式:
  - US-ASCII
  - UTF-8
  - UTF-16
  - ISO-8859-1
  
  但是因為XML通常會指定文件的編碼方式,所以就不用雞婆,直接用 NULL 即可。所以我們最常見的作法就是採用如下宣告:

  XML_Parser xml = XML_ParserCreate(NULL);

  由於XML處理程式使用 Stream 架構,所以要先掛上一個Callback,也就是說我們要處理的XML是哪一段。

    XML_SetElementHandler(XML_Parser p,
                      XML_StartElementHandler start,
                      XML_EndElementHandler end);

    typedef void    (*XML_StartElementHandler)(void *userData,
                            const XML_Char *name,
                            const XML_Char **atts);

    typedef void    (*XML_EndElementHandler)(void *userData,
                             const XML_Char *name);

   XML_SetElementHandler() 就是用來掛上兩個 Callback,一個程式叫做 start() 是開始找出XML要處理的字串,一個 end() 用來找出結束的字串。 srat() 與 end() 都是Function pointer,指到兩個獨立的處理程式(Function)。但是在呼叫 XML_SetElementHandler() 設定Callback之前,我們還要為這兩個 Callback程式中的 userData 進行設定。
   還有一個變數叫做 Depth,這是指XML的深度。就是第一層、第二層的意思。

   設定 userData 的程式是  
   
   XML_SetUserData(XML_Parser parser, void *p) 
   
   這個呼叫會自動將 *p 所指到的記憶體,自動變成 *userData,而成為 start() 與 end() 的 data stream input。
   start() callback 的功能就是找出 XML 自串的起始,然後將字串當中的字放到 **atts 之中回傳。  end() 的功能就是找出結尾字,也放到 *name 之中回傳。
     
   也許用例子來說明比較容易,看下面看這個例子:
   <server>
        <client ip="114.36.9.181" lat="25.0478" lon="121.5318" isp="Chunghwa Telecom" isprating="3.7" rating="0" ispdlavg="0" ispulavg="0" loggedin="0" country="TW"/>
   </server>
   
   我們想要讀出這串XML裏面的值。
   先將我們要處理的資料結構餵到 XML expat 當中。

   XML_SetUserData(xml, p_client);

   這個 p_client 是如此宣告的
   
    struct client_info *p_client ;

    struct client_info
    {
        char   ip[MAX_IPADDRESS_STRLEN];
        double lat;
        double lon;
        char   isp[MAX_ISP_NAME];
    };   

    對應到上面的XML,可以知道我們要出 ip 的值,填到上面的資料結構中,程式就可以繼續處理了。這時候要先寫 start 與 end 這兩個Callback。

Start_Callback()因為是Call-back,所以要使用預定的帶入的三個參數:
- void          *userData   : 就是上面的的 XML_SetUserData 設定的資料結構(就是 p_client)。
- const char    *tagname    : tagname 是一個要處理XML的標頭字串,以上面的例子來說,就是"client"。
- const char    **atts      : 這個一個Array。Call-back程式把字串中的內容Parsing出來之後,將結果放到Array當中。

注意這邊 tagname 與 atts 都是 Const char,說明這是一個Global Variable,必須在外圍先做宣告。

static void XMLCALL 
Start_Callback(void *userData, const char *tagname, const char **atts)
{
    int i;

    if (strcmp(tagname, "client") == 0) {

        struct client_info *p_client = (struct client_info *)userData;

        for (i = 0; atts[i]; i += 2) {
            if (strcmp(atts[i], "ip") == 0)
                strcpy(p_client->ip, atts[i + 1]);
            if (strcmp(atts[i], "isp") == 0)
                strcpy(p_client->isp, atts[i + 1]);
            if (strcmp(atts[i], "lat") == 0)
                p_client->lat = atof(atts[i + 1]);
            if (strcmp(atts[i], "lon") == 0)
                p_client->lon = atof(atts[i + 1]);
        }
    }
}

End_Callback() 與 Start_Callback() 一樣,用來處理XML的結尾,通常這個結尾都很簡單,所以幾乎沒有甚麼好寫的。
這Call Back 有兩個參數: 
userData    : 就是之前設定好的XML Stream ;
name        : 只需要做的XML Tag,例如前面的例子,就是傳入 "client" 這個字串。

static void XMLCALL 
end_callback(void *userData, const char *tagname)
{
    depth--;  / *將處理這張XML的位置還原 */

    if (strcmp(tagname, "clinet") == 0) {

        /* 下面就是完成Pasing XML之後,做一堆處理這些XML的工作 */

        int     i;
        struct server_info *p_server = (struct server_info *)userData;

        p_server->distance = get_distance(client.lat, client.lon, p_server->lat, p_server->lon);

        for (i = 0; i < MAX_CLOSEST_SERVER_NUM; i++) {

            if ( servers[i].url[0] == 0 ) {

                break;
            }
        }

        if ( i == MAX_CLOSEST_SERVER_NUM ) {

            for (i = 0; i <MAX_CLOSEST_SERVER_NUM; i++) {

                if (servers[i].distance >  p_server->distance) {
                    break;
                }
            }
        }
        if (i != MAX_CLOSEST_SERVER_NUM)
            memcpy(&servers[i], p_server, sizeof(struct server_info));
        memset(p_server, 0, sizeof(struct server_info));
    }

}

Libcurl 的使用範例

/* 
 * 這個例子說明如何寫Libcurl中的Callback 程式,這Callback將把 URL回傳的資料,放到記憶體當中。
 * Shows how the write callback function can be used to download data into a
 * chunk of memory instead of storing it in a file.
 * 
 */ 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
#include <curl/curl.h>
 
struct MemoryStruct {
  char *memory;
  size_t size;
};
/*
  這個例子的就是存取一個URL,傳回內容與CONTENT SIZE。
    
  size_t callback_function( void *ptr, size_t size, size_t nmemb, void *stream);
  這函數將在libcurl接收到數據後被調用,用來儲存http送回的資料。
  程式在處理回傳資料最難的就是不知道回傳的資料量有多少? 所以記憶體也不知道要需告多大。所以就設計了這個Callback。
  *ptr 是一快記憶體空間,裡面放著 http 傳回的資訊。
  size :說明 Contents 所指到的記憶體有多大?
  memb :這是一個有趣的參數,因為網路上每個字的單位有多大? 所以利用這個變數來做通知 Callback。實際的記憶體SIZE = size * memb
    userp:這個參數主要回傳一個應用程式可以使用的資料結構,
            struct MemoryStruct {
            char *memory;
            size_t size;
            };
    *memory 之中放著由 ptr 送進來的 HTTP 回傳的資料。
    size 是 memory 的 SIZER。
    CURLOPT_WRITEDATA用於表明CURLOPT_WRITEFUNCTION函數中的stream指針的來源。
    如果你沒有通過CURLOPT_WRITEFUNCTION屬性给easy handle設置回調函數,libcurl會提供一個預設的回調函數,它只是簡單的將接收到的數據印到標准輸出。
    你也可以通過 CURLOPT_WRITEDATA屬性之中的Callback,將資訊寫到記憶體之中,傳給Libcurl,然後透過 HTTP傳送出去。
*/
static size_t
WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp)
{
    size_t realsize = size * nmemb;
    struct MemoryStruct *mem = (struct MemoryStruct *)userp;
    
    char *ptr = realloc(mem->memory, mem->size + realsize + 1);
    if(ptr == NULL) {
        /* 
        記憶體不足所以拿不到記憶體,out of memory! 
        */
        printf("not enough memory (realloc returned NULL)\n");
        return 0;
    }
    
    mem->memory = ptr;
    memcpy(&(mem->memory[mem->size]), contents, realsize);
    mem->size += realsize;
    mem->memory[mem->size] = 0;
    
    return realsize;
}
 
int main(void)
{
    CURL *curl_handle;
    CURLcode res;
    
    struct MemoryStruct chunk;
    
    chunk.memory = malloc(1);  /* will be grown as needed by the realloc above */ 
    chunk.size = 0;    /* no data at this point */ 
    
    curl_global_init(CURL_GLOBAL_ALL);
 
    /* 
        將curl Library進行初始化,init the curl session 
    */
    curl_handle = curl_easy_init();
 
    /* 
        設定要存取的URL 
    */
    curl_easy_setopt(curl_handle, CURLOPT_URL, "https://www.example.com/");
 
    /* send all data to this function  */ 
    
    curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
 
    /* 
       we pass our 'chunk' struct to the callback function
       chunk 就會傳給 WriteMemoryCallback 之中的第四個參數 userp 
    */
    
    curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void *)&chunk);
 
    /* 
       這一行如果不寫,有時有問題,有的SERVER會有問題,所以務必要寫
    */ 
    curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "libcurl-agent/1.0");
 
    /* get it down ! */ 
    res = curl_easy_perform(curl_handle);
 
    /* check for errors */ 
    if(res != CURLE_OK) {
        fprintf(stderr, "curl_easy_perform() failed: %s\n",
        curl_easy_strerror(res));
    }
    else {
        /*
         * Now, our chunk.memory points to a memory block that is chunk.size
         * bytes big and contains the remote file.
         *
         * Do something nice with it!
        */ 
        printf("%lu bytes retrieved\n", (unsigned long)chunk.size);
    }
    /* cleanup curl stuff */ 
    curl_easy_cleanup(curl_handle);
    free(chunk.memory);
    /* we're done with libcurl, so clean it up */ 
    curl_global_cleanup();
    return 0;
}