Traceroute(路由追蹤)的原理及實(shí)現(xiàn)

現(xiàn)實(shí)世界中的網(wǎng)絡(luò)是由無(wú)數(shù)的計(jì)算機(jī)和路由器組成的一張的大網(wǎng)榆纽,應(yīng)用的數(shù)據(jù)包在發(fā)送到服務(wù)器之前都要經(jīng)過(guò)層層的路由轉(zhuǎn)發(fā)铅祸。而Traceroute是一種常規(guī)的網(wǎng)絡(luò)分析工具眼耀,用來(lái)定位到目標(biāo)主機(jī)之間的所有路由器

原理

在介紹Traceroute的原理之前固蛾,需要了解幾個(gè)技術(shù)名詞:

  • IP協(xié)議

    IP協(xié)議是TCP/IP協(xié)議族中最核心的部分碉哑,它的作用是在兩臺(tái)主機(jī)之間傳輸數(shù)據(jù),所有上層協(xié)議的數(shù)據(jù)(HTTP鸵熟、TCP副编、UDP等)都會(huì)被封裝在一個(gè)個(gè)的IP數(shù)據(jù)包中被發(fā)送到網(wǎng)絡(luò)上。

  • ICMP
    ICMP全稱為互聯(lián)網(wǎng)控制報(bào)文協(xié)議流强,它常用于傳遞錯(cuò)誤信息痹届,ICMP協(xié)議是IP層的一部分,它的報(bào)文也是通過(guò)IP數(shù)據(jù)包來(lái)傳輸?shù)摹?/p>

  • TTL
    TTL(time-to-live)是IP數(shù)據(jù)包中的一個(gè)字段打月,它指定了數(shù)據(jù)包最多能經(jīng)過(guò)幾次路由器队腐。從我們?cè)粗鳈C(jī)發(fā)出去的數(shù)據(jù)包在到達(dá)目的主機(jī)的路上要經(jīng)過(guò)許多個(gè)路由器的轉(zhuǎn)發(fā),在發(fā)送數(shù)據(jù)包的時(shí)候源主機(jī)會(huì)設(shè)置一個(gè)TTL的值奏篙,每經(jīng)過(guò)一個(gè)路由器TTL就會(huì)被減去一柴淘,當(dāng)TTL為0的時(shí)候該數(shù)據(jù)包會(huì)被直接丟棄(不再繼續(xù)轉(zhuǎn)發(fā)),并發(fā)送一個(gè)超時(shí)ICMP報(bào)文給源主機(jī)。

具體到traceroute的實(shí)現(xiàn)細(xì)節(jié)上为严,有兩種不同的方案:

基于UDP實(shí)現(xiàn)

在基于UDP的實(shí)現(xiàn)中敛熬,客戶端發(fā)送的數(shù)據(jù)包是通過(guò)UDP協(xié)議來(lái)傳輸?shù)模褂昧艘粋€(gè)大于30000的端口號(hào)第股,服務(wù)器在收到這個(gè)數(shù)據(jù)包的時(shí)候會(huì)返回一個(gè)端口不可達(dá)的ICMP錯(cuò)誤信息荸型,客戶端通過(guò)判斷收到的錯(cuò)誤信息是TTL超時(shí)還是端口不可達(dá)來(lái)判斷數(shù)據(jù)包是否到達(dá)目標(biāo)主機(jī),具體的流程如圖:

基于UDP實(shí)現(xiàn)的traceroute
  1. 客戶端發(fā)送一個(gè)TTL為1炸茧,端口號(hào)大于30000的UDP數(shù)據(jù)包瑞妇,到達(dá)第一站路由器之后TTL被減去1,返回了一個(gè)超時(shí)的ICMP數(shù)據(jù)包梭冠,客戶端得到第一跳路由器的地址辕狰。
  2. 客戶端發(fā)送一個(gè)TTL為2的數(shù)據(jù)包,在第二跳的路由器節(jié)點(diǎn)處超時(shí)控漠,得到第二跳路由器的地址蔓倍。
  3. 客戶端發(fā)送一個(gè)TTL為3的數(shù)據(jù)包,數(shù)據(jù)包成功到達(dá)目標(biāo)主機(jī)盐捷,返回一個(gè)端口不可達(dá)錯(cuò)誤偶翅,traceroute結(jié)束。

