Skip to content

Commit

Permalink
Add WireGuard analyzer (#41)
Browse files Browse the repository at this point in the history
* feat: add WireGuard analyzer

* chore(wg): reduce map creating for non wg packets

* chore: import format

* docs: add wg usage

---------

Co-authored-by: Toby <[email protected]>
  • Loading branch information
haruue and tobyxdd authored Jan 31, 2024
1 parent f07a38b commit 8d94400
Show file tree
Hide file tree
Showing 6 changed files with 284 additions and 3 deletions.
6 changes: 5 additions & 1 deletion README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) に基づくトロイの木馬 (プロキシプロトコル) 検出
Expand Down Expand Up @@ -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")
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down
6 changes: 5 additions & 1 deletion README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
- [开发中] 基于机器学习的流量分类
Expand Down Expand Up @@ -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")
Expand Down
217 changes: 217 additions & 0 deletions analyzer/udp/wireguard.go
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ var analyzers = []analyzer.Analyzer{
&tcp.TLSAnalyzer{},
&tcp.TrojanAnalyzer{},
&udp.DNSAnalyzer{},
&udp.WireGuardAnalyzer{},
}

var modifiers = []modifier.Modifier{
Expand Down
51 changes: 51 additions & 0 deletions docs/Analyzers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

0 comments on commit 8d94400

Please sign in to comment.