From 8d944008554c2bdbd9c2b63a306a26c43dff72b0 Mon Sep 17 00:00:00 2001 From: Haruue Date: Wed, 31 Jan 2024 10:05:51 +0800 Subject: [PATCH] Add WireGuard analyzer (#41) * feat: add WireGuard analyzer * chore(wg): reduce map creating for non wg packets * chore: import format * docs: add wg usage --------- Co-authored-by: Toby --- README.ja.md | 6 +- README.md | 6 +- README.zh.md | 6 +- analyzer/udp/wireguard.go | 217 ++++++++++++++++++++++++++++++++++++++ cmd/root.go | 1 + docs/Analyzers.md | 51 +++++++++ 6 files changed, 284 insertions(+), 3 deletions(-) create mode 100644 analyzer/udp/wireguard.go diff --git a/README.ja.md b/README.ja.md index fa3fb5f..390d262 100644 --- a/README.ja.md +++ b/README.ja.md @@ -16,7 +16,7 @@ OpenGFW は、Linux 上の [GFW](https://en.wikipedia.org/wiki/Great_Firewall) ## 特徴 - フル IP/TCP 再アセンブル、各種プロトコルアナライザー - - HTTP、TLS、DNS、SSH、SOCKS4/5、その他多数 + - HTTP、TLS、DNS、SSH、SOCKS4/5、WireGuard、その他多数 - Shadowsocks の"完全に暗号化されたトラフィック"の検出、 など。 (https://gfw.report/publications/usenixsecurity23/data/paper/paper.pdf) - トロイの木馬キラー (https://github.com/XTLS/Trojan-killer) に基づくトロイの木馬 (プロキシプロトコル) 検出 @@ -104,6 +104,10 @@ workers: action: block expr: string(socks?.req?.addr) endsWith "google.com" && socks?.req?.port == 80 +- name: block wireguard by handshake response + action: drop + expr: wireguard?.handshake_response?.receiver_index_matched == true + - name: block bilibili geosite action: block expr: geosite(string(tls?.req?.sni), "bilibili") diff --git a/README.md b/README.md index 6127b71..55ff4e1 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Linux that's in many ways more powerful than the real thing. It's cyber sovereig ## Features - Full IP/TCP reassembly, various protocol analyzers - - HTTP, TLS, DNS, SSH, SOCKS4/5, and many more to come + - HTTP, TLS, DNS, SSH, SOCKS4/5, WireGuard, and many more to come - "Fully encrypted traffic" detection for Shadowsocks, etc. (https://gfw.report/publications/usenixsecurity23/data/paper/paper.pdf) - Trojan (proxy protocol) detection based on Trojan-killer (https://github.com/XTLS/Trojan-killer) @@ -108,6 +108,10 @@ to [Expr Language Definition](https://expr-lang.org/docs/language-definition). action: block expr: string(socks?.req?.addr) endsWith "google.com" && socks?.req?.port == 80 +- name: block wireguard by handshake response + action: drop + expr: wireguard?.handshake_response?.receiver_index_matched == true + - name: block bilibili geosite action: block expr: geosite(string(tls?.req?.sni), "bilibili") diff --git a/README.zh.md b/README.zh.md index 0fb87a1..7a8f2ec 100644 --- a/README.zh.md +++ b/README.zh.md @@ -17,7 +17,7 @@ OpenGFW 是一个 Linux 上灵活、易用、开源的 [GFW](https://zh.wikipedi ## 功能 - 完整的 IP/TCP 重组,各种协议解析器 - - HTTP, TLS, DNS, SSH, SOCKS4/5, 更多协议正在开发中 + - HTTP, TLS, DNS, SSH, SOCKS4/5, WireGuard, 更多协议正在开发中 - Shadowsocks 等 "全加密流量" 检测 (https://gfw.report/publications/usenixsecurity23/data/paper/paper.pdf) - 基于 Trojan-killer 的 Trojan 检测 (https://github.com/XTLS/Trojan-killer) - [开发中] 基于机器学习的流量分类 @@ -103,6 +103,10 @@ workers: action: block expr: string(socks?.req?.addr) endsWith "google.com" && socks?.req?.port == 80 +- name: block wireguard by handshake response + action: drop + expr: wireguard?.handshake_response?.receiver_index_matched == true + - name: block bilibili geosite action: block expr: geosite(string(tls?.req?.sni), "bilibili") diff --git a/analyzer/udp/wireguard.go b/analyzer/udp/wireguard.go new file mode 100644 index 0000000..b53390b --- /dev/null +++ b/analyzer/udp/wireguard.go @@ -0,0 +1,217 @@ +package udp + +import ( + "container/ring" + "encoding/binary" + "slices" + "sync" + + "github.com/apernet/OpenGFW/analyzer" +) + +var ( + _ analyzer.UDPAnalyzer = (*WireGuardAnalyzer)(nil) + _ analyzer.UDPStream = (*wireGuardUDPStream)(nil) +) + +const ( + wireguardUDPInvalidCountThreshold = 4 + wireguardRememberedIndexCount = 6 + wireguardPropKeyMessageType = "message_type" +) + +const ( + wireguardTypeHandshakeInitiation = 1 + wireguardTypeHandshakeResponse = 2 + wireguardTypeData = 4 + wireguardTypeCookieReply = 3 +) + +const ( + wireguardSizeHandshakeInitiation = 148 + wireguardSizeHandshakeResponse = 92 + wireguardMinSizePacketData = 32 // 16 bytes header + 16 bytes AEAD overhead + wireguardSizePacketCookieReply = 64 +) + +type WireGuardAnalyzer struct{} + +func (a *WireGuardAnalyzer) Name() string { + return "wireguard" +} + +func (a *WireGuardAnalyzer) Limit() int { + return 0 +} + +func (a *WireGuardAnalyzer) NewUDP(info analyzer.UDPInfo, logger analyzer.Logger) analyzer.UDPStream { + return newWireGuardUDPStream(logger) +} + +type wireGuardUDPStream struct { + logger analyzer.Logger + invalidCount int + rememberedIndexes *ring.Ring + rememberedIndexesLock sync.RWMutex +} + +func newWireGuardUDPStream(logger analyzer.Logger) *wireGuardUDPStream { + return &wireGuardUDPStream{ + logger: logger, + rememberedIndexes: ring.New(wireguardRememberedIndexCount), + } +} + +func (s *wireGuardUDPStream) Feed(rev bool, data []byte) (u *analyzer.PropUpdate, done bool) { + m := s.parseWireGuardPacket(rev, data) + if m == nil { + s.invalidCount++ + return nil, s.invalidCount >= wireguardUDPInvalidCountThreshold + } + s.invalidCount = 0 // Reset invalid count on valid WireGuard packet + messageType := m[wireguardPropKeyMessageType].(byte) + propUpdateType := analyzer.PropUpdateMerge + if messageType == wireguardTypeHandshakeInitiation { + propUpdateType = analyzer.PropUpdateReplace + } + return &analyzer.PropUpdate{ + Type: propUpdateType, + M: m, + }, false +} + +func (s *wireGuardUDPStream) Close(limited bool) *analyzer.PropUpdate { + return nil +} + +func (s *wireGuardUDPStream) parseWireGuardPacket(rev bool, data []byte) analyzer.PropMap { + if len(data) < 4 { + return nil + } + if slices.Max(data[1:4]) != 0 { + return nil + } + + messageType := data[0] + var propKey string + var propValue analyzer.PropMap + switch messageType { + case wireguardTypeHandshakeInitiation: + propKey = "handshake_initiation" + propValue = s.parseWireGuardHandshakeInitiation(rev, data) + case wireguardTypeHandshakeResponse: + propKey = "handshake_response" + propValue = s.parseWireGuardHandshakeResponse(rev, data) + case wireguardTypeData: + propKey = "packet_data" + propValue = s.parseWireGuardPacketData(rev, data) + case wireguardTypeCookieReply: + propKey = "packet_cookie_reply" + propValue = s.parseWireGuardPacketCookieReply(rev, data) + } + if propValue == nil { + return nil + } + + m := make(analyzer.PropMap) + m[wireguardPropKeyMessageType] = messageType + m[propKey] = propValue + return m +} + +func (s *wireGuardUDPStream) parseWireGuardHandshakeInitiation(rev bool, data []byte) analyzer.PropMap { + if len(data) != wireguardSizeHandshakeInitiation { + return nil + } + m := make(analyzer.PropMap) + + senderIndex := binary.LittleEndian.Uint32(data[4:8]) + m["sender_index"] = senderIndex + s.putSenderIndex(rev, senderIndex) + + return m +} + +func (s *wireGuardUDPStream) parseWireGuardHandshakeResponse(rev bool, data []byte) analyzer.PropMap { + if len(data) != wireguardSizeHandshakeResponse { + return nil + } + m := make(analyzer.PropMap) + + senderIndex := binary.LittleEndian.Uint32(data[4:8]) + m["sender_index"] = senderIndex + s.putSenderIndex(rev, senderIndex) + + receiverIndex := binary.LittleEndian.Uint32(data[8:12]) + m["receiver_index"] = receiverIndex + m["receiver_index_matched"] = s.matchReceiverIndex(rev, receiverIndex) + + return m +} + +func (s *wireGuardUDPStream) parseWireGuardPacketData(rev bool, data []byte) analyzer.PropMap { + if len(data) < wireguardMinSizePacketData { + return nil + } + if len(data)%16 != 0 { + // WireGuard zero padding the packet to make the length a multiple of 16 + return nil + } + m := make(analyzer.PropMap) + + receiverIndex := binary.LittleEndian.Uint32(data[4:8]) + m["receiver_index"] = receiverIndex + m["receiver_index_matched"] = s.matchReceiverIndex(rev, receiverIndex) + + m["counter"] = binary.LittleEndian.Uint64(data[8:16]) + + return m +} + +func (s *wireGuardUDPStream) parseWireGuardPacketCookieReply(rev bool, data []byte) analyzer.PropMap { + if len(data) != wireguardSizePacketCookieReply { + return nil + } + m := make(analyzer.PropMap) + + receiverIndex := binary.LittleEndian.Uint32(data[4:8]) + m["receiver_index"] = receiverIndex + m["receiver_index_matched"] = s.matchReceiverIndex(rev, receiverIndex) + + return m +} + +type wireGuardIndex struct { + SenderIndex uint32 + Reverse bool +} + +func (s *wireGuardUDPStream) putSenderIndex(rev bool, senderIndex uint32) { + s.rememberedIndexesLock.Lock() + defer s.rememberedIndexesLock.Unlock() + + s.rememberedIndexes.Value = &wireGuardIndex{ + SenderIndex: senderIndex, + Reverse: rev, + } + s.rememberedIndexes = s.rememberedIndexes.Prev() +} + +func (s *wireGuardUDPStream) matchReceiverIndex(rev bool, receiverIndex uint32) bool { + s.rememberedIndexesLock.RLock() + defer s.rememberedIndexesLock.RUnlock() + + var found bool + ris := s.rememberedIndexes + for it := ris.Next(); it != ris; it = it.Next() { + if it.Value == nil { + break + } + wgidx := it.Value.(*wireGuardIndex) + if wgidx.Reverse == !rev && wgidx.SenderIndex == receiverIndex { + found = true + break + } + } + return found +} diff --git a/cmd/root.go b/cmd/root.go index a4bf13d..f3ff665 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -92,6 +92,7 @@ var analyzers = []analyzer.Analyzer{ &tcp.TLSAnalyzer{}, &tcp.TrojanAnalyzer{}, &udp.DNSAnalyzer{}, + &udp.WireGuardAnalyzer{}, } var modifiers = []modifier.Modifier{ diff --git a/docs/Analyzers.md b/docs/Analyzers.md index b068f2f..9db1e19 100644 --- a/docs/Analyzers.md +++ b/docs/Analyzers.md @@ -369,3 +369,54 @@ Example for blocking connections to `google.com:80` and user `foobar`: action: block expr: socks?.req?.auth?.method == 2 && socks?.req?.auth?.username == "foobar" ``` + + +## WireGuard + +```json5 +{ + "wireguard": { + "message_type": 1, // 0x1: handshake_initiation, 0x2: handshake_response, 0x3: packet_cookie_reply, 0x4: packet_data + "handshake_initiation": { + "sender_index": 0x12345678 + }, + "handshake_response": { + "sender_index": 0x12345678, + "receiver_index": 0x87654321, + "receiver_index_matched": true + }, + "packet_data": { + "receiver_index": 0x12345678, + "receiver_index_matched": true + }, + "packet_cookie_reply": { + "receiver_index": 0x12345678, + "receiver_index_matched": true + } + } +} +``` + +Example for blocking WireGuard traffic: + +```yaml +# false positive: high +- name: Block all WireGuard-like traffic + action: block + expr: wireguard != nil + +# false positive: medium +- name: Block WireGuard by handshake_initiation + action: drop + expr: wireguard?.handshake_initiation != nil + +# false positive: low +- name: Block WireGuard by handshake_response + action: drop + expr: wireguard?.handshake_response?.receiver_index_matched == true + +# false positive: pretty low +- name: Block WireGuard by packet_data + action: block + expr: wireguard?.packet_data?.receiver_index_matched == true +```