Linux和macOS系統(tǒng)自帶了一個(gè)traceroute指令碉渡,可以結(jié)合Wireshark抓包來(lái)看看它的實(shí)現(xiàn)原理聚谁。首先對(duì)百度的域名進(jìn)行traceroute:traceroute www.baidu.com,每一跳默認(rèn)發(fā)送三個(gè)數(shù)據(jù)包滞诺,我們會(huì)看到下面這樣的輸出:

traceroute.png

對(duì)該域名的IP:115.239.210.27進(jìn)行traceroute形导,此時(shí)Wireshark抓包的結(jié)果如下:

抓包結(jié)果

注意看紅框處的內(nèi)容,跟第一張圖對(duì)比习霹,可以看到traceroute程序首先通過(guò)UDP協(xié)議向目標(biāo)地址115.239.210.27發(fā)送了一個(gè)TTL為1的數(shù)據(jù)包朵耕,然后在第一個(gè)路由器中TTL超時(shí),返回一個(gè)錯(cuò)誤類型為Time-to-live exceeded的ICMP數(shù)據(jù)包淋叶,此時(shí)我們通過(guò)該數(shù)據(jù)包的源地址可知第一站路由器的地址為10.242.0.1阎曹。之后只需要不停增加TTL的值就能得到每一跳的地址了。

然而一直跑下去會(huì)發(fā)現(xiàn)煞檩,traceroute并不能到達(dá)目的地处嫌,當(dāng)TTL增加到一定大小之后就一直拿不到返回的數(shù)據(jù)包了:

結(jié)果全是丟失

其實(shí)這個(gè)時(shí)候數(shù)據(jù)包已經(jīng)到達(dá)目標(biāo)服務(wù)器了,但是因?yàn)榘踩珕?wèn)題大部分的應(yīng)用服務(wù)器都不提供UDP服務(wù)(或者被防火墻擋掉)形娇,所以我們拿不到服務(wù)器的任何返回锰霜,程序就理所當(dāng)然的認(rèn)為還沒(méi)有結(jié)束,一直嘗試增加數(shù)據(jù)包的TTL桐早。

目前在網(wǎng)上找到許多開(kāi)源iOS traceroute實(shí)現(xiàn)大多都是基于UDP的方案,實(shí)際用起來(lái)并不能達(dá)到想要的效果,所以我們需要采用另一種方案來(lái)實(shí)現(xiàn)哄酝。

基于ICMP實(shí)現(xiàn)

上述方案失敗的原因是由于服務(wù)器對(duì)于UDP數(shù)據(jù)包的處理友存,所以在這一種實(shí)現(xiàn)中我們不使用UDP協(xié)議,而是直接發(fā)送一個(gè)ICMP回顯請(qǐng)求(echo request)數(shù)據(jù)包陶衅,服務(wù)器在收到回顯請(qǐng)求的時(shí)候會(huì)向客戶端發(fā)送一個(gè)ICMP回顯應(yīng)答(echo reply)數(shù)據(jù)包屡立,在這之后的流程還是跟第一種方案一樣。這樣就避免了我們的traceroute數(shù)據(jù)包被服務(wù)器的防火墻策略墻掉搀军。

采用這種方案的實(shí)現(xiàn)流程如下:

基于ICMP實(shí)現(xiàn)的traceroute
  1. 客戶端發(fā)送一個(gè)TTL為1的ICMP請(qǐng)求回顯數(shù)據(jù)包膨俐,在第一跳的時(shí)候超時(shí)并返回一個(gè)ICMP超時(shí)數(shù)據(jù)包,得到第一跳的地址罩句。
  2. 客戶端發(fā)送一個(gè)TTL為2的ICMP請(qǐng)求回顯數(shù)據(jù)包焚刺,得到第二跳的地址。
  3. 客戶端發(fā)送一個(gè)TTL為3的ICMP請(qǐng)求回顯數(shù)據(jù)包门烂,到達(dá)目標(biāo)主機(jī)乳愉,目標(biāo)主機(jī)返回一個(gè)ICMP回顯應(yīng)答,traceroute結(jié)束屯远。

可以看出與第一種實(shí)現(xiàn)相比蔓姚,區(qū)別主要在發(fā)送的數(shù)據(jù)包類型以及對(duì)于結(jié)束的判斷上,大體的流程還是一致的慨丐。

