從 Wireshark 到 patch OpenVPN driver

Chumy | Apr 8, 2023 min read

緣由

當初會這樣是因為接到一個很有趣的 case

image image image

簡單來說就是想要 client 可以打 VPN 到另一邊,並且讓 PPPoE 可以透過 VPN 拿到中華電信的 public IP address。

未命名绘图 drawio (63)

至於他們原本的架構,也是很有趣,首先會找人重架的原因是因為他們說接 VPN 後網路很不穩定,speedtest 也無法測速的那種。

於是我問了架構發現他們架構很酷。

首先他們是用那種小電腦做 server,如下圖。

image

而裡面長這樣。

image

就是他小電腦裝 windows 10,裡面只有裝 VMware,然後開兩台 windows 的 VM,一台是 windows 7 做 VPN server,裡面用 softether vpn,另一台 windows 10 我不知道用途,他們說測試用。

然後我就想這一台小電腦塞那麼多台 windows 不當才怪,再加上 softether vpn 沒架過並沒有很熟,也不確定這個效能好不好。

於是我就推薦他們 host 機可以用 Proxmox VE 做虛擬化環境,然後 VPN 的部分由於 PPPoE 是 L2 的協定,需要 ethernet header,因此 VPN 的選擇上就需要用 L2 Tunnel。關於甚麼是 L2 Tunnel 可以去看這篇文章這篇文章

這邊我選擇用 OpenVPN tap 作為 L2 Tunnel。

因此架構就改成 Proxmox VE 裡面裝 Debian 跟 Windows 10,Debian 用 OpenVPN tap。

過程

於是乎我開始架設 VPN 架設的 config 可以參考這裡

image

image

image

架好的我開開心心的用 windows 測試

image

image

image

image

到這邊我想說拿到 IP 了應該能動了,結果 ping 一下發現出不去。

image

這就很奇怪了,於是乎我 ping 看看 ipv6 看看是不是一樣的問題,結果可以通。

image

這下有趣了,V4 沒通 V6 有通,怎麼想都不正常,再加上我兩個都有拿到 IP。

image

而作為一個遇到問題會死命 debug 的我直接就開始抓包測試。

可以發現 ICMP ping 確實有封裝上 PPPoE header 並且從 VPN 網卡送出,且 VPN server 是有收到並轉發出去的,但是遲遲沒收到 response,因此可能是上游因為某些原因,可能不認可我們持有的 V4 IP 然後 drop 掉,但是認可 V6 的 IP。

image

image

因此就要來想一下為何上游不認可這個 V4 IP,在思考很久後得出一個可能是因為有些包在中途掉包而導致 PPPoE server 沒收到某個 ACK 導致這個問題的發生。

所以就要拿 client 跟 server 的 pppoe pcap 分析來比對

image

然後就可以發現有一個 ACK 送到 server 就不見了,這就有趣了,如此有規律的有特定包掉包,這基本上可以排除因為碰撞或資料有誤導致的自然掉包的可能。

這邊就有一個問題了,linux 作為 client 會不會有這種情況發生。

所以這邊就來測試。

image

image

image

image

可以發現 linux 是完全正常的 PPPoE 也是通的,沒有發生 windows 的那種情況,那這代表是 windows 的 OpenVPN 網卡 driver 有問題。

那接下來就要想問題點了。

image

為何他如此與眾不同,這邊就可以聯想到這個 packet 是所有 PPPoE packet 中最小的,長度只有 32 byte。

image

因此我猜測是因為長度問題,可能 driver 某個地方有一個如果發現 packet 長度過短就 drop 掉的 code。

所以我就用 scapy 寫了一個可以發送很小的 ethernet frame 的測試程式來做測試。

from scapy.all import *
import sys

IFACES.show() # let’s see what interfaces are available. Windows only
iface = IFACES.dev_from_index(35)
socket = conf.L2socket(iface=iface)
payload = Ether(dst = sys.argv[2], src = sys.argv[1])/Raw(load=sys.argv[3].encode())
print(bytes(payload))
print(len(bytes(payload)))
socket.send(payload)

先發一個長度 40 的包。

image

image

發現 server 有收到,改發長度 20 的包。

image

image

會發現 server 沒有收到,改發長度 30 的包。

image

image

會發現 server 還是沒有收到,改發長度 35 的包。

image

image

會發現 server 收到了,改發長度 33 的包。

image

image

會發現 server 又沒有收到,改發長度 34 的包。

image

image

會發現 server 又收到了。

因此我們可以確定最小可以接受的 packet length 是 34,由於 IPv4 的 configuration ACK 大小是 32,因此再送往 server 前就被 windows 的 driver 給 drop 掉了,也就是。

if len(packet) < 34:
    drop(packet)

因此我就去搜 tap-windows6 這個 repo 裡有沒有地方是 34 的。

image

image

但是沒發現任何東西,於是慢慢減少數字看看有沒有甚麼線索,直到找到 20 我看到一個有趣的東西。

image

image

這時我就聯想到 34 這個數字剛好是 ethernet header + ipv4 header 的長度,因此我馬上對整個專案搜尋 IP_HEADER_SIZE,會發現馬上就出現了一些很有趣的內容。

image

其中 txpath.c 上面的 < (ETHERNET_HEADER_SIZE + IP_HEADER_SIZE) 只是做分類而已,因此不影響重點是下面兩個。

image

首先我們可以很確定這個 function 是對 tap adapter 做處理,閱讀 function name tapNetBufferListNetBufferLengthsValid 可以猜測這是驗證 packet 長度的 function,那問題來了,理論上 tap adapter 只要有 ethernet header 都可以傳的,那為何需要驗證 IP_HEADER_SIZE

於是我把它改成這樣。

image

並且發了 PR

一個很有趣的小插曲是,在我還沒發現 code 有問題前我就有發 issue 了,說 Packet lost for pppoe over openvpn tap,但是他們並沒有採信。

image

image

image

最後我找到問題並發了 PR 他們才發覺真的有這問題。

image

image

並且經過了一寫討論與測試並發布後,我測試完 PPPoE 就正常了。

image

image

image