值得一提的是在Windows系統(tǒng)中也有traceroute程序坡脐,它的名字叫做tracerttracert就是用采用這種方法來(lái)實(shí)現(xiàn)的房揭,感興趣的話可以自行嘗試一下挨措,這里就不再演示了。

實(shí)現(xiàn)

這里我們主要討論基于ICMP的實(shí)現(xiàn)崩溪,相關(guān)的Demo已經(jīng)上傳至github:https://github.com/L-Zephyr/TracerouteDemo.git

采用這種方案時(shí)浅役,ICMP數(shù)據(jù)包的創(chuàng)建、解析伶唯、校驗(yàn)都需要我們自己進(jìn)行觉既,ICMP是封裝在IP數(shù)據(jù)包的數(shù)據(jù)段中傳輸?shù)模躁P(guān)鍵在于如何創(chuàng)建和發(fā)送ICMP數(shù)據(jù)乳幸,以及接收到返回的數(shù)據(jù)時(shí)如何從IP數(shù)據(jù)包中將ICMP解析出來(lái):

創(chuàng)建ICMP數(shù)據(jù)

ICMP數(shù)據(jù)包頭部的格式如下:

ICMP數(shù)據(jù)結(jié)構(gòu)

其中的類型字段用來(lái)表示消息的類型瞪讼,在Wiki上可以看到所有類型代表的含義。報(bào)文中的標(biāo)識(shí)符和序列號(hào)由發(fā)送端指定粹断,如果這個(gè)ICMP報(bào)文是一個(gè)請(qǐng)求回顯的報(bào)文(類型為8符欠,代碼為0),這兩個(gè)字段會(huì)被原封不動(dòng)的返回瓶埋。

根據(jù)上圖中各個(gè)字段的大小可以定義如下類型:

typedef struct ICMPPacket {
    uint8_t     type; // 類型
    uint8_t     code; // 類型代碼
    uint16_t    checksum; // 校驗(yàn)碼
    uint16_t    identifier; // ID
    uint16_t    sequenceNumber; // 序列號(hào)
    // data...
} ICMPPacket;

其中的type字段指定了這個(gè)ICMP數(shù)據(jù)包的類型希柿,是需要重點(diǎn)關(guān)注的對(duì)象诊沪,為此定義一個(gè)報(bào)文類型的枚舉:

// ICMPv4報(bào)文類型
typedef enum ICMPv4Type {
    kICMPv4TypeEchoReply = 0, // 回顯應(yīng)答
    kICMPv4TypeEchoRequest = 8, // 回顯請(qǐng)求
    kICMPv4TypeTimeOut = 11, // 超時(shí)
}ICMPv4Type;

比較麻煩的是校驗(yàn)的計(jì)算,這一部分直接使用了蘋果官方示例SimplePing中的代碼曾撤,所涉及到的幾個(gè)工具方法封裝在類型TracerouteCommon中端姚。

在發(fā)送數(shù)據(jù)的時(shí)系統(tǒng)會(huì)自動(dòng)加上IP頭部不需要自己處理,如此一來(lái)我們只需要?jiǎng)?chuàng)建一個(gè)ICMPPacket數(shù)據(jù)包并通過(guò)socket發(fā)送到目標(biāo)服務(wù)器就可以了挤悉。

解析ICMP數(shù)據(jù)

接下來(lái)就是要接收服務(wù)器向我們返回的ICMP數(shù)據(jù)了渐裸,我們接收到的是帶有IP頭部的原始數(shù)據(jù),所以必須先進(jìn)行一些處理將ICMP從IP數(shù)據(jù)包中提取出來(lái)装悲,IP數(shù)據(jù)包由兩部分組成:數(shù)據(jù)包頭部信息部分以及實(shí)際的數(shù)據(jù)部分昏鹃。下圖是IPv4數(shù)據(jù)包的結(jié)構(gòu):

IPv4數(shù)據(jù)包格式

一眼看上去是不是感覺(jué)很混亂,其實(shí)這里面只有用紅框圈出來(lái)的這這三個(gè)字段需要我們關(guān)心:版本表示該數(shù)據(jù)包是IPv4還是IPv6诀诊;之前說(shuō)過(guò)ICMP協(xié)議是通過(guò)IP協(xié)議來(lái)傳輸?shù)亩床常绻摂?shù)據(jù)包傳輸?shù)氖荌CMP協(xié)議則協(xié)議字段會(huì)被設(shè)置為1;由于IPv4數(shù)據(jù)包帶有可選的選項(xiàng)字段畏梆,所以其頭部的長(zhǎng)度是可變的您宪,此時(shí)需要根據(jù)首部長(zhǎng)度字段來(lái)獲取具體的數(shù)據(jù)。

根據(jù)上面的結(jié)構(gòu)可以定義類型:

typedef struct IPv4Header {
    uint8_t versionAndHeaderLength; // 版本和首部長(zhǎng)度
    uint8_t serviceType;
    uint16_t totalLength; 
    uint16_t identifier;
    uint16_t flagsAndFragmentOffset;
    uint8_t timeToLive;
    uint8_t protocol; // 協(xié)議類型奠涌,1表示ICMP
    uint16_t checksum;
    uint8_t sourceAddress[4];
    uint8_t destAddress[4];
    // options...
    // data...
} IPv4Header;

提取ICMP數(shù)據(jù)包的方法如下:

+ (ICMPPacket *)unpackICMPv4Packet:(char *)packet len:(int)len {
    if (len < (sizeof(IPv4Header) + sizeof(ICMPPacket))) {
        return NULL;
    }
    
    const struct IPv4Header *ipPtr = (const IPv4Header *)packet;
    if ((ipPtr->versionAndHeaderLength & 0xF0) != 0x40 || // IPv4
        ipPtr->protocol != 1) { // ICMP
        return NULL;
    }
    
    // 獲取IP頭部長(zhǎng)度
    size_t ipHeaderLength = (ipPtr->versionAndHeaderLength & 0x0F) * sizeof(uint32_t); 
    if (len < ipHeaderLength + sizeof(ICMPPacket)) {
        return NULL;
    }
    
    // 返回?cái)?shù)據(jù)部分的ICMP
    return (ICMPPacket *)((char *)packet + ipHeaderLength);
}

其中出現(xiàn)的如ipPtr->versionAndHeaderLength & 0xF0的判斷是因?yàn)榘姹咎?hào)和首部長(zhǎng)度各自只占4個(gè)bit宪巨,在結(jié)構(gòu)中直接定義了一個(gè)1字節(jié)的uint8_t類型來(lái)表示,所以只能通過(guò)位運(yùn)算符&來(lái)獲取各自的值溜畅。

整體流程

有了上面的兩步捏卓,剩下的事情就很簡(jiǎn)單了,下面是整體流程的偽代碼:

// 1. 創(chuàng)建一個(gè)套接字
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);

// 2. 最多嘗試30跳
int ttl = 1;
for (0...30) {
    // 3. 設(shè)置TTL慈格,發(fā)送3個(gè)ICMP數(shù)據(jù)包怠晴,每一跳都將遞增TTL
    setsockopt(sock, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl));
    ++ttl;
    for (0...3) {
        // 4. 發(fā)送并等待返回的數(shù)據(jù)包
        sendto(...);
        recvfrom(...);
        
        // 5. 解析數(shù)據(jù)包,記錄數(shù)據(jù)浴捆,成功條件判斷
        ICMPPacket *packet = unpack(...);
    }
}

socket的類型采用了SOCK_DGRAM蒜田,有些小伙伴可能會(huì)感到疑惑:用SOCK_DGRAM創(chuàng)建套接字不還是發(fā)送UDP數(shù)據(jù)么?

確實(shí)在許多系統(tǒng)的實(shí)現(xiàn)中要直接發(fā)送ICMP的話需要使用原始套接字(類型為SOCK_RAW)选泻,這在iOS系統(tǒng)中是不被允許使用的冲粤,但是查閱資料中了解到macOS支持一種使用參數(shù)SOCK_DGRAMIPPROTO_ICMP來(lái)直接創(chuàng)建ICMP套接字方式,嘗試之下果然iOS也支持這種用法页眯。不過(guò)在使用中發(fā)現(xiàn)了一個(gè)問(wèn)題:使用IPv4套接字的時(shí)候接收到的數(shù)據(jù)包是帶有原始IP頭部的梯捕,而使用IPv6套接字的時(shí)候收到的數(shù)據(jù)包卻沒(méi)有IP頭部,這個(gè)問(wèn)題讓我比較疑惑窝撵,各位大佬如果有對(duì)這一塊了解的話還望賜教傀顾。

總結(jié)

Demo中的示例程序已經(jīng)在模擬器和真機(jī)環(huán)境經(jīng)過(guò)測(cè)試,可以看到碌奉,現(xiàn)在Traceroute已經(jīng)能夠正常的工作了:

Traceroute Demo

有些路由器會(huì)隱藏的自己的位置短曾,不讓ICMP Timeout的消息通過(guò)寒砖,結(jié)果就是在那一跳上始終會(huì)顯示星號(hào),此外服務(wù)器也可以偽造traceroute路徑的错英,不過(guò)一般應(yīng)用服務(wù)器也沒(méi)有理由這么做入撒,所以Traceroute的結(jié)果還是能夠?yàn)榫W(wǎng)絡(luò)分析提供一些參考的隆豹。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末椭岩,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子璃赡,更是在濱河造成了極大的恐慌判哥,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件碉考,死亡現(xiàn)場(chǎng)離奇詭異塌计,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)侯谁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門锌仅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人墙贱,你說(shuō)我怎么就攤上這事热芹。” “怎么了惨撇?”我有些...
    開(kāi)封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵伊脓,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我魁衙,道長(zhǎng)报腔,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任剖淀,我火速辦了婚禮纯蛾,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘纵隔。我一直安慰自己翻诉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布巨朦。 她就那樣靜靜地躺著米丘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪糊啡。 梳的紋絲不亂的頭發(fā)上拄查,一...
    開(kāi)封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音棚蓄,去河邊找鬼堕扶。 笑死碍脏,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的稍算。 我是一名探鬼主播典尾,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼糊探!你這毒婦竟也來(lái)了钾埂?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤科平,失蹤者是張志新(化名)和其女友劉穎褥紫,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瞪慧,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡髓考,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了弃酌。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片氨菇。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖妓湘,靈堂內(nèi)的尸體忽然破棺而出查蓉,到底是詐尸還是另有隱情,我是刑警寧澤多柑,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布奶是,位于F島的核電站,受9級(jí)特大地震影響竣灌,放射性物質(zhì)發(fā)生泄漏聂沙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一初嘹、第九天 我趴在偏房一處隱蔽的房頂上張望及汉。 院中可真熱鬧,春花似錦屯烦、人聲如沸坷随。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)温眉。三九已至,卻和暖如春翁狐,著一層夾襖步出監(jiān)牢的瞬間类溢,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工露懒, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留闯冷,地道東北人砂心。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像蛇耀,于是被迫代替她去往敵國(guó)和親辩诞。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • 1.這篇文章不是本人原創(chuàng)的纺涤,只是個(gè)人為了對(duì)這部分知識(shí)做一個(gè)整理和系統(tǒng)的輸出而編輯成的译暂,在此鄭重地向本文所引用文章的...
    SOMCENT閱讀 13,063評(píng)論 6 174
  • 個(gè)人認(rèn)為,Goodboy1881先生的TCP /IP 協(xié)議詳解學(xué)習(xí)博客系列博客是一部非常精彩的學(xué)習(xí)筆記洒琢,這雖然只是...
    貳零壹柒_fc10閱讀 5,054評(píng)論 0 8
  • 8.1 引言 由Van Jacobson編寫的Traceroute程序是一個(gè)能更深入探索TCP/IP協(xié)議的方便可用...
    張芳濤閱讀 1,661評(píng)論 0 3
  • Traceroute是一個(gè)非常便利的網(wǎng)絡(luò)診斷工具秧秉。它可以輸出以下三個(gè)內(nèi)容: 1 網(wǎng)絡(luò)數(shù)據(jù)包的從源地址到目的地址的整...
    蝎子看互聯(lián)網(wǎng)閱讀 934評(píng)論 0 51
  • 11.1 引言 UDP是一個(gè)簡(jiǎn)單的面向數(shù)據(jù)報(bào)的運(yùn)輸層協(xié)議:進(jìn)程的每個(gè)輸出操作都正好產(chǎn)生一個(gè)UDP數(shù)據(jù)報(bào)褐桌,并組裝成一...
    張芳濤閱讀 2,808評(píng)論 1 6