Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54f62ce0bb | ||
|
|
22bbf0d9c7 | ||
|
|
7e511e94de | ||
|
|
e177837301 | ||
|
|
be672a97ab | ||
|
|
5c77cede3d | ||
|
|
ebff4308e4 | ||
|
|
94cfe7b2c1 | ||
|
|
ef352450a2 | ||
|
|
4ede93ce7b | ||
|
|
c1e90960dd | ||
|
|
7a52228ec6 | ||
|
|
6d33a0d51c | ||
|
|
27c9b91a61 | ||
|
|
36bb4b796d | ||
|
|
843f17896c | ||
|
|
6871244809 | ||
|
|
f8f0153664 | ||
|
|
8d94400855 | ||
|
|
f07a38bc47 |
49
README.ja.md
49
README.ja.md
@@ -16,17 +16,17 @@ OpenGFW は、Linux 上の [GFW](https://en.wikipedia.org/wiki/Great_Firewall)
|
||||
## 特徴
|
||||
|
||||
- フル IP/TCP 再アセンブル、各種プロトコルアナライザー
|
||||
- HTTP、TLS、DNS、SSH、SOCKS4/5、その他多数
|
||||
- Shadowsocks の"完全に暗号化されたトラフィック"の検出、
|
||||
など。 (https://gfw.report/publications/usenixsecurity23/data/paper/paper.pdf)
|
||||
- HTTP、TLS、QUIC、DNS、SSH、SOCKS4/5、WireGuard、その他多数
|
||||
- Shadowsocks の「完全に暗号化されたトラフィック」の検出など (https://gfw.report/publications/usenixsecurity23/data/paper/paper.pdf)
|
||||
- トロイの木馬キラー (https://github.com/XTLS/Trojan-killer) に基づくトロイの木馬 (プロキシプロトコル) 検出
|
||||
- [WIP] 機械学習に基づくトラフィック分類
|
||||
- IPv4 と IPv6 をフルサポート
|
||||
- フローベースのマルチコア負荷分散
|
||||
- 接続オフロード
|
||||
- [expr](https://github.com/expr-lang/expr) に基づく強力なルールエンジン
|
||||
- ルールのホットリロード (`SIGHUP` を送信してリロード)
|
||||
- 柔軟なアナライザ&モディファイアフレームワーク
|
||||
- 拡張可能な IO 実装(今のところ NFQueue のみ)
|
||||
- 拡張可能な IO 実装 (今のところ NFQueue のみ)
|
||||
- [WIP] ウェブ UI
|
||||
|
||||
## ユースケース
|
||||
@@ -52,6 +52,16 @@ export OPENGFW_LOG_LEVEL=debug
|
||||
./OpenGFW -c config.yaml rules.yaml
|
||||
```
|
||||
|
||||
#### OpenWrt
|
||||
|
||||
OpenGFW は OpenWrt 23.05 で動作することがテストされています(他のバージョンも動作するはずですが、検証されていません)。
|
||||
|
||||
依存関係をインストールしてください:
|
||||
|
||||
```shell
|
||||
opkg install kmod-nft-queue kmod-nf-conntrack-netlink
|
||||
```
|
||||
|
||||
### 設定例
|
||||
|
||||
```yaml
|
||||
@@ -65,12 +75,17 @@ workers:
|
||||
tcpMaxBufferedPagesTotal: 4096
|
||||
tcpMaxBufferedPagesPerConn: 64
|
||||
udpMaxStreams: 4096
|
||||
|
||||
# 特定のローカルGeoIP / GeoSiteデータベースファイルを読み込むためのパス。
|
||||
# 設定されていない場合は、https://github.com/LoyalSoldier/v2ray-rules-dat から自動的にダウンロードされます。
|
||||
# geo:
|
||||
# geoip: geoip.dat
|
||||
# geosite: geosite.dat
|
||||
```
|
||||
|
||||
### ルール例
|
||||
|
||||
サポートされているすべてのプロトコルと、それぞれのプロトコルがどのようなフィールドを持っているかについてのドキュメントはまだ準備できておりません。
|
||||
一旦は、"analyzer "ディレクトリの下にあるコードを直接チェックする必要があります。
|
||||
[アナライザーのプロパティ](docs/Analyzers.md)
|
||||
|
||||
式言語の構文については、[Expr 言語定義](https://expr-lang.org/docs/language-definition)を参照してください。
|
||||
|
||||
@@ -83,6 +98,10 @@ workers:
|
||||
action: block
|
||||
expr: string(tls?.req?.sni) endsWith "v2ex.com"
|
||||
|
||||
- name: block v2ex quic
|
||||
action: block
|
||||
expr: string(quic?.req?.sni) endsWith "v2ex.com"
|
||||
|
||||
- name: block shadowsocks
|
||||
action: block
|
||||
expr: fet != nil && fet.yes
|
||||
@@ -103,11 +122,27 @@ workers:
|
||||
- name: block google socks
|
||||
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")
|
||||
|
||||
- name: block CN geoip
|
||||
action: block
|
||||
expr: geoip(string(ip.dst), "cn")
|
||||
|
||||
- name: block cidr
|
||||
action: block
|
||||
expr: cidr(string(ip.dst), "192.168.0.0/16")
|
||||
```
|
||||
|
||||
#### サポートされるアクション
|
||||
|
||||
- `allow`: 接続を許可し、それ以上の処理は行わない。
|
||||
- `block`: 接続をブロックし。
|
||||
- `block`: 接続をブロックし、それ以上の処理は行わない。
|
||||
- `drop`: UDP の場合、ルールのトリガーとなったパケットをドロップし、同じフローに含まれる以降のパケットの処理を継続する。TCP の場合は、`block` と同じ。
|
||||
- `modify`: UDP の場合、与えられた修飾子を使って、ルールをトリガしたパケットを修正し、同じフロー内の今後のパケットを処理し続ける。TCP の場合は、`allow` と同じ。
|
||||
|
||||
39
README.md
39
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, QUIC, 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)
|
||||
@@ -29,6 +29,7 @@ Linux that's in many ways more powerful than the real thing. It's cyber sovereig
|
||||
- Flow-based multicore load balancing
|
||||
- Connection offloading
|
||||
- Powerful rule engine based on [expr](https://github.com/expr-lang/expr)
|
||||
- Hot-reloadable rules (send `SIGHUP` to reload)
|
||||
- Flexible analyzer & modifier framework
|
||||
- Extensible IO implementation (only NFQueue for now)
|
||||
- [WIP] Web UI
|
||||
@@ -56,6 +57,16 @@ export OPENGFW_LOG_LEVEL=debug
|
||||
./OpenGFW -c config.yaml rules.yaml
|
||||
```
|
||||
|
||||
#### OpenWrt
|
||||
|
||||
OpenGFW has been tested to work on OpenWrt 23.05 (other versions should also work, just not verified).
|
||||
|
||||
Install the dependencies:
|
||||
|
||||
```shell
|
||||
opkg install kmod-nft-queue kmod-nf-conntrack-netlink
|
||||
```
|
||||
|
||||
### Example config
|
||||
|
||||
```yaml
|
||||
@@ -69,6 +80,12 @@ workers:
|
||||
tcpMaxBufferedPagesTotal: 4096
|
||||
tcpMaxBufferedPagesPerConn: 64
|
||||
udpMaxStreams: 4096
|
||||
|
||||
# The path to load specific local geoip/geosite db files.
|
||||
# If not set, they will be automatically downloaded from https://github.com/Loyalsoldier/v2ray-rules-dat
|
||||
# geo:
|
||||
# geoip: geoip.dat
|
||||
# geosite: geosite.dat
|
||||
```
|
||||
|
||||
### Example rules
|
||||
@@ -87,6 +104,10 @@ to [Expr Language Definition](https://expr-lang.org/docs/language-definition).
|
||||
action: block
|
||||
expr: string(tls?.req?.sni) endsWith "v2ex.com"
|
||||
|
||||
- name: block v2ex quic
|
||||
action: block
|
||||
expr: string(quic?.req?.sni) endsWith "v2ex.com"
|
||||
|
||||
- name: block shadowsocks
|
||||
action: block
|
||||
expr: fet != nil && fet.yes
|
||||
@@ -107,6 +128,22 @@ to [Expr Language Definition](https://expr-lang.org/docs/language-definition).
|
||||
- name: block google socks
|
||||
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")
|
||||
|
||||
- name: block CN geoip
|
||||
action: block
|
||||
expr: geoip(string(ip.dst), "cn")
|
||||
|
||||
- name: block cidr
|
||||
action: block
|
||||
expr: cidr(string(ip.dst), "192.168.0.0/16")
|
||||
```
|
||||
|
||||
#### Supported actions
|
||||
|
||||
39
README.zh.md
39
README.zh.md
@@ -17,7 +17,7 @@ OpenGFW 是一个 Linux 上灵活、易用、开源的 [GFW](https://zh.wikipedi
|
||||
## 功能
|
||||
|
||||
- 完整的 IP/TCP 重组,各种协议解析器
|
||||
- HTTP, TLS, DNS, SSH, SOCKS4/5, 更多协议正在开发中
|
||||
- HTTP, TLS, QUIC, DNS, SSH, SOCKS4/5, WireGuard, 更多协议正在开发中
|
||||
- Shadowsocks 等 "全加密流量" 检测 (https://gfw.report/publications/usenixsecurity23/data/paper/paper.pdf)
|
||||
- 基于 Trojan-killer 的 Trojan 检测 (https://github.com/XTLS/Trojan-killer)
|
||||
- [开发中] 基于机器学习的流量分类
|
||||
@@ -25,6 +25,7 @@ OpenGFW 是一个 Linux 上灵活、易用、开源的 [GFW](https://zh.wikipedi
|
||||
- 基于流的多核负载均衡
|
||||
- 连接 offloading
|
||||
- 基于 [expr](https://github.com/expr-lang/expr) 的强大规则引擎
|
||||
- 规则可以热重载 (发送 `SIGHUP` 信号)
|
||||
- 灵活的协议解析和修改框架
|
||||
- 可扩展的 IO 实现 (目前只有 NFQueue)
|
||||
- [开发中] Web UI
|
||||
@@ -52,6 +53,16 @@ export OPENGFW_LOG_LEVEL=debug
|
||||
./OpenGFW -c config.yaml rules.yaml
|
||||
```
|
||||
|
||||
#### OpenWrt
|
||||
|
||||
OpenGFW 在 OpenWrt 23.05 上测试可用(其他版本应该也可以,暂时未经验证)。
|
||||
|
||||
安装依赖:
|
||||
|
||||
```shell
|
||||
opkg install kmod-nft-queue kmod-nf-conntrack-netlink
|
||||
```
|
||||
|
||||
### 样例配置
|
||||
|
||||
```yaml
|
||||
@@ -65,6 +76,12 @@ workers:
|
||||
tcpMaxBufferedPagesTotal: 4096
|
||||
tcpMaxBufferedPagesPerConn: 64
|
||||
udpMaxStreams: 4096
|
||||
|
||||
# 指定的 geoip/geosite 档案路径
|
||||
# 如果未设置,将自动从 https://github.com/Loyalsoldier/v2ray-rules-dat 下载
|
||||
# geo:
|
||||
# geoip: geoip.dat
|
||||
# geosite: geosite.dat
|
||||
```
|
||||
|
||||
### 样例规则
|
||||
@@ -82,6 +99,10 @@ workers:
|
||||
action: block
|
||||
expr: string(tls?.req?.sni) endsWith "v2ex.com"
|
||||
|
||||
- name: block v2ex quic
|
||||
action: block
|
||||
expr: string(quic?.req?.sni) endsWith "v2ex.com"
|
||||
|
||||
- name: block shadowsocks
|
||||
action: block
|
||||
expr: fet != nil && fet.yes
|
||||
@@ -102,6 +123,22 @@ workers:
|
||||
- name: block google socks
|
||||
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")
|
||||
|
||||
- name: block CN geoip
|
||||
action: block
|
||||
expr: geoip(string(ip.dst), "cn")
|
||||
|
||||
- name: block cidr
|
||||
action: block
|
||||
expr: cidr(string(ip.dst), "192.168.0.0/16")
|
||||
```
|
||||
|
||||
#### 支持的 action
|
||||
|
||||
205
analyzer/internal/tls.go
Normal file
205
analyzer/internal/tls.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"github.com/apernet/OpenGFW/analyzer"
|
||||
"github.com/apernet/OpenGFW/analyzer/utils"
|
||||
)
|
||||
|
||||
func ParseTLSClientHello(chBuf *utils.ByteBuffer) analyzer.PropMap {
|
||||
var ok bool
|
||||
m := make(analyzer.PropMap)
|
||||
// Version, random & session ID length combined are within 35 bytes,
|
||||
// so no need for bounds checking
|
||||
m["version"], _ = chBuf.GetUint16(false, true)
|
||||
m["random"], _ = chBuf.Get(32, true)
|
||||
sessionIDLen, _ := chBuf.GetByte(true)
|
||||
m["session"], ok = chBuf.Get(int(sessionIDLen), true)
|
||||
if !ok {
|
||||
// Not enough data for session ID
|
||||
return nil
|
||||
}
|
||||
cipherSuitesLen, ok := chBuf.GetUint16(false, true)
|
||||
if !ok {
|
||||
// Not enough data for cipher suites length
|
||||
return nil
|
||||
}
|
||||
if cipherSuitesLen%2 != 0 {
|
||||
// Cipher suites are 2 bytes each, so must be even
|
||||
return nil
|
||||
}
|
||||
ciphers := make([]uint16, cipherSuitesLen/2)
|
||||
for i := range ciphers {
|
||||
ciphers[i], ok = chBuf.GetUint16(false, true)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
m["ciphers"] = ciphers
|
||||
compressionMethodsLen, ok := chBuf.GetByte(true)
|
||||
if !ok {
|
||||
// Not enough data for compression methods length
|
||||
return nil
|
||||
}
|
||||
// Compression methods are 1 byte each, we just put a byte slice here
|
||||
m["compression"], ok = chBuf.Get(int(compressionMethodsLen), true)
|
||||
if !ok {
|
||||
// Not enough data for compression methods
|
||||
return nil
|
||||
}
|
||||
extsLen, ok := chBuf.GetUint16(false, true)
|
||||
if !ok {
|
||||
// No extensions, I guess it's possible?
|
||||
return m
|
||||
}
|
||||
extBuf, ok := chBuf.GetSubBuffer(int(extsLen), true)
|
||||
if !ok {
|
||||
// Not enough data for extensions
|
||||
return nil
|
||||
}
|
||||
for extBuf.Len() > 0 {
|
||||
extType, ok := extBuf.GetUint16(false, true)
|
||||
if !ok {
|
||||
// Not enough data for extension type
|
||||
return nil
|
||||
}
|
||||
extLen, ok := extBuf.GetUint16(false, true)
|
||||
if !ok {
|
||||
// Not enough data for extension length
|
||||
return nil
|
||||
}
|
||||
extDataBuf, ok := extBuf.GetSubBuffer(int(extLen), true)
|
||||
if !ok || !parseTLSExtensions(extType, extDataBuf, m) {
|
||||
// Not enough data for extension data, or invalid extension
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func ParseTLSServerHello(shBuf *utils.ByteBuffer) analyzer.PropMap {
|
||||
var ok bool
|
||||
m := make(analyzer.PropMap)
|
||||
// Version, random & session ID length combined are within 35 bytes,
|
||||
// so no need for bounds checking
|
||||
m["version"], _ = shBuf.GetUint16(false, true)
|
||||
m["random"], _ = shBuf.Get(32, true)
|
||||
sessionIDLen, _ := shBuf.GetByte(true)
|
||||
m["session"], ok = shBuf.Get(int(sessionIDLen), true)
|
||||
if !ok {
|
||||
// Not enough data for session ID
|
||||
return nil
|
||||
}
|
||||
cipherSuite, ok := shBuf.GetUint16(false, true)
|
||||
if !ok {
|
||||
// Not enough data for cipher suite
|
||||
return nil
|
||||
}
|
||||
m["cipher"] = cipherSuite
|
||||
compressionMethod, ok := shBuf.GetByte(true)
|
||||
if !ok {
|
||||
// Not enough data for compression method
|
||||
return nil
|
||||
}
|
||||
m["compression"] = compressionMethod
|
||||
extsLen, ok := shBuf.GetUint16(false, true)
|
||||
if !ok {
|
||||
// No extensions, I guess it's possible?
|
||||
return m
|
||||
}
|
||||
extBuf, ok := shBuf.GetSubBuffer(int(extsLen), true)
|
||||
if !ok {
|
||||
// Not enough data for extensions
|
||||
return nil
|
||||
}
|
||||
for extBuf.Len() > 0 {
|
||||
extType, ok := extBuf.GetUint16(false, true)
|
||||
if !ok {
|
||||
// Not enough data for extension type
|
||||
return nil
|
||||
}
|
||||
extLen, ok := extBuf.GetUint16(false, true)
|
||||
if !ok {
|
||||
// Not enough data for extension length
|
||||
return nil
|
||||
}
|
||||
extDataBuf, ok := extBuf.GetSubBuffer(int(extLen), true)
|
||||
if !ok || !parseTLSExtensions(extType, extDataBuf, m) {
|
||||
// Not enough data for extension data, or invalid extension
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func parseTLSExtensions(extType uint16, extDataBuf *utils.ByteBuffer, m analyzer.PropMap) bool {
|
||||
switch extType {
|
||||
case 0x0000: // SNI
|
||||
ok := extDataBuf.Skip(2) // Ignore list length, we only care about the first entry for now
|
||||
if !ok {
|
||||
// Not enough data for list length
|
||||
return false
|
||||
}
|
||||
sniType, ok := extDataBuf.GetByte(true)
|
||||
if !ok || sniType != 0 {
|
||||
// Not enough data for SNI type, or not hostname
|
||||
return false
|
||||
}
|
||||
sniLen, ok := extDataBuf.GetUint16(false, true)
|
||||
if !ok {
|
||||
// Not enough data for SNI length
|
||||
return false
|
||||
}
|
||||
m["sni"], ok = extDataBuf.GetString(int(sniLen), true)
|
||||
if !ok {
|
||||
// Not enough data for SNI
|
||||
return false
|
||||
}
|
||||
case 0x0010: // ALPN
|
||||
ok := extDataBuf.Skip(2) // Ignore list length, as we read until the end
|
||||
if !ok {
|
||||
// Not enough data for list length
|
||||
return false
|
||||
}
|
||||
var alpnList []string
|
||||
for extDataBuf.Len() > 0 {
|
||||
alpnLen, ok := extDataBuf.GetByte(true)
|
||||
if !ok {
|
||||
// Not enough data for ALPN length
|
||||
return false
|
||||
}
|
||||
alpn, ok := extDataBuf.GetString(int(alpnLen), true)
|
||||
if !ok {
|
||||
// Not enough data for ALPN
|
||||
return false
|
||||
}
|
||||
alpnList = append(alpnList, alpn)
|
||||
}
|
||||
m["alpn"] = alpnList
|
||||
case 0x002b: // Supported Versions
|
||||
if extDataBuf.Len() == 2 {
|
||||
// Server only selects one version
|
||||
m["supported_versions"], _ = extDataBuf.GetUint16(false, true)
|
||||
} else {
|
||||
// Client sends a list of versions
|
||||
ok := extDataBuf.Skip(1) // Ignore list length, as we read until the end
|
||||
if !ok {
|
||||
// Not enough data for list length
|
||||
return false
|
||||
}
|
||||
var versions []uint16
|
||||
for extDataBuf.Len() > 0 {
|
||||
ver, ok := extDataBuf.GetUint16(false, true)
|
||||
if !ok {
|
||||
// Not enough data for version
|
||||
return false
|
||||
}
|
||||
versions = append(versions, ver)
|
||||
}
|
||||
m["supported_versions"] = versions
|
||||
}
|
||||
case 0xfe0d: // ECH
|
||||
// We can't parse ECH for now, just set a flag
|
||||
m["ech"] = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package tcp
|
||||
|
||||
import (
|
||||
"github.com/apernet/OpenGFW/analyzer"
|
||||
"github.com/apernet/OpenGFW/analyzer/internal"
|
||||
"github.com/apernet/OpenGFW/analyzer/utils"
|
||||
)
|
||||
|
||||
@@ -142,74 +143,14 @@ func (s *tlsStream) parseClientHello() utils.LSMAction {
|
||||
// Not a full client hello yet
|
||||
return utils.LSMActionPause
|
||||
}
|
||||
s.reqUpdated = true
|
||||
s.reqMap = make(analyzer.PropMap)
|
||||
// Version, random & session ID length combined are within 35 bytes,
|
||||
// so no need for bounds checking
|
||||
s.reqMap["version"], _ = chBuf.GetUint16(false, true)
|
||||
s.reqMap["random"], _ = chBuf.Get(32, true)
|
||||
sessionIDLen, _ := chBuf.GetByte(true)
|
||||
s.reqMap["session"], ok = chBuf.Get(int(sessionIDLen), true)
|
||||
if !ok {
|
||||
// Not enough data for session ID
|
||||
m := internal.ParseTLSClientHello(chBuf)
|
||||
if m == nil {
|
||||
return utils.LSMActionCancel
|
||||
}
|
||||
cipherSuitesLen, ok := chBuf.GetUint16(false, true)
|
||||
if !ok {
|
||||
// Not enough data for cipher suites length
|
||||
return utils.LSMActionCancel
|
||||
}
|
||||
if cipherSuitesLen%2 != 0 {
|
||||
// Cipher suites are 2 bytes each, so must be even
|
||||
return utils.LSMActionCancel
|
||||
}
|
||||
ciphers := make([]uint16, cipherSuitesLen/2)
|
||||
for i := range ciphers {
|
||||
ciphers[i], ok = chBuf.GetUint16(false, true)
|
||||
if !ok {
|
||||
return utils.LSMActionCancel
|
||||
}
|
||||
}
|
||||
s.reqMap["ciphers"] = ciphers
|
||||
compressionMethodsLen, ok := chBuf.GetByte(true)
|
||||
if !ok {
|
||||
// Not enough data for compression methods length
|
||||
return utils.LSMActionCancel
|
||||
}
|
||||
// Compression methods are 1 byte each, we just put a byte slice here
|
||||
s.reqMap["compression"], ok = chBuf.Get(int(compressionMethodsLen), true)
|
||||
if !ok {
|
||||
// Not enough data for compression methods
|
||||
return utils.LSMActionCancel
|
||||
}
|
||||
extsLen, ok := chBuf.GetUint16(false, true)
|
||||
if !ok {
|
||||
// No extensions, I guess it's possible?
|
||||
} else {
|
||||
s.reqUpdated = true
|
||||
s.reqMap = m
|
||||
return utils.LSMActionNext
|
||||
}
|
||||
extBuf, ok := chBuf.GetSubBuffer(int(extsLen), true)
|
||||
if !ok {
|
||||
// Not enough data for extensions
|
||||
return utils.LSMActionCancel
|
||||
}
|
||||
for extBuf.Len() > 0 {
|
||||
extType, ok := extBuf.GetUint16(false, true)
|
||||
if !ok {
|
||||
// Not enough data for extension type
|
||||
return utils.LSMActionCancel
|
||||
}
|
||||
extLen, ok := extBuf.GetUint16(false, true)
|
||||
if !ok {
|
||||
// Not enough data for extension length
|
||||
return utils.LSMActionCancel
|
||||
}
|
||||
extDataBuf, ok := extBuf.GetSubBuffer(int(extLen), true)
|
||||
if !ok || !s.handleExtensions(extType, extDataBuf, s.reqMap) {
|
||||
// Not enough data for extension data, or invalid extension
|
||||
return utils.LSMActionCancel
|
||||
}
|
||||
}
|
||||
return utils.LSMActionNext
|
||||
}
|
||||
|
||||
func (s *tlsStream) parseServerHello() utils.LSMAction {
|
||||
@@ -218,131 +159,14 @@ func (s *tlsStream) parseServerHello() utils.LSMAction {
|
||||
// Not a full server hello yet
|
||||
return utils.LSMActionPause
|
||||
}
|
||||
s.respUpdated = true
|
||||
s.respMap = make(analyzer.PropMap)
|
||||
// Version, random & session ID length combined are within 35 bytes,
|
||||
// so no need for bounds checking
|
||||
s.respMap["version"], _ = shBuf.GetUint16(false, true)
|
||||
s.respMap["random"], _ = shBuf.Get(32, true)
|
||||
sessionIDLen, _ := shBuf.GetByte(true)
|
||||
s.respMap["session"], ok = shBuf.Get(int(sessionIDLen), true)
|
||||
if !ok {
|
||||
// Not enough data for session ID
|
||||
m := internal.ParseTLSServerHello(shBuf)
|
||||
if m == nil {
|
||||
return utils.LSMActionCancel
|
||||
}
|
||||
cipherSuite, ok := shBuf.GetUint16(false, true)
|
||||
if !ok {
|
||||
// Not enough data for cipher suite
|
||||
return utils.LSMActionCancel
|
||||
}
|
||||
s.respMap["cipher"] = cipherSuite
|
||||
compressionMethod, ok := shBuf.GetByte(true)
|
||||
if !ok {
|
||||
// Not enough data for compression method
|
||||
return utils.LSMActionCancel
|
||||
}
|
||||
s.respMap["compression"] = compressionMethod
|
||||
extsLen, ok := shBuf.GetUint16(false, true)
|
||||
if !ok {
|
||||
// No extensions, I guess it's possible?
|
||||
} else {
|
||||
s.respUpdated = true
|
||||
s.respMap = m
|
||||
return utils.LSMActionNext
|
||||
}
|
||||
extBuf, ok := shBuf.GetSubBuffer(int(extsLen), true)
|
||||
if !ok {
|
||||
// Not enough data for extensions
|
||||
return utils.LSMActionCancel
|
||||
}
|
||||
for extBuf.Len() > 0 {
|
||||
extType, ok := extBuf.GetUint16(false, true)
|
||||
if !ok {
|
||||
// Not enough data for extension type
|
||||
return utils.LSMActionCancel
|
||||
}
|
||||
extLen, ok := extBuf.GetUint16(false, true)
|
||||
if !ok {
|
||||
// Not enough data for extension length
|
||||
return utils.LSMActionCancel
|
||||
}
|
||||
extDataBuf, ok := extBuf.GetSubBuffer(int(extLen), true)
|
||||
if !ok || !s.handleExtensions(extType, extDataBuf, s.respMap) {
|
||||
// Not enough data for extension data, or invalid extension
|
||||
return utils.LSMActionCancel
|
||||
}
|
||||
}
|
||||
return utils.LSMActionNext
|
||||
}
|
||||
|
||||
func (s *tlsStream) handleExtensions(extType uint16, extDataBuf *utils.ByteBuffer, m analyzer.PropMap) bool {
|
||||
switch extType {
|
||||
case 0x0000: // SNI
|
||||
ok := extDataBuf.Skip(2) // Ignore list length, we only care about the first entry for now
|
||||
if !ok {
|
||||
// Not enough data for list length
|
||||
return false
|
||||
}
|
||||
sniType, ok := extDataBuf.GetByte(true)
|
||||
if !ok || sniType != 0 {
|
||||
// Not enough data for SNI type, or not hostname
|
||||
return false
|
||||
}
|
||||
sniLen, ok := extDataBuf.GetUint16(false, true)
|
||||
if !ok {
|
||||
// Not enough data for SNI length
|
||||
return false
|
||||
}
|
||||
m["sni"], ok = extDataBuf.GetString(int(sniLen), true)
|
||||
if !ok {
|
||||
// Not enough data for SNI
|
||||
return false
|
||||
}
|
||||
case 0x0010: // ALPN
|
||||
ok := extDataBuf.Skip(2) // Ignore list length, as we read until the end
|
||||
if !ok {
|
||||
// Not enough data for list length
|
||||
return false
|
||||
}
|
||||
var alpnList []string
|
||||
for extDataBuf.Len() > 0 {
|
||||
alpnLen, ok := extDataBuf.GetByte(true)
|
||||
if !ok {
|
||||
// Not enough data for ALPN length
|
||||
return false
|
||||
}
|
||||
alpn, ok := extDataBuf.GetString(int(alpnLen), true)
|
||||
if !ok {
|
||||
// Not enough data for ALPN
|
||||
return false
|
||||
}
|
||||
alpnList = append(alpnList, alpn)
|
||||
}
|
||||
m["alpn"] = alpnList
|
||||
case 0x002b: // Supported Versions
|
||||
if extDataBuf.Len() == 2 {
|
||||
// Server only selects one version
|
||||
m["supported_versions"], _ = extDataBuf.GetUint16(false, true)
|
||||
} else {
|
||||
// Client sends a list of versions
|
||||
ok := extDataBuf.Skip(1) // Ignore list length, as we read until the end
|
||||
if !ok {
|
||||
// Not enough data for list length
|
||||
return false
|
||||
}
|
||||
var versions []uint16
|
||||
for extDataBuf.Len() > 0 {
|
||||
ver, ok := extDataBuf.GetUint16(false, true)
|
||||
if !ok {
|
||||
// Not enough data for version
|
||||
return false
|
||||
}
|
||||
versions = append(versions, ver)
|
||||
}
|
||||
m["supported_versions"] = versions
|
||||
}
|
||||
case 0xfe0d: // ECH
|
||||
// We can't parse ECH for now, just set a flag
|
||||
m["ech"] = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *tlsStream) Close(limited bool) *analyzer.PropUpdate {
|
||||
|
||||
31
analyzer/udp/internal/quic/LICENSE
Normal file
31
analyzer/udp/internal/quic/LICENSE
Normal file
@@ -0,0 +1,31 @@
|
||||
Author:: Cuong Manh Le <cuong.manhle.vn@gmail.com>
|
||||
Copyright:: Copyright (c) 2023, Cuong Manh Le
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
* Neither the name of the @organization@ nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL LE MANH CUONG
|
||||
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
||||
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
|
||||
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
1
analyzer/udp/internal/quic/README.md
Normal file
1
analyzer/udp/internal/quic/README.md
Normal file
@@ -0,0 +1 @@
|
||||
The code here is from https://github.com/cuonglm/quicsni with various modifications.
|
||||
105
analyzer/udp/internal/quic/header.go
Normal file
105
analyzer/udp/internal/quic/header.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package quic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/quic-go/quic-go/quicvarint"
|
||||
)
|
||||
|
||||
// The Header represents a QUIC header.
|
||||
type Header struct {
|
||||
Type uint8
|
||||
Version uint32
|
||||
SrcConnectionID []byte
|
||||
DestConnectionID []byte
|
||||
Length int64
|
||||
Token []byte
|
||||
}
|
||||
|
||||
// ParseInitialHeader parses the initial packet of a QUIC connection,
|
||||
// return the initial header and number of bytes read so far.
|
||||
func ParseInitialHeader(data []byte) (*Header, int64, error) {
|
||||
br := bytes.NewReader(data)
|
||||
hdr, err := parseLongHeader(br)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
n := int64(len(data) - br.Len())
|
||||
return hdr, n, nil
|
||||
}
|
||||
|
||||
func parseLongHeader(b *bytes.Reader) (*Header, error) {
|
||||
typeByte, err := b.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h := &Header{}
|
||||
ver, err := beUint32(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.Version = ver
|
||||
if h.Version != 0 && typeByte&0x40 == 0 {
|
||||
return nil, errors.New("not a QUIC packet")
|
||||
}
|
||||
destConnIDLen, err := b.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.DestConnectionID = make([]byte, int(destConnIDLen))
|
||||
if err := readConnectionID(b, h.DestConnectionID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
srcConnIDLen, err := b.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.SrcConnectionID = make([]byte, int(srcConnIDLen))
|
||||
if err := readConnectionID(b, h.SrcConnectionID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
initialPacketType := byte(0b00)
|
||||
if h.Version == V2 {
|
||||
initialPacketType = 0b01
|
||||
}
|
||||
if (typeByte >> 4 & 0b11) == initialPacketType {
|
||||
tokenLen, err := quicvarint.Read(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tokenLen > uint64(b.Len()) {
|
||||
return nil, io.EOF
|
||||
}
|
||||
h.Token = make([]byte, tokenLen)
|
||||
if _, err := io.ReadFull(b, h.Token); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
pl, err := quicvarint.Read(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.Length = int64(pl)
|
||||
return h, err
|
||||
}
|
||||
|
||||
func readConnectionID(r io.Reader, cid []byte) error {
|
||||
_, err := io.ReadFull(r, cid)
|
||||
if err == io.ErrUnexpectedEOF {
|
||||
return io.EOF
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func beUint32(r io.Reader) (uint32, error) {
|
||||
b := make([]byte, 4)
|
||||
if _, err := io.ReadFull(r, b); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return binary.BigEndian.Uint32(b), nil
|
||||
}
|
||||
193
analyzer/udp/internal/quic/packet_protector.go
Normal file
193
analyzer/udp/internal/quic/packet_protector.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package quic
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
|
||||
"golang.org/x/crypto/chacha20"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/cryptobyte"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
// NewProtectionKey creates a new ProtectionKey.
|
||||
func NewProtectionKey(suite uint16, secret []byte, v uint32) (*ProtectionKey, error) {
|
||||
return newProtectionKey(suite, secret, v)
|
||||
}
|
||||
|
||||
// NewInitialProtectionKey is like NewProtectionKey, but the returned protection key
|
||||
// is used for encrypt/decrypt Initial Packet only.
|
||||
//
|
||||
// See: https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-initial-secrets
|
||||
func NewInitialProtectionKey(secret []byte, v uint32) (*ProtectionKey, error) {
|
||||
return NewProtectionKey(tls.TLS_AES_128_GCM_SHA256, secret, v)
|
||||
}
|
||||
|
||||
// NewPacketProtector creates a new PacketProtector.
|
||||
func NewPacketProtector(key *ProtectionKey) *PacketProtector {
|
||||
return &PacketProtector{key: key}
|
||||
}
|
||||
|
||||
// PacketProtector is used for protecting a QUIC packet.
|
||||
//
|
||||
// See: https://www.rfc-editor.org/rfc/rfc9001.html#name-packet-protection
|
||||
type PacketProtector struct {
|
||||
key *ProtectionKey
|
||||
}
|
||||
|
||||
// UnProtect decrypts a QUIC packet.
|
||||
func (pp *PacketProtector) UnProtect(packet []byte, pnOffset, pnMax int64) ([]byte, error) {
|
||||
if isLongHeader(packet[0]) && int64(len(packet)) < pnOffset+4+16 {
|
||||
return nil, errors.New("packet with long header is too small")
|
||||
}
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc9001.html#name-header-protection-sample
|
||||
sampleOffset := pnOffset + 4
|
||||
sample := packet[sampleOffset : sampleOffset+16]
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc9001.html#name-header-protection-applicati
|
||||
mask := pp.key.headerProtection(sample)
|
||||
if isLongHeader(packet[0]) {
|
||||
// Long header: 4 bits masked
|
||||
packet[0] ^= mask[0] & 0x0f
|
||||
} else {
|
||||
// Short header: 5 bits masked
|
||||
packet[0] ^= mask[0] & 0x1f
|
||||
}
|
||||
|
||||
pnLen := packet[0]&0x3 + 1
|
||||
pn := int64(0)
|
||||
for i := uint8(0); i < pnLen; i++ {
|
||||
packet[pnOffset:][i] ^= mask[1+i]
|
||||
pn = (pn << 8) | int64(packet[pnOffset:][i])
|
||||
}
|
||||
pn = decodePacketNumber(pnMax, pn, pnLen)
|
||||
hdr := packet[:pnOffset+int64(pnLen)]
|
||||
payload := packet[pnOffset:][pnLen:]
|
||||
dec, err := pp.key.aead.Open(payload[:0], pp.key.nonce(pn), payload, hdr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decryption failed: %w", err)
|
||||
}
|
||||
return dec, nil
|
||||
}
|
||||
|
||||
// ProtectionKey is the key used to protect a QUIC packet.
|
||||
type ProtectionKey struct {
|
||||
aead cipher.AEAD
|
||||
headerProtection func(sample []byte) (mask []byte)
|
||||
iv []byte
|
||||
}
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-aead-usage
|
||||
//
|
||||
// "The 62 bits of the reconstructed QUIC packet number in network byte order are
|
||||
// left-padded with zeros to the size of the IV. The exclusive OR of the padded
|
||||
// packet number and the IV forms the AEAD nonce."
|
||||
func (pk *ProtectionKey) nonce(pn int64) []byte {
|
||||
nonce := make([]byte, len(pk.iv))
|
||||
binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(pn))
|
||||
for i := range pk.iv {
|
||||
nonce[i] ^= pk.iv[i]
|
||||
}
|
||||
return nonce
|
||||
}
|
||||
|
||||
func newProtectionKey(suite uint16, secret []byte, v uint32) (*ProtectionKey, error) {
|
||||
switch suite {
|
||||
case tls.TLS_AES_128_GCM_SHA256:
|
||||
key := hkdfExpandLabel(crypto.SHA256.New, secret, keyLabel(v), nil, 16)
|
||||
c, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
aead, err := cipher.NewGCM(c)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
iv := hkdfExpandLabel(crypto.SHA256.New, secret, ivLabel(v), nil, aead.NonceSize())
|
||||
hpKey := hkdfExpandLabel(crypto.SHA256.New, secret, headerProtectionLabel(v), nil, 16)
|
||||
hp, err := aes.NewCipher(hpKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
k := &ProtectionKey{}
|
||||
k.aead = aead
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-aes-based-header-protection
|
||||
k.headerProtection = func(sample []byte) []byte {
|
||||
mask := make([]byte, hp.BlockSize())
|
||||
hp.Encrypt(mask, sample)
|
||||
return mask
|
||||
}
|
||||
k.iv = iv
|
||||
return k, nil
|
||||
case tls.TLS_CHACHA20_POLY1305_SHA256:
|
||||
key := hkdfExpandLabel(crypto.SHA256.New, secret, keyLabel(v), nil, chacha20poly1305.KeySize)
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iv := hkdfExpandLabel(crypto.SHA256.New, secret, ivLabel(v), nil, aead.NonceSize())
|
||||
hpKey := hkdfExpandLabel(sha256.New, secret, headerProtectionLabel(v), nil, chacha20.KeySize)
|
||||
k := &ProtectionKey{}
|
||||
k.aead = aead
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-chacha20-based-header-prote
|
||||
k.headerProtection = func(sample []byte) []byte {
|
||||
nonce := sample[4:16]
|
||||
c, err := chacha20.NewUnauthenticatedCipher(hpKey, nonce)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.SetCounter(binary.LittleEndian.Uint32(sample[:4]))
|
||||
mask := make([]byte, 5)
|
||||
c.XORKeyStream(mask, mask)
|
||||
return mask
|
||||
}
|
||||
k.iv = iv
|
||||
return k, nil
|
||||
}
|
||||
return nil, errors.New("not supported cipher suite")
|
||||
}
|
||||
|
||||
// decodePacketNumber decode the packet number after header protection removed.
|
||||
//
|
||||
// See: https://datatracker.ietf.org/doc/html/draft-ietf-quic-transport-32#section-appendix.a
|
||||
func decodePacketNumber(largest, truncated int64, nbits uint8) int64 {
|
||||
expected := largest + 1
|
||||
win := int64(1 << (nbits * 8))
|
||||
hwin := win / 2
|
||||
mask := win - 1
|
||||
candidate := (expected &^ mask) | truncated
|
||||
switch {
|
||||
case candidate <= expected-hwin && candidate < (1<<62)-win:
|
||||
return candidate + win
|
||||
case candidate > expected+hwin && candidate >= win:
|
||||
return candidate - win
|
||||
}
|
||||
return candidate
|
||||
}
|
||||
|
||||
// Copied from crypto/tls/key_schedule.go.
|
||||
func hkdfExpandLabel(hash func() hash.Hash, secret []byte, label string, context []byte, length int) []byte {
|
||||
var hkdfLabel cryptobyte.Builder
|
||||
hkdfLabel.AddUint16(uint16(length))
|
||||
hkdfLabel.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||
b.AddBytes([]byte("tls13 "))
|
||||
b.AddBytes([]byte(label))
|
||||
})
|
||||
hkdfLabel.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||
b.AddBytes(context)
|
||||
})
|
||||
out := make([]byte, length)
|
||||
n, err := hkdf.Expand(hash, secret, hkdfLabel.BytesOrPanic()).Read(out)
|
||||
if err != nil || n != length {
|
||||
panic("quic: HKDF-Expand-Label invocation failed unexpectedly")
|
||||
}
|
||||
return out
|
||||
}
|
||||
94
analyzer/udp/internal/quic/packet_protector_test.go
Normal file
94
analyzer/udp/internal/quic/packet_protector_test.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package quic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
func TestInitialPacketProtector_UnProtect(t *testing.T) {
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-server-initial
|
||||
protect := mustHexDecodeString(`
|
||||
c7ff0000200008f067a5502a4262b500 4075fb12ff07823a5d24534d906ce4c7
|
||||
6782a2167e3479c0f7f6395dc2c91676 302fe6d70bb7cbeb117b4ddb7d173498
|
||||
44fd61dae200b8338e1b932976b61d91 e64a02e9e0ee72e3a6f63aba4ceeeec5
|
||||
be2f24f2d86027572943533846caa13e 6f163fb257473d0eda5047360fd4a47e
|
||||
fd8142fafc0f76
|
||||
`)
|
||||
unProtect := mustHexDecodeString(`
|
||||
02000000000600405a020000560303ee fce7f7b37ba1d1632e96677825ddf739
|
||||
88cfc79825df566dc5430b9a045a1200 130100002e00330024001d00209d3c94
|
||||
0d89690b84d08a60993c144eca684d10 81287c834d5311bcf32bb9da1a002b00
|
||||
020304
|
||||
`)
|
||||
|
||||
connID := mustHexDecodeString(`8394c8f03e515708`)
|
||||
|
||||
packet := append([]byte{}, protect...)
|
||||
hdr, offset, err := ParseInitialHeader(packet)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
initialSecret := hkdf.Extract(crypto.SHA256.New, connID, getSalt(hdr.Version))
|
||||
serverSecret := hkdfExpandLabel(crypto.SHA256.New, initialSecret, "server in", []byte{}, crypto.SHA256.Size())
|
||||
key, err := NewInitialProtectionKey(serverSecret, hdr.Version)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pp := NewPacketProtector(key)
|
||||
got, err := pp.UnProtect(protect, offset, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(got, unProtect) {
|
||||
t.Error("UnProtect returns wrong result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPacketProtectorShortHeader_UnProtect(t *testing.T) {
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-chacha20-poly1305-short-hea
|
||||
protect := mustHexDecodeString(`4cfe4189655e5cd55c41f69080575d7999c25a5bfb`)
|
||||
unProtect := mustHexDecodeString(`01`)
|
||||
hdr := mustHexDecodeString(`4200bff4`)
|
||||
|
||||
secret := mustHexDecodeString(`9ac312a7f877468ebe69422748ad00a1 5443f18203a07d6060f688f30f21632b`)
|
||||
k, err := NewProtectionKey(tls.TLS_CHACHA20_POLY1305_SHA256, secret, V1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pnLen := int(hdr[0]&0x03) + 1
|
||||
offset := len(hdr) - pnLen
|
||||
pp := NewPacketProtector(k)
|
||||
got, err := pp.UnProtect(protect, int64(offset), 654360564)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(got, unProtect) {
|
||||
t.Error("UnProtect returns wrong result")
|
||||
}
|
||||
}
|
||||
|
||||
func mustHexDecodeString(s string) []byte {
|
||||
b, err := hex.DecodeString(normalizeHex(s))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func normalizeHex(s string) string {
|
||||
return strings.Map(func(c rune) rune {
|
||||
if unicode.IsSpace(c) {
|
||||
return -1
|
||||
}
|
||||
return c
|
||||
}, s)
|
||||
}
|
||||
122
analyzer/udp/internal/quic/payload.go
Normal file
122
analyzer/udp/internal/quic/payload.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package quic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
|
||||
"github.com/quic-go/quic-go/quicvarint"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
func ReadCryptoPayload(packet []byte) ([]byte, error) {
|
||||
hdr, offset, err := ParseInitialHeader(packet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Some sanity checks
|
||||
if hdr.Version != V1 && hdr.Version != V2 {
|
||||
return nil, fmt.Errorf("unsupported version: %x", hdr.Version)
|
||||
}
|
||||
if offset == 0 || hdr.Length == 0 {
|
||||
return nil, errors.New("invalid packet")
|
||||
}
|
||||
|
||||
initialSecret := hkdf.Extract(crypto.SHA256.New, hdr.DestConnectionID, getSalt(hdr.Version))
|
||||
clientSecret := hkdfExpandLabel(crypto.SHA256.New, initialSecret, "client in", []byte{}, crypto.SHA256.Size())
|
||||
key, err := NewInitialProtectionKey(clientSecret, hdr.Version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("NewInitialProtectionKey: %w", err)
|
||||
}
|
||||
pp := NewPacketProtector(key)
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-client-initial
|
||||
//
|
||||
// "The unprotected header includes the connection ID and a 4-byte packet number encoding for a packet number of 2"
|
||||
if int64(len(packet)) < offset+hdr.Length {
|
||||
return nil, fmt.Errorf("packet is too short: %d < %d", len(packet), offset+hdr.Length)
|
||||
}
|
||||
unProtectedPayload, err := pp.UnProtect(packet[:offset+hdr.Length], offset, 2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
frs, err := extractCryptoFrames(bytes.NewReader(unProtectedPayload))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := assembleCryptoFrames(frs)
|
||||
if data == nil {
|
||||
return nil, errors.New("unable to assemble crypto frames")
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
const (
|
||||
paddingFrameType = 0x00
|
||||
pingFrameType = 0x01
|
||||
cryptoFrameType = 0x06
|
||||
)
|
||||
|
||||
type cryptoFrame struct {
|
||||
Offset int64
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func extractCryptoFrames(r *bytes.Reader) ([]cryptoFrame, error) {
|
||||
var frames []cryptoFrame
|
||||
for r.Len() > 0 {
|
||||
typ, err := quicvarint.Read(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if typ == paddingFrameType || typ == pingFrameType {
|
||||
continue
|
||||
}
|
||||
if typ != cryptoFrameType {
|
||||
return nil, fmt.Errorf("encountered unexpected frame type: %d", typ)
|
||||
}
|
||||
var frame cryptoFrame
|
||||
offset, err := quicvarint.Read(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
frame.Offset = int64(offset)
|
||||
dataLen, err := quicvarint.Read(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
frame.Data = make([]byte, dataLen)
|
||||
if _, err := io.ReadFull(r, frame.Data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
frames = append(frames, frame)
|
||||
}
|
||||
return frames, nil
|
||||
}
|
||||
|
||||
// assembleCryptoFrames assembles multiple crypto frames into a single slice (if possible).
|
||||
// It returns an error if the frames cannot be assembled. This can happen if the frames are not contiguous.
|
||||
func assembleCryptoFrames(frames []cryptoFrame) []byte {
|
||||
if len(frames) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(frames) == 1 {
|
||||
return frames[0].Data
|
||||
}
|
||||
// sort the frames by offset
|
||||
sort.Slice(frames, func(i, j int) bool { return frames[i].Offset < frames[j].Offset })
|
||||
// check if the frames are contiguous
|
||||
for i := 1; i < len(frames); i++ {
|
||||
if frames[i].Offset != frames[i-1].Offset+int64(len(frames[i-1].Data)) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// concatenate the frames
|
||||
data := make([]byte, frames[len(frames)-1].Offset+int64(len(frames[len(frames)-1].Data)))
|
||||
for _, frame := range frames {
|
||||
copy(data[frame.Offset:], frame.Data)
|
||||
}
|
||||
return data
|
||||
}
|
||||
59
analyzer/udp/internal/quic/quic.go
Normal file
59
analyzer/udp/internal/quic/quic.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package quic
|
||||
|
||||
const (
|
||||
V1 uint32 = 0x1
|
||||
V2 uint32 = 0x6b3343cf
|
||||
|
||||
hkdfLabelKeyV1 = "quic key"
|
||||
hkdfLabelKeyV2 = "quicv2 key"
|
||||
hkdfLabelIVV1 = "quic iv"
|
||||
hkdfLabelIVV2 = "quicv2 iv"
|
||||
hkdfLabelHPV1 = "quic hp"
|
||||
hkdfLabelHPV2 = "quicv2 hp"
|
||||
)
|
||||
|
||||
var (
|
||||
quicSaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99}
|
||||
// https://www.rfc-editor.org/rfc/rfc9001.html#name-initial-secrets
|
||||
quicSaltV1 = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a}
|
||||
// https://www.ietf.org/archive/id/draft-ietf-quic-v2-10.html#name-initial-salt-2
|
||||
quicSaltV2 = []byte{0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93, 0x81, 0xbe, 0x6e, 0x26, 0x9d, 0xcb, 0xf9, 0xbd, 0x2e, 0xd9}
|
||||
)
|
||||
|
||||
// isLongHeader reports whether b is the first byte of a long header packet.
|
||||
func isLongHeader(b byte) bool {
|
||||
return b&0x80 > 0
|
||||
}
|
||||
|
||||
func getSalt(v uint32) []byte {
|
||||
switch v {
|
||||
case V1:
|
||||
return quicSaltV1
|
||||
case V2:
|
||||
return quicSaltV2
|
||||
}
|
||||
return quicSaltOld
|
||||
}
|
||||
|
||||
func keyLabel(v uint32) string {
|
||||
kl := hkdfLabelKeyV1
|
||||
if v == V2 {
|
||||
kl = hkdfLabelKeyV2
|
||||
}
|
||||
return kl
|
||||
}
|
||||
|
||||
func ivLabel(v uint32) string {
|
||||
ivl := hkdfLabelIVV1
|
||||
if v == V2 {
|
||||
ivl = hkdfLabelIVV2
|
||||
}
|
||||
return ivl
|
||||
}
|
||||
|
||||
func headerProtectionLabel(v uint32) string {
|
||||
if v == V2 {
|
||||
return hkdfLabelHPV2
|
||||
}
|
||||
return hkdfLabelHPV1
|
||||
}
|
||||
82
analyzer/udp/quic.go
Normal file
82
analyzer/udp/quic.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package udp
|
||||
|
||||
import (
|
||||
"github.com/apernet/OpenGFW/analyzer"
|
||||
"github.com/apernet/OpenGFW/analyzer/internal"
|
||||
"github.com/apernet/OpenGFW/analyzer/udp/internal/quic"
|
||||
"github.com/apernet/OpenGFW/analyzer/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
quicInvalidCountThreshold = 4
|
||||
)
|
||||
|
||||
var (
|
||||
_ analyzer.UDPAnalyzer = (*QUICAnalyzer)(nil)
|
||||
_ analyzer.UDPStream = (*quicStream)(nil)
|
||||
)
|
||||
|
||||
type QUICAnalyzer struct{}
|
||||
|
||||
func (a *QUICAnalyzer) Name() string {
|
||||
return "quic"
|
||||
}
|
||||
|
||||
func (a *QUICAnalyzer) Limit() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (a *QUICAnalyzer) NewUDP(info analyzer.UDPInfo, logger analyzer.Logger) analyzer.UDPStream {
|
||||
return &quicStream{logger: logger}
|
||||
}
|
||||
|
||||
type quicStream struct {
|
||||
logger analyzer.Logger
|
||||
invalidCount int
|
||||
}
|
||||
|
||||
func (s *quicStream) Feed(rev bool, data []byte) (u *analyzer.PropUpdate, done bool) {
|
||||
if rev {
|
||||
// We don't support server direction for now
|
||||
s.invalidCount++
|
||||
return nil, s.invalidCount >= quicInvalidCountThreshold
|
||||
}
|
||||
pl, err := quic.ReadCryptoPayload(data)
|
||||
if err != nil || len(pl) < 4 {
|
||||
s.invalidCount++
|
||||
return nil, s.invalidCount >= quicInvalidCountThreshold
|
||||
}
|
||||
// Should be a TLS client hello
|
||||
if pl[0] != 0x01 {
|
||||
// Not a client hello
|
||||
s.invalidCount++
|
||||
return nil, s.invalidCount >= quicInvalidCountThreshold
|
||||
}
|
||||
chLen := int(pl[1])<<16 | int(pl[2])<<8 | int(pl[3])
|
||||
if chLen < 41 {
|
||||
// 2 (Protocol Version) +
|
||||
// 32 (Random) +
|
||||
// 1 (Session ID Length) +
|
||||
// 2 (Cipher Suites Length) +_ws.col.protocol == "TLSv1.3"
|
||||
// 2 (Cipher Suite) +
|
||||
// 1 (Compression Methods Length) +
|
||||
// 1 (Compression Method) +
|
||||
// No extensions
|
||||
// This should be the bare minimum for a client hello
|
||||
s.invalidCount++
|
||||
return nil, s.invalidCount >= quicInvalidCountThreshold
|
||||
}
|
||||
m := internal.ParseTLSClientHello(&utils.ByteBuffer{Buf: pl[4:]})
|
||||
if m == nil {
|
||||
s.invalidCount++
|
||||
return nil, s.invalidCount >= quicInvalidCountThreshold
|
||||
}
|
||||
return &analyzer.PropUpdate{
|
||||
Type: analyzer.PropUpdateMerge,
|
||||
M: analyzer.PropMap{"req": m},
|
||||
}, true
|
||||
}
|
||||
|
||||
func (s *quicStream) Close(limited bool) *analyzer.PropUpdate {
|
||||
return nil
|
||||
}
|
||||
217
analyzer/udp/wireguard.go
Normal file
217
analyzer/udp/wireguard.go
Normal file
@@ -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
|
||||
}
|
||||
49
cmd/root.go
49
cmd/root.go
@@ -7,6 +7,7 @@ import (
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/apernet/OpenGFW/analyzer"
|
||||
"github.com/apernet/OpenGFW/analyzer/tcp"
|
||||
@@ -92,6 +93,8 @@ var analyzers = []analyzer.Analyzer{
|
||||
&tcp.TLSAnalyzer{},
|
||||
&tcp.TrojanAnalyzer{},
|
||||
&udp.DNSAnalyzer{},
|
||||
&udp.QUICAnalyzer{},
|
||||
&udp.WireGuardAnalyzer{},
|
||||
}
|
||||
|
||||
var modifiers = []modifier.Modifier{
|
||||
@@ -161,6 +164,7 @@ func initLogger() {
|
||||
type cliConfig struct {
|
||||
IO cliConfigIO `mapstructure:"io"`
|
||||
Workers cliConfigWorkers `mapstructure:"workers"`
|
||||
Ruleset cliConfigRuleset `mapstructure:"ruleset"`
|
||||
}
|
||||
|
||||
type cliConfigIO struct {
|
||||
@@ -176,6 +180,11 @@ type cliConfigWorkers struct {
|
||||
UDPMaxStreams int `mapstructure:"udpMaxStreams"`
|
||||
}
|
||||
|
||||
type cliConfigRuleset struct {
|
||||
GeoIp string `mapstructure:"geoip"`
|
||||
GeoSite string `mapstructure:"geosite"`
|
||||
}
|
||||
|
||||
func (c *cliConfig) fillLogger(config *engine.Config) error {
|
||||
config.Logger = &engineLogger{}
|
||||
return nil
|
||||
@@ -244,7 +253,11 @@ func runMain(cmd *cobra.Command, args []string) {
|
||||
if err != nil {
|
||||
logger.Fatal("failed to load rules", zap.Error(err))
|
||||
}
|
||||
rs, err := ruleset.CompileExprRules(rawRs, analyzers, modifiers)
|
||||
rsConfig := &ruleset.BuiltinConfig{
|
||||
GeoSiteFilename: config.Ruleset.GeoSite,
|
||||
GeoIpFilename: config.Ruleset.GeoIp,
|
||||
}
|
||||
rs, err := ruleset.CompileExprRules(rawRs, analyzers, modifiers, rsConfig)
|
||||
if err != nil {
|
||||
logger.Fatal("failed to compile rules", zap.Error(err))
|
||||
}
|
||||
@@ -256,14 +269,42 @@ func runMain(cmd *cobra.Command, args []string) {
|
||||
logger.Fatal("failed to initialize engine", zap.Error(err))
|
||||
}
|
||||
|
||||
// Signal handling
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
sigChan := make(chan os.Signal)
|
||||
signal.Notify(sigChan, os.Interrupt, os.Kill)
|
||||
<-sigChan
|
||||
// Graceful shutdown
|
||||
shutdownChan := make(chan os.Signal)
|
||||
signal.Notify(shutdownChan, os.Interrupt, os.Kill)
|
||||
<-shutdownChan
|
||||
logger.Info("shutting down gracefully...")
|
||||
cancelFunc()
|
||||
}()
|
||||
go func() {
|
||||
// Rule reload
|
||||
reloadChan := make(chan os.Signal)
|
||||
signal.Notify(reloadChan, syscall.SIGHUP)
|
||||
for {
|
||||
<-reloadChan
|
||||
logger.Info("reloading rules")
|
||||
rawRs, err := ruleset.ExprRulesFromYAML(args[0])
|
||||
if err != nil {
|
||||
logger.Error("failed to load rules, using old rules", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
rs, err := ruleset.CompileExprRules(rawRs, analyzers, modifiers, rsConfig)
|
||||
if err != nil {
|
||||
logger.Error("failed to compile rules, using old rules", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
err = en.UpdateRuleset(rs)
|
||||
if err != nil {
|
||||
logger.Error("failed to update ruleset", zap.Error(err))
|
||||
} else {
|
||||
logger.Info("rules reloaded")
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
logger.Info("engine started")
|
||||
logger.Info("engine exited", zap.Error(en.Run(ctx)))
|
||||
}
|
||||
|
||||
@@ -179,51 +179,17 @@ Example for blocking all SSH connections:
|
||||
{
|
||||
"tls": {
|
||||
"req": {
|
||||
"alpn": [
|
||||
"h2",
|
||||
"http/1.1"
|
||||
],
|
||||
"alpn": ["h2", "http/1.1"],
|
||||
"ciphers": [
|
||||
4866,
|
||||
4867,
|
||||
4865,
|
||||
49196,
|
||||
49200,
|
||||
159,
|
||||
52393,
|
||||
52392,
|
||||
52394,
|
||||
49195,
|
||||
49199,
|
||||
158,
|
||||
49188,
|
||||
49192,
|
||||
107,
|
||||
49187,
|
||||
49191,
|
||||
103,
|
||||
49162,
|
||||
49172,
|
||||
57,
|
||||
49161,
|
||||
49171,
|
||||
51,
|
||||
157,
|
||||
156,
|
||||
61,
|
||||
60,
|
||||
53,
|
||||
47,
|
||||
255
|
||||
4866, 4867, 4865, 49196, 49200, 159, 52393, 52392, 52394, 49195, 49199,
|
||||
158, 49188, 49192, 107, 49187, 49191, 103, 49162, 49172, 57, 49161,
|
||||
49171, 51, 157, 156, 61, 60, 53, 47, 255
|
||||
],
|
||||
"compression": "AA==",
|
||||
"random": "UqfPi+EmtMgusILrKcELvVWwpOdPSM/My09nPXl84dg=",
|
||||
"session": "jCTrpAzHpwrfuYdYx4FEjZwbcQxCuZ52HGIoOcbw1vA=",
|
||||
"sni": "ipinfo.io",
|
||||
"supported_versions": [
|
||||
772,
|
||||
771
|
||||
],
|
||||
"supported_versions": [772, 771],
|
||||
"version": 771,
|
||||
"ech": true
|
||||
},
|
||||
@@ -247,6 +213,37 @@ Example for blocking TLS connections to `ipinfo.io`:
|
||||
expr: tls != nil && tls.req != nil && tls.req.sni == "ipinfo.io"
|
||||
```
|
||||
|
||||
## QUIC
|
||||
|
||||
QUIC analyzer produces the same result format as TLS analyzer, but currently only supports "req" direction (client
|
||||
hello), not "resp" (server hello).
|
||||
|
||||
```json
|
||||
{
|
||||
"quic": {
|
||||
"req": {
|
||||
"alpn": ["h3"],
|
||||
"ciphers": [4865, 4866, 4867],
|
||||
"compression": "AA==",
|
||||
"ech": true,
|
||||
"random": "FUYLceFReLJl9dRQ0HAus7fi2ZGuKIAApF4keeUqg00=",
|
||||
"session": "",
|
||||
"sni": "quic.rocks",
|
||||
"supported_versions": [772],
|
||||
"version": 771
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Example for blocking QUIC connections to `quic.rocks`:
|
||||
|
||||
```yaml
|
||||
- name: Block quic.rocks QUIC
|
||||
action: block
|
||||
expr: quic != nil && quic.req != nil && quic.req.sni == "quic.rocks"
|
||||
```
|
||||
|
||||
## Trojan (proxy protocol)
|
||||
|
||||
Check https://github.com/XTLS/Trojan-killer for more information.
|
||||
@@ -273,13 +270,13 @@ Example for blocking Trojan connections:
|
||||
|
||||
SOCKS4:
|
||||
|
||||
```json5
|
||||
```json
|
||||
{
|
||||
"socks": {
|
||||
"version": 4,
|
||||
"req": {
|
||||
"cmd": 1,
|
||||
"addr_type": 1, // same as socks5
|
||||
"addr_type": 1, // same as socks5
|
||||
"addr": "1.1.1.1",
|
||||
// for socks4a
|
||||
// "addr_type": 3,
|
||||
@@ -290,7 +287,7 @@ SOCKS4:
|
||||
}
|
||||
},
|
||||
"resp": {
|
||||
"rep": 90, // 0x5A(90) granted
|
||||
"rep": 90, // 0x5A(90) granted
|
||||
"addr_type": 1,
|
||||
"addr": "1.1.1.1",
|
||||
"port": 443
|
||||
@@ -301,26 +298,26 @@ SOCKS4:
|
||||
|
||||
SOCKS5 without auth:
|
||||
|
||||
```json5
|
||||
```json
|
||||
{
|
||||
"socks": {
|
||||
"version": 5,
|
||||
"req": {
|
||||
"cmd": 1, // 0x01: connect, 0x02: bind, 0x03: udp
|
||||
"addr_type": 3, // 0x01: ipv4, 0x03: domain, 0x04: ipv6
|
||||
"cmd": 1, // 0x01: connect, 0x02: bind, 0x03: udp
|
||||
"addr_type": 3, // 0x01: ipv4, 0x03: domain, 0x04: ipv6
|
||||
"addr": "google.com",
|
||||
"port": 80,
|
||||
"auth": {
|
||||
"method": 0 // 0x00: no auth, 0x02: username/password
|
||||
"method": 0 // 0x00: no auth, 0x02: username/password
|
||||
}
|
||||
},
|
||||
"resp": {
|
||||
"rep": 0, // 0x00: success
|
||||
"addr_type": 1, // 0x01: ipv4, 0x03: domain, 0x04: ipv6
|
||||
"rep": 0, // 0x00: success
|
||||
"addr_type": 1, // 0x01: ipv4, 0x03: domain, 0x04: ipv6
|
||||
"addr": "198.18.1.31",
|
||||
"port": 80,
|
||||
"auth": {
|
||||
"method": 0 // 0x00: no auth, 0x02: username/password
|
||||
"method": 0 // 0x00: no auth, 0x02: username/password
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -329,29 +326,29 @@ SOCKS5 without auth:
|
||||
|
||||
SOCKS5 with auth:
|
||||
|
||||
```json5
|
||||
```json
|
||||
{
|
||||
"socks": {
|
||||
"version": 5,
|
||||
"req": {
|
||||
"cmd": 1, // 0x01: connect, 0x02: bind, 0x03: udp
|
||||
"addr_type": 3, // 0x01: ipv4, 0x03: domain, 0x04: ipv6
|
||||
"cmd": 1, // 0x01: connect, 0x02: bind, 0x03: udp
|
||||
"addr_type": 3, // 0x01: ipv4, 0x03: domain, 0x04: ipv6
|
||||
"addr": "google.com",
|
||||
"port": 80,
|
||||
"auth": {
|
||||
"method": 2, // 0x00: no auth, 0x02: username/password
|
||||
"method": 2, // 0x00: no auth, 0x02: username/password
|
||||
"username": "user",
|
||||
"password": "pass"
|
||||
}
|
||||
},
|
||||
"resp": {
|
||||
"rep": 0, // 0x00: success
|
||||
"addr_type": 1, // 0x01: ipv4, 0x03: domain, 0x04: ipv6
|
||||
"rep": 0, // 0x00: success
|
||||
"addr_type": 1, // 0x01: ipv4, 0x03: domain, 0x04: ipv6
|
||||
"addr": "198.18.1.31",
|
||||
"port": 80,
|
||||
"auth": {
|
||||
"method": 2, // 0x00: no auth, 0x02: username/password
|
||||
"status": 0 // 0x00: success, 0x01: failure
|
||||
"method": 2, // 0x00: no auth, 0x02: username/password
|
||||
"status": 0 // 0x00: success, 0x01: failure
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -369,3 +366,53 @@ 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
|
||||
|
||||
```json
|
||||
{
|
||||
"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
|
||||
```
|
||||
|
||||
@@ -60,13 +60,6 @@ func (f *tcpStreamFactory) New(ipFlow, tcpFlow gopacket.Flow, tcp *layers.TCP, a
|
||||
rs := f.Ruleset
|
||||
f.RulesetMutex.RUnlock()
|
||||
ans := analyzersToTCPAnalyzers(rs.Analyzers(info))
|
||||
if len(ans) == 0 {
|
||||
ctx := ac.(*tcpContext)
|
||||
ctx.Verdict = tcpVerdictAcceptStream
|
||||
f.Logger.TCPStreamAction(info, ruleset.ActionAllow, true)
|
||||
// a tcpStream with no activeEntries is a no-op
|
||||
return &tcpStream{}
|
||||
}
|
||||
// Create entries for each analyzer
|
||||
entries := make([]*tcpStreamEntry, 0, len(ans))
|
||||
for _, a := range ans {
|
||||
@@ -109,6 +102,7 @@ type tcpStream struct {
|
||||
ruleset ruleset.Ruleset
|
||||
activeEntries []*tcpStreamEntry
|
||||
doneEntries []*tcpStreamEntry
|
||||
lastVerdict tcpVerdict
|
||||
}
|
||||
|
||||
type tcpStreamEntry struct {
|
||||
@@ -119,8 +113,16 @@ type tcpStreamEntry struct {
|
||||
}
|
||||
|
||||
func (s *tcpStream) Accept(tcp *layers.TCP, ci gopacket.CaptureInfo, dir reassembly.TCPFlowDirection, nextSeq reassembly.Sequence, start *bool, ac reassembly.AssemblerContext) bool {
|
||||
// Only accept packets if we still have active entries
|
||||
return len(s.activeEntries) > 0
|
||||
if len(s.activeEntries) > 0 || s.virgin {
|
||||
// Make sure every stream matches against the ruleset at least once,
|
||||
// even if there are no activeEntries, as the ruleset may have built-in
|
||||
// properties that need to be matched.
|
||||
return true
|
||||
} else {
|
||||
ctx := ac.(*tcpContext)
|
||||
ctx.Verdict = s.lastVerdict
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (s *tcpStream) ReassembledSG(sg reassembly.ScatterGather, ac reassembly.AssemblerContext) {
|
||||
@@ -152,7 +154,9 @@ func (s *tcpStream) ReassembledSG(sg reassembly.ScatterGather, ac reassembly.Ass
|
||||
}
|
||||
action := result.Action
|
||||
if action != ruleset.ActionMaybe && action != ruleset.ActionModify {
|
||||
ctx.Verdict = actionToTCPVerdict(action)
|
||||
verdict := actionToTCPVerdict(action)
|
||||
s.lastVerdict = verdict
|
||||
ctx.Verdict = verdict
|
||||
s.logger.TCPStreamAction(s.info, action, false)
|
||||
// Verdict issued, no need to process any more packets
|
||||
s.closeActiveEntries()
|
||||
@@ -160,6 +164,7 @@ func (s *tcpStream) ReassembledSG(sg reassembly.ScatterGather, ac reassembly.Ass
|
||||
}
|
||||
if len(s.activeEntries) == 0 && ctx.Verdict == tcpVerdictAccept {
|
||||
// All entries are done but no verdict issued, accept stream
|
||||
s.lastVerdict = tcpVerdictAcceptStream
|
||||
ctx.Verdict = tcpVerdictAcceptStream
|
||||
s.logger.TCPStreamAction(s.info, ruleset.ActionAllow, true)
|
||||
}
|
||||
|
||||
@@ -61,12 +61,6 @@ func (f *udpStreamFactory) New(ipFlow, udpFlow gopacket.Flow, udp *layers.UDP, u
|
||||
rs := f.Ruleset
|
||||
f.RulesetMutex.RUnlock()
|
||||
ans := analyzersToUDPAnalyzers(rs.Analyzers(info))
|
||||
if len(ans) == 0 {
|
||||
uc.Verdict = udpVerdictAcceptStream
|
||||
f.Logger.UDPStreamAction(info, ruleset.ActionAllow, true)
|
||||
// a udpStream with no activeEntries is a no-op
|
||||
return &udpStream{}
|
||||
}
|
||||
// Create entries for each analyzer
|
||||
entries := make([]*udpStreamEntry, 0, len(ans))
|
||||
for _, a := range ans {
|
||||
@@ -167,6 +161,7 @@ type udpStream struct {
|
||||
ruleset ruleset.Ruleset
|
||||
activeEntries []*udpStreamEntry
|
||||
doneEntries []*udpStreamEntry
|
||||
lastVerdict udpVerdict
|
||||
}
|
||||
|
||||
type udpStreamEntry struct {
|
||||
@@ -177,8 +172,15 @@ type udpStreamEntry struct {
|
||||
}
|
||||
|
||||
func (s *udpStream) Accept(udp *layers.UDP, rev bool, uc *udpContext) bool {
|
||||
// Only accept packets if we still have active entries
|
||||
return len(s.activeEntries) > 0
|
||||
if len(s.activeEntries) > 0 || s.virgin {
|
||||
// Make sure every stream matches against the ruleset at least once,
|
||||
// even if there are no activeEntries, as the ruleset may have built-in
|
||||
// properties that need to be matched.
|
||||
return true
|
||||
} else {
|
||||
uc.Verdict = s.lastVerdict
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (s *udpStream) Feed(udp *layers.UDP, rev bool, uc *udpContext) {
|
||||
@@ -221,8 +223,9 @@ func (s *udpStream) Feed(udp *layers.UDP, rev bool, uc *udpContext) {
|
||||
}
|
||||
}
|
||||
if action != ruleset.ActionMaybe {
|
||||
var final bool
|
||||
uc.Verdict, final = actionToUDPVerdict(action)
|
||||
verdict, final := actionToUDPVerdict(action)
|
||||
s.lastVerdict = verdict
|
||||
uc.Verdict = verdict
|
||||
s.logger.UDPStreamAction(s.info, action, false)
|
||||
if final {
|
||||
s.closeActiveEntries()
|
||||
@@ -231,6 +234,7 @@ func (s *udpStream) Feed(udp *layers.UDP, rev bool, uc *udpContext) {
|
||||
}
|
||||
if len(s.activeEntries) == 0 && uc.Verdict == udpVerdictAccept {
|
||||
// All entries are done but no verdict issued, accept stream
|
||||
s.lastVerdict = udpVerdictAcceptStream
|
||||
uc.Verdict = udpVerdictAcceptStream
|
||||
s.logger.UDPStreamAction(s.info, ruleset.ActionAllow, true)
|
||||
}
|
||||
|
||||
@@ -127,7 +127,10 @@ func (w *worker) Run(ctx context.Context) {
|
||||
}
|
||||
|
||||
func (w *worker) UpdateRuleset(r ruleset.Ruleset) error {
|
||||
return w.tcpStreamFactory.UpdateRuleset(r)
|
||||
if err := w.tcpStreamFactory.UpdateRuleset(r); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.udpStreamFactory.UpdateRuleset(r)
|
||||
}
|
||||
|
||||
func (w *worker) handle(streamID uint32, p gopacket.Packet) (io.Verdict, []byte) {
|
||||
|
||||
12
go.mod
12
go.mod
@@ -1,22 +1,27 @@
|
||||
module github.com/apernet/OpenGFW
|
||||
|
||||
go 1.20
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/bwmarrin/snowflake v0.3.0
|
||||
github.com/coreos/go-iptables v0.7.0
|
||||
github.com/expr-lang/expr v1.15.7
|
||||
github.com/florianl/go-nfqueue v1.3.2-0.20231218173729-f2bdeb033acf
|
||||
github.com/google/gopacket v1.1.19
|
||||
github.com/google/gopacket v1.1.20-0.20220810144506-32ee38206866
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7
|
||||
github.com/mdlayher/netlink v1.6.0
|
||||
github.com/quic-go/quic-go v0.41.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/viper v1.18.2
|
||||
github.com/stretchr/testify v1.8.4
|
||||
go.uber.org/zap v1.26.0
|
||||
golang.org/x/crypto v0.19.0
|
||||
google.golang.org/protobuf v1.31.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
@@ -26,6 +31,7 @@ require (
|
||||
github.com/mdlayher/socket v0.1.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
@@ -37,7 +43,7 @@ require (
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sync v0.5.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
)
|
||||
|
||||
42
go.sum
42
go.sum
@@ -6,19 +6,29 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/expr-lang/expr v1.15.7 h1:BK0JcWUkoW6nrbLBo6xCKhz4BvH5DSOOu1Gx5lucyZo=
|
||||
github.com/expr-lang/expr v1.15.7/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
|
||||
github.com/florianl/go-nfqueue v1.3.2-0.20231218173729-f2bdeb033acf h1:NqGS3vTHzVENbIfd87cXZwdpO6MB2R1PjHMJLi4Z3ow=
|
||||
github.com/florianl/go-nfqueue v1.3.2-0.20231218173729-f2bdeb033acf/go.mod h1:eSnAor2YCfMCVYrVNEhkLGN/r1L+J4uDjc0EUy0tfq4=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/gopacket v1.1.20-0.20220810144506-32ee38206866 h1:NaJi58bCZZh0jjPw78EqDZekPEfhlzYE01C5R+zh1tE=
|
||||
github.com/google/gopacket v1.1.20-0.20220810144506-32ee38206866/go.mod h1:riddUzxTSBpJXk3qBHtYr4qOhFhT6k/1c0E3qkQjQpA=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
@@ -28,7 +38,9 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
|
||||
github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk=
|
||||
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mdlayher/netlink v1.6.0 h1:rOHX5yl7qnlpiVkFWoqccueppMtXzeziFjWAjLg6sz0=
|
||||
@@ -37,11 +49,19 @@ github.com/mdlayher/socket v0.1.1 h1:q3uOGirUPfAV2MUoaC7BavjQ154J7+JOkTWyiV+intI
|
||||
github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
|
||||
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
|
||||
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
|
||||
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/quic-go v0.41.0 h1:aD8MmHfgqTURWNJy48IYFg2OnxwHT3JL7ahGs73lb4k=
|
||||
github.com/quic-go/quic-go v0.41.0/go.mod h1:qCkNjqczPEvgsOnxZ0eCD14lv+B2LHlFAB++CNOh9hA=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
@@ -68,13 +88,19 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
|
||||
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
|
||||
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
@@ -91,14 +117,16 @@ golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -108,11 +136,17 @@ golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
156
io/nfqueue.go
156
io/nfqueue.go
@@ -4,7 +4,10 @@ import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/coreos/go-iptables/iptables"
|
||||
"github.com/florianl/go-nfqueue"
|
||||
@@ -18,8 +21,50 @@ const (
|
||||
|
||||
nfqueueConnMarkAccept = 1001
|
||||
nfqueueConnMarkDrop = 1002
|
||||
|
||||
nftFamily = "inet"
|
||||
nftTable = "opengfw"
|
||||
)
|
||||
|
||||
var nftRulesForward = fmt.Sprintf(`
|
||||
define ACCEPT_CTMARK=%d
|
||||
define DROP_CTMARK=%d
|
||||
define QUEUE_NUM=%d
|
||||
|
||||
table %s %s {
|
||||
chain FORWARD {
|
||||
type filter hook forward priority filter; policy accept;
|
||||
|
||||
ct mark $ACCEPT_CTMARK counter accept
|
||||
ct mark $DROP_CTMARK counter drop
|
||||
counter queue num $QUEUE_NUM bypass
|
||||
}
|
||||
}
|
||||
`, nfqueueConnMarkAccept, nfqueueConnMarkDrop, nfqueueNum, nftFamily, nftTable)
|
||||
|
||||
var nftRulesLocal = fmt.Sprintf(`
|
||||
define ACCEPT_CTMARK=%d
|
||||
define DROP_CTMARK=%d
|
||||
define QUEUE_NUM=%d
|
||||
|
||||
table %s %s {
|
||||
chain INPUT {
|
||||
type filter hook input priority filter; policy accept;
|
||||
|
||||
ct mark $ACCEPT_CTMARK counter accept
|
||||
ct mark $DROP_CTMARK counter drop
|
||||
counter queue num $QUEUE_NUM bypass
|
||||
}
|
||||
chain OUTPUT {
|
||||
type filter hook output priority filter; policy accept;
|
||||
|
||||
ct mark $ACCEPT_CTMARK counter accept
|
||||
ct mark $DROP_CTMARK counter drop
|
||||
counter queue num $QUEUE_NUM bypass
|
||||
}
|
||||
}
|
||||
`, nfqueueConnMarkAccept, nfqueueConnMarkDrop, nfqueueNum, nftFamily, nftTable)
|
||||
|
||||
var iptRulesForward = []iptRule{
|
||||
{"filter", "FORWARD", []string{"-m", "connmark", "--mark", strconv.Itoa(nfqueueConnMarkAccept), "-j", "ACCEPT"}},
|
||||
{"filter", "FORWARD", []string{"-m", "connmark", "--mark", strconv.Itoa(nfqueueConnMarkDrop), "-j", "DROP"}},
|
||||
@@ -43,8 +88,11 @@ var errNotNFQueuePacket = errors.New("not an NFQueue packet")
|
||||
type nfqueuePacketIO struct {
|
||||
n *nfqueue.Nfqueue
|
||||
local bool
|
||||
ipt4 *iptables.IPTables
|
||||
ipt6 *iptables.IPTables
|
||||
rSet bool // whether the nftables/iptables rules have been set
|
||||
|
||||
// iptables not nil = use iptables instead of nftables
|
||||
ipt4 *iptables.IPTables
|
||||
ipt6 *iptables.IPTables
|
||||
}
|
||||
|
||||
type NFQueuePacketIOConfig struct {
|
||||
@@ -56,13 +104,18 @@ func NewNFQueuePacketIO(config NFQueuePacketIOConfig) (PacketIO, error) {
|
||||
if config.QueueSize == 0 {
|
||||
config.QueueSize = nfqueueDefaultQueueSize
|
||||
}
|
||||
ipt4, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ipt6, err := iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var ipt4, ipt6 *iptables.IPTables
|
||||
var err error
|
||||
if nftCheck() != nil {
|
||||
// We prefer nftables, but if it's not available, fall back to iptables
|
||||
ipt4, err = iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ipt6, err = iptables.NewWithProtocol(iptables.ProtocolIPv6)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
n, err := nfqueue.Open(&nfqueue.Config{
|
||||
NfQueue: nfqueueNum,
|
||||
@@ -74,22 +127,16 @@ func NewNFQueuePacketIO(config NFQueuePacketIOConfig) (PacketIO, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
io := &nfqueuePacketIO{
|
||||
return &nfqueuePacketIO{
|
||||
n: n,
|
||||
local: config.Local,
|
||||
ipt4: ipt4,
|
||||
ipt6: ipt6,
|
||||
}
|
||||
err = io.setupIpt(config.Local, false)
|
||||
if err != nil {
|
||||
_ = n.Close()
|
||||
return nil, err
|
||||
}
|
||||
return io, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (n *nfqueuePacketIO) Register(ctx context.Context, cb PacketCallback) error {
|
||||
return n.n.RegisterWithErrorFunc(ctx,
|
||||
err := n.n.RegisterWithErrorFunc(ctx,
|
||||
func(a nfqueue.Attribute) int {
|
||||
if a.PacketID == nil || a.Ct == nil || a.Payload == nil || len(*a.Payload) < 20 {
|
||||
// Invalid packet, ignore
|
||||
@@ -106,6 +153,21 @@ func (n *nfqueuePacketIO) Register(ctx context.Context, cb PacketCallback) error
|
||||
func(e error) int {
|
||||
return okBoolToInt(cb(nil, e))
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !n.rSet {
|
||||
if n.ipt4 != nil {
|
||||
err = n.setupIpt(n.local, false)
|
||||
} else {
|
||||
err = n.setupNft(n.local, false)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n.rSet = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *nfqueuePacketIO) SetVerdict(p Packet, v Verdict, newPacket []byte) error {
|
||||
@@ -130,6 +192,39 @@ func (n *nfqueuePacketIO) SetVerdict(p Packet, v Verdict, newPacket []byte) erro
|
||||
}
|
||||
}
|
||||
|
||||
func (n *nfqueuePacketIO) Close() error {
|
||||
if n.rSet {
|
||||
if n.ipt4 != nil {
|
||||
_ = n.setupIpt(n.local, true)
|
||||
} else {
|
||||
_ = n.setupNft(n.local, true)
|
||||
}
|
||||
n.rSet = false
|
||||
}
|
||||
return n.n.Close()
|
||||
}
|
||||
|
||||
func (n *nfqueuePacketIO) setupNft(local, remove bool) error {
|
||||
var rules string
|
||||
if local {
|
||||
rules = nftRulesLocal
|
||||
} else {
|
||||
rules = nftRulesForward
|
||||
}
|
||||
var err error
|
||||
if remove {
|
||||
err = nftDelete(nftFamily, nftTable)
|
||||
} else {
|
||||
// Delete first to make sure no leftover rules
|
||||
_ = nftDelete(nftFamily, nftTable)
|
||||
err = nftAdd(rules)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *nfqueuePacketIO) setupIpt(local, remove bool) error {
|
||||
var rules []iptRule
|
||||
if local {
|
||||
@@ -149,12 +244,6 @@ func (n *nfqueuePacketIO) setupIpt(local, remove bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *nfqueuePacketIO) Close() error {
|
||||
err := n.setupIpt(n.local, true)
|
||||
_ = n.n.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
var _ Packet = (*nfqueuePacket)(nil)
|
||||
|
||||
type nfqueuePacket struct {
|
||||
@@ -179,6 +268,25 @@ func okBoolToInt(ok bool) int {
|
||||
}
|
||||
}
|
||||
|
||||
func nftCheck() error {
|
||||
_, err := exec.LookPath("nft")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func nftAdd(input string) error {
|
||||
cmd := exec.Command("nft", "-f", "-")
|
||||
cmd.Stdin = strings.NewReader(input)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func nftDelete(family, table string) error {
|
||||
cmd := exec.Command("nft", "delete", "table", family, table)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
type iptRule struct {
|
||||
Table, Chain string
|
||||
RuleSpec []string
|
||||
|
||||
18
ruleset/builtins/cidr.go
Normal file
18
ruleset/builtins/cidr.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package builtins
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
func MatchCIDR(ip string, cidr *net.IPNet) bool {
|
||||
ipAddr := net.ParseIP(ip)
|
||||
if ipAddr == nil {
|
||||
return false
|
||||
}
|
||||
return cidr.Contains(ipAddr)
|
||||
}
|
||||
|
||||
func CompileCIDR(cidr string) (*net.IPNet, error) {
|
||||
_, ipNet, err := net.ParseCIDR(cidr)
|
||||
return ipNet, err
|
||||
}
|
||||
128
ruleset/builtins/geo/geo_loader.go
Normal file
128
ruleset/builtins/geo/geo_loader.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package geo
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/apernet/OpenGFW/ruleset/builtins/geo/v2geo"
|
||||
)
|
||||
|
||||
const (
|
||||
geoipFilename = "geoip.dat"
|
||||
geoipURL = "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geoip.dat"
|
||||
geositeFilename = "geosite.dat"
|
||||
geositeURL = "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat"
|
||||
|
||||
geoDefaultUpdateInterval = 7 * 24 * time.Hour // 7 days
|
||||
)
|
||||
|
||||
var _ GeoLoader = (*V2GeoLoader)(nil)
|
||||
|
||||
// V2GeoLoader provides the on-demand GeoIP/MatchGeoSite database
|
||||
// loading functionality required by the ACL engine.
|
||||
// Empty filenames = automatic download from built-in URLs.
|
||||
type V2GeoLoader struct {
|
||||
GeoIPFilename string
|
||||
GeoSiteFilename string
|
||||
UpdateInterval time.Duration
|
||||
|
||||
DownloadFunc func(filename, url string)
|
||||
DownloadErrFunc func(err error)
|
||||
|
||||
geoipMap map[string]*v2geo.GeoIP
|
||||
geositeMap map[string]*v2geo.GeoSite
|
||||
}
|
||||
|
||||
func NewDefaultGeoLoader(geoSiteFilename, geoIpFilename string) *V2GeoLoader {
|
||||
return &V2GeoLoader{
|
||||
GeoIPFilename: geoIpFilename,
|
||||
GeoSiteFilename: geoSiteFilename,
|
||||
DownloadFunc: func(filename, url string) {},
|
||||
DownloadErrFunc: func(err error) {},
|
||||
}
|
||||
}
|
||||
|
||||
func (l *V2GeoLoader) shouldDownload(filename string) bool {
|
||||
info, err := os.Stat(filename)
|
||||
if os.IsNotExist(err) {
|
||||
return true
|
||||
}
|
||||
dt := time.Now().Sub(info.ModTime())
|
||||
if l.UpdateInterval == 0 {
|
||||
return dt > geoDefaultUpdateInterval
|
||||
} else {
|
||||
return dt > l.UpdateInterval
|
||||
}
|
||||
}
|
||||
|
||||
func (l *V2GeoLoader) download(filename, url string) error {
|
||||
l.DownloadFunc(filename, url)
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
l.DownloadErrFunc(err)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
l.DownloadErrFunc(err)
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = io.Copy(f, resp.Body)
|
||||
l.DownloadErrFunc(err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (l *V2GeoLoader) LoadGeoIP() (map[string]*v2geo.GeoIP, error) {
|
||||
if l.geoipMap != nil {
|
||||
return l.geoipMap, nil
|
||||
}
|
||||
autoDL := false
|
||||
filename := l.GeoIPFilename
|
||||
if filename == "" {
|
||||
autoDL = true
|
||||
filename = geoipFilename
|
||||
}
|
||||
if autoDL && l.shouldDownload(filename) {
|
||||
err := l.download(filename, geoipURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
m, err := v2geo.LoadGeoIP(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.geoipMap = m
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (l *V2GeoLoader) LoadGeoSite() (map[string]*v2geo.GeoSite, error) {
|
||||
if l.geositeMap != nil {
|
||||
return l.geositeMap, nil
|
||||
}
|
||||
autoDL := false
|
||||
filename := l.GeoSiteFilename
|
||||
if filename == "" {
|
||||
autoDL = true
|
||||
filename = geositeFilename
|
||||
}
|
||||
if autoDL && l.shouldDownload(filename) {
|
||||
err := l.download(filename, geositeURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
m, err := v2geo.LoadGeoSite(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.geositeMap = m
|
||||
return m, nil
|
||||
}
|
||||
115
ruleset/builtins/geo/geo_matcher.go
Normal file
115
ruleset/builtins/geo/geo_matcher.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package geo
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type GeoMatcher struct {
|
||||
geoLoader GeoLoader
|
||||
geoSiteMatcher map[string]hostMatcher
|
||||
siteMatcherLock sync.Mutex
|
||||
geoIpMatcher map[string]hostMatcher
|
||||
ipMatcherLock sync.Mutex
|
||||
}
|
||||
|
||||
func NewGeoMatcher(geoSiteFilename, geoIpFilename string) (*GeoMatcher, error) {
|
||||
geoLoader := NewDefaultGeoLoader(geoSiteFilename, geoIpFilename)
|
||||
|
||||
return &GeoMatcher{
|
||||
geoLoader: geoLoader,
|
||||
geoSiteMatcher: make(map[string]hostMatcher),
|
||||
geoIpMatcher: make(map[string]hostMatcher),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (g *GeoMatcher) MatchGeoIp(ip, condition string) bool {
|
||||
g.ipMatcherLock.Lock()
|
||||
defer g.ipMatcherLock.Unlock()
|
||||
|
||||
matcher, ok := g.geoIpMatcher[condition]
|
||||
if !ok {
|
||||
// GeoIP matcher
|
||||
condition = strings.ToLower(condition)
|
||||
country := condition
|
||||
if len(country) == 0 {
|
||||
return false
|
||||
}
|
||||
gMap, err := g.geoLoader.LoadGeoIP()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
list, ok := gMap[country]
|
||||
if !ok || list == nil {
|
||||
return false
|
||||
}
|
||||
matcher, err = newGeoIPMatcher(list)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
g.geoIpMatcher[condition] = matcher
|
||||
}
|
||||
parseIp := net.ParseIP(ip)
|
||||
if parseIp == nil {
|
||||
return false
|
||||
}
|
||||
ipv4 := parseIp.To4()
|
||||
if ipv4 != nil {
|
||||
return matcher.Match(HostInfo{IPv4: ipv4})
|
||||
}
|
||||
ipv6 := parseIp.To16()
|
||||
if ipv6 != nil {
|
||||
return matcher.Match(HostInfo{IPv6: ipv6})
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *GeoMatcher) MatchGeoSite(site, condition string) bool {
|
||||
g.siteMatcherLock.Lock()
|
||||
defer g.siteMatcherLock.Unlock()
|
||||
|
||||
matcher, ok := g.geoSiteMatcher[condition]
|
||||
if !ok {
|
||||
// MatchGeoSite matcher
|
||||
condition = strings.ToLower(condition)
|
||||
name, attrs := parseGeoSiteName(condition)
|
||||
if len(name) == 0 {
|
||||
return false
|
||||
}
|
||||
gMap, err := g.geoLoader.LoadGeoSite()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
list, ok := gMap[name]
|
||||
if !ok || list == nil {
|
||||
return false
|
||||
}
|
||||
matcher, err = newGeositeMatcher(list, attrs)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
g.geoSiteMatcher[condition] = matcher
|
||||
}
|
||||
return matcher.Match(HostInfo{Name: site})
|
||||
}
|
||||
|
||||
func (g *GeoMatcher) LoadGeoSite() error {
|
||||
_, err := g.geoLoader.LoadGeoSite()
|
||||
return err
|
||||
}
|
||||
|
||||
func (g *GeoMatcher) LoadGeoIP() error {
|
||||
_, err := g.geoLoader.LoadGeoIP()
|
||||
return err
|
||||
}
|
||||
|
||||
func parseGeoSiteName(s string) (string, []string) {
|
||||
parts := strings.Split(s, "@")
|
||||
base := strings.TrimSpace(parts[0])
|
||||
attrs := parts[1:]
|
||||
for i := range attrs {
|
||||
attrs[i] = strings.TrimSpace(attrs[i])
|
||||
}
|
||||
return base, attrs
|
||||
}
|
||||
27
ruleset/builtins/geo/interface.go
Normal file
27
ruleset/builtins/geo/interface.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package geo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/apernet/OpenGFW/ruleset/builtins/geo/v2geo"
|
||||
)
|
||||
|
||||
type HostInfo struct {
|
||||
Name string
|
||||
IPv4 net.IP
|
||||
IPv6 net.IP
|
||||
}
|
||||
|
||||
func (h HostInfo) String() string {
|
||||
return fmt.Sprintf("%s|%s|%s", h.Name, h.IPv4, h.IPv6)
|
||||
}
|
||||
|
||||
type GeoLoader interface {
|
||||
LoadGeoIP() (map[string]*v2geo.GeoIP, error)
|
||||
LoadGeoSite() (map[string]*v2geo.GeoSite, error)
|
||||
}
|
||||
|
||||
type hostMatcher interface {
|
||||
Match(HostInfo) bool
|
||||
}
|
||||
213
ruleset/builtins/geo/matchers_v2geo.go
Normal file
213
ruleset/builtins/geo/matchers_v2geo.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package geo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/apernet/OpenGFW/ruleset/builtins/geo/v2geo"
|
||||
)
|
||||
|
||||
var _ hostMatcher = (*geoipMatcher)(nil)
|
||||
|
||||
type geoipMatcher struct {
|
||||
N4 []*net.IPNet // sorted
|
||||
N6 []*net.IPNet // sorted
|
||||
Inverse bool
|
||||
}
|
||||
|
||||
// matchIP tries to match the given IP address with the corresponding IPNets.
|
||||
// Note that this function does NOT handle the Inverse flag.
|
||||
func (m *geoipMatcher) matchIP(ip net.IP) bool {
|
||||
var n []*net.IPNet
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
// N4 stores IPv4 addresses in 4-byte form.
|
||||
// Make sure we use it here too, otherwise bytes.Compare will fail.
|
||||
ip = ip4
|
||||
n = m.N4
|
||||
} else {
|
||||
n = m.N6
|
||||
}
|
||||
left, right := 0, len(n)-1
|
||||
for left <= right {
|
||||
mid := (left + right) / 2
|
||||
if n[mid].Contains(ip) {
|
||||
return true
|
||||
} else if bytes.Compare(n[mid].IP, ip) < 0 {
|
||||
left = mid + 1
|
||||
} else {
|
||||
right = mid - 1
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *geoipMatcher) Match(host HostInfo) bool {
|
||||
if host.IPv4 != nil {
|
||||
if m.matchIP(host.IPv4) {
|
||||
return !m.Inverse
|
||||
}
|
||||
}
|
||||
if host.IPv6 != nil {
|
||||
if m.matchIP(host.IPv6) {
|
||||
return !m.Inverse
|
||||
}
|
||||
}
|
||||
return m.Inverse
|
||||
}
|
||||
|
||||
func newGeoIPMatcher(list *v2geo.GeoIP) (*geoipMatcher, error) {
|
||||
n4 := make([]*net.IPNet, 0)
|
||||
n6 := make([]*net.IPNet, 0)
|
||||
for _, cidr := range list.Cidr {
|
||||
if len(cidr.Ip) == 4 {
|
||||
// IPv4
|
||||
n4 = append(n4, &net.IPNet{
|
||||
IP: cidr.Ip,
|
||||
Mask: net.CIDRMask(int(cidr.Prefix), 32),
|
||||
})
|
||||
} else if len(cidr.Ip) == 16 {
|
||||
// IPv6
|
||||
n6 = append(n6, &net.IPNet{
|
||||
IP: cidr.Ip,
|
||||
Mask: net.CIDRMask(int(cidr.Prefix), 128),
|
||||
})
|
||||
} else {
|
||||
return nil, errors.New("invalid IP length")
|
||||
}
|
||||
}
|
||||
// Sort the IPNets, so we can do binary search later.
|
||||
sort.Slice(n4, func(i, j int) bool {
|
||||
return bytes.Compare(n4[i].IP, n4[j].IP) < 0
|
||||
})
|
||||
sort.Slice(n6, func(i, j int) bool {
|
||||
return bytes.Compare(n6[i].IP, n6[j].IP) < 0
|
||||
})
|
||||
return &geoipMatcher{
|
||||
N4: n4,
|
||||
N6: n6,
|
||||
Inverse: list.InverseMatch,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ hostMatcher = (*geositeMatcher)(nil)
|
||||
|
||||
type geositeDomainType int
|
||||
|
||||
const (
|
||||
geositeDomainPlain geositeDomainType = iota
|
||||
geositeDomainRegex
|
||||
geositeDomainRoot
|
||||
geositeDomainFull
|
||||
)
|
||||
|
||||
type geositeDomain struct {
|
||||
Type geositeDomainType
|
||||
Value string
|
||||
Regex *regexp.Regexp
|
||||
Attrs map[string]bool
|
||||
}
|
||||
|
||||
type geositeMatcher struct {
|
||||
Domains []geositeDomain
|
||||
// Attributes are matched using "and" logic - if you have multiple attributes here,
|
||||
// a domain must have all of those attributes to be considered a match.
|
||||
Attrs []string
|
||||
}
|
||||
|
||||
func (m *geositeMatcher) matchDomain(domain geositeDomain, host HostInfo) bool {
|
||||
// Match attributes first
|
||||
if len(m.Attrs) > 0 {
|
||||
if len(domain.Attrs) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, attr := range m.Attrs {
|
||||
if !domain.Attrs[attr] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch domain.Type {
|
||||
case geositeDomainPlain:
|
||||
return strings.Contains(host.Name, domain.Value)
|
||||
case geositeDomainRegex:
|
||||
if domain.Regex != nil {
|
||||
return domain.Regex.MatchString(host.Name)
|
||||
}
|
||||
case geositeDomainFull:
|
||||
return host.Name == domain.Value
|
||||
case geositeDomainRoot:
|
||||
if host.Name == domain.Value {
|
||||
return true
|
||||
}
|
||||
return strings.HasSuffix(host.Name, "."+domain.Value)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *geositeMatcher) Match(host HostInfo) bool {
|
||||
for _, domain := range m.Domains {
|
||||
if m.matchDomain(domain, host) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func newGeositeMatcher(list *v2geo.GeoSite, attrs []string) (*geositeMatcher, error) {
|
||||
domains := make([]geositeDomain, len(list.Domain))
|
||||
for i, domain := range list.Domain {
|
||||
switch domain.Type {
|
||||
case v2geo.Domain_Plain:
|
||||
domains[i] = geositeDomain{
|
||||
Type: geositeDomainPlain,
|
||||
Value: domain.Value,
|
||||
Attrs: domainAttributeToMap(domain.Attribute),
|
||||
}
|
||||
case v2geo.Domain_Regex:
|
||||
regex, err := regexp.Compile(domain.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
domains[i] = geositeDomain{
|
||||
Type: geositeDomainRegex,
|
||||
Regex: regex,
|
||||
Attrs: domainAttributeToMap(domain.Attribute),
|
||||
}
|
||||
case v2geo.Domain_Full:
|
||||
domains[i] = geositeDomain{
|
||||
Type: geositeDomainFull,
|
||||
Value: domain.Value,
|
||||
Attrs: domainAttributeToMap(domain.Attribute),
|
||||
}
|
||||
case v2geo.Domain_RootDomain:
|
||||
domains[i] = geositeDomain{
|
||||
Type: geositeDomainRoot,
|
||||
Value: domain.Value,
|
||||
Attrs: domainAttributeToMap(domain.Attribute),
|
||||
}
|
||||
default:
|
||||
return nil, errors.New("unsupported domain type")
|
||||
}
|
||||
}
|
||||
return &geositeMatcher{
|
||||
Domains: domains,
|
||||
Attrs: attrs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func domainAttributeToMap(attrs []*v2geo.Domain_Attribute) map[string]bool {
|
||||
m := make(map[string]bool)
|
||||
for _, attr := range attrs {
|
||||
// Supposedly there are also int attributes,
|
||||
// but nobody seems to use them, so we treat everything as boolean for now.
|
||||
m[attr.Key] = true
|
||||
}
|
||||
return m
|
||||
}
|
||||
44
ruleset/builtins/geo/v2geo/load.go
Normal file
44
ruleset/builtins/geo/v2geo/load.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package v2geo
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// LoadGeoIP loads a GeoIP data file and converts it to a map.
|
||||
// The keys of the map (country codes) are all normalized to lowercase.
|
||||
func LoadGeoIP(filename string) (map[string]*GeoIP, error) {
|
||||
bs, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var list GeoIPList
|
||||
if err := proto.Unmarshal(bs, &list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]*GeoIP)
|
||||
for _, entry := range list.Entry {
|
||||
m[strings.ToLower(entry.CountryCode)] = entry
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// LoadGeoSite loads a GeoSite data file and converts it to a map.
|
||||
// The keys of the map (site keys) are all normalized to lowercase.
|
||||
func LoadGeoSite(filename string) (map[string]*GeoSite, error) {
|
||||
bs, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var list GeoSiteList
|
||||
if err := proto.Unmarshal(bs, &list); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[string]*GeoSite)
|
||||
for _, entry := range list.Entry {
|
||||
m[strings.ToLower(entry.CountryCode)] = entry
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
54
ruleset/builtins/geo/v2geo/load_test.go
Normal file
54
ruleset/builtins/geo/v2geo/load_test.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package v2geo
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLoadGeoIP(t *testing.T) {
|
||||
m, err := LoadGeoIP("geoip.dat")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Exact checks since we know the data.
|
||||
assert.Len(t, m, 252)
|
||||
assert.Equal(t, m["cn"].CountryCode, "CN")
|
||||
assert.Len(t, m["cn"].Cidr, 10407)
|
||||
assert.Equal(t, m["us"].CountryCode, "US")
|
||||
assert.Len(t, m["us"].Cidr, 193171)
|
||||
assert.Equal(t, m["private"].CountryCode, "PRIVATE")
|
||||
assert.Len(t, m["private"].Cidr, 18)
|
||||
assert.Contains(t, m["private"].Cidr, &CIDR{
|
||||
Ip: []byte("\xc0\xa8\x00\x00"),
|
||||
Prefix: 16,
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadGeoSite(t *testing.T) {
|
||||
m, err := LoadGeoSite("geosite.dat")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Exact checks since we know the data.
|
||||
assert.Len(t, m, 1204)
|
||||
assert.Equal(t, m["netflix"].CountryCode, "NETFLIX")
|
||||
assert.Len(t, m["netflix"].Domain, 25)
|
||||
assert.Contains(t, m["netflix"].Domain, &Domain{
|
||||
Type: Domain_Full,
|
||||
Value: "netflix.com.edgesuite.net",
|
||||
})
|
||||
assert.Contains(t, m["netflix"].Domain, &Domain{
|
||||
Type: Domain_RootDomain,
|
||||
Value: "fast.com",
|
||||
})
|
||||
assert.Len(t, m["google"].Domain, 1066)
|
||||
assert.Contains(t, m["google"].Domain, &Domain{
|
||||
Type: Domain_RootDomain,
|
||||
Value: "ggpht.cn",
|
||||
Attribute: []*Domain_Attribute{
|
||||
{
|
||||
Key: "cn",
|
||||
TypedValue: &Domain_Attribute_BoolValue{BoolValue: true},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
745
ruleset/builtins/geo/v2geo/v2geo.pb.go
Normal file
745
ruleset/builtins/geo/v2geo/v2geo.pb.go
Normal file
@@ -0,0 +1,745 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.31.0
|
||||
// protoc v4.24.4
|
||||
// source: v2geo.proto
|
||||
|
||||
package v2geo
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
// Type of domain value.
|
||||
type Domain_Type int32
|
||||
|
||||
const (
|
||||
// The value is used as is.
|
||||
Domain_Plain Domain_Type = 0
|
||||
// The value is used as a regular expression.
|
||||
Domain_Regex Domain_Type = 1
|
||||
// The value is a root domain.
|
||||
Domain_RootDomain Domain_Type = 2
|
||||
// The value is a domain.
|
||||
Domain_Full Domain_Type = 3
|
||||
)
|
||||
|
||||
// Enum value maps for Domain_Type.
|
||||
var (
|
||||
Domain_Type_name = map[int32]string{
|
||||
0: "Plain",
|
||||
1: "Regex",
|
||||
2: "RootDomain",
|
||||
3: "Full",
|
||||
}
|
||||
Domain_Type_value = map[string]int32{
|
||||
"Plain": 0,
|
||||
"Regex": 1,
|
||||
"RootDomain": 2,
|
||||
"Full": 3,
|
||||
}
|
||||
)
|
||||
|
||||
func (x Domain_Type) Enum() *Domain_Type {
|
||||
p := new(Domain_Type)
|
||||
*p = x
|
||||
return p
|
||||
}
|
||||
|
||||
func (x Domain_Type) String() string {
|
||||
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||
}
|
||||
|
||||
func (Domain_Type) Descriptor() protoreflect.EnumDescriptor {
|
||||
return file_v2geo_proto_enumTypes[0].Descriptor()
|
||||
}
|
||||
|
||||
func (Domain_Type) Type() protoreflect.EnumType {
|
||||
return &file_v2geo_proto_enumTypes[0]
|
||||
}
|
||||
|
||||
func (x Domain_Type) Number() protoreflect.EnumNumber {
|
||||
return protoreflect.EnumNumber(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Domain_Type.Descriptor instead.
|
||||
func (Domain_Type) EnumDescriptor() ([]byte, []int) {
|
||||
return file_v2geo_proto_rawDescGZIP(), []int{0, 0}
|
||||
}
|
||||
|
||||
// Domain for routing decision.
|
||||
type Domain struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// Domain matching type.
|
||||
Type Domain_Type `protobuf:"varint,1,opt,name=type,proto3,enum=Domain_Type" json:"type,omitempty"`
|
||||
// Domain value.
|
||||
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
|
||||
// Attributes of this domain. May be used for filtering.
|
||||
Attribute []*Domain_Attribute `protobuf:"bytes,3,rep,name=attribute,proto3" json:"attribute,omitempty"`
|
||||
}
|
||||
|
||||
func (x *Domain) Reset() {
|
||||
*x = Domain{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_v2geo_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *Domain) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Domain) ProtoMessage() {}
|
||||
|
||||
func (x *Domain) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v2geo_proto_msgTypes[0]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Domain.ProtoReflect.Descriptor instead.
|
||||
func (*Domain) Descriptor() ([]byte, []int) {
|
||||
return file_v2geo_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *Domain) GetType() Domain_Type {
|
||||
if x != nil {
|
||||
return x.Type
|
||||
}
|
||||
return Domain_Plain
|
||||
}
|
||||
|
||||
func (x *Domain) GetValue() string {
|
||||
if x != nil {
|
||||
return x.Value
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Domain) GetAttribute() []*Domain_Attribute {
|
||||
if x != nil {
|
||||
return x.Attribute
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IP for routing decision, in CIDR form.
|
||||
type CIDR struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
// IP address, should be either 4 or 16 bytes.
|
||||
Ip []byte `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"`
|
||||
// Number of leading ones in the network mask.
|
||||
Prefix uint32 `protobuf:"varint,2,opt,name=prefix,proto3" json:"prefix,omitempty"`
|
||||
}
|
||||
|
||||
func (x *CIDR) Reset() {
|
||||
*x = CIDR{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_v2geo_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *CIDR) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*CIDR) ProtoMessage() {}
|
||||
|
||||
func (x *CIDR) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v2geo_proto_msgTypes[1]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use CIDR.ProtoReflect.Descriptor instead.
|
||||
func (*CIDR) Descriptor() ([]byte, []int) {
|
||||
return file_v2geo_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *CIDR) GetIp() []byte {
|
||||
if x != nil {
|
||||
return x.Ip
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *CIDR) GetPrefix() uint32 {
|
||||
if x != nil {
|
||||
return x.Prefix
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type GeoIP struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
CountryCode string `protobuf:"bytes,1,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"`
|
||||
Cidr []*CIDR `protobuf:"bytes,2,rep,name=cidr,proto3" json:"cidr,omitempty"`
|
||||
InverseMatch bool `protobuf:"varint,3,opt,name=inverse_match,json=inverseMatch,proto3" json:"inverse_match,omitempty"`
|
||||
// resource_hash instruct simplified config converter to load domain from geo file.
|
||||
ResourceHash []byte `protobuf:"bytes,4,opt,name=resource_hash,json=resourceHash,proto3" json:"resource_hash,omitempty"`
|
||||
Code string `protobuf:"bytes,5,opt,name=code,proto3" json:"code,omitempty"`
|
||||
}
|
||||
|
||||
func (x *GeoIP) Reset() {
|
||||
*x = GeoIP{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_v2geo_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *GeoIP) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GeoIP) ProtoMessage() {}
|
||||
|
||||
func (x *GeoIP) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v2geo_proto_msgTypes[2]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GeoIP.ProtoReflect.Descriptor instead.
|
||||
func (*GeoIP) Descriptor() ([]byte, []int) {
|
||||
return file_v2geo_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *GeoIP) GetCountryCode() string {
|
||||
if x != nil {
|
||||
return x.CountryCode
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GeoIP) GetCidr() []*CIDR {
|
||||
if x != nil {
|
||||
return x.Cidr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GeoIP) GetInverseMatch() bool {
|
||||
if x != nil {
|
||||
return x.InverseMatch
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *GeoIP) GetResourceHash() []byte {
|
||||
if x != nil {
|
||||
return x.ResourceHash
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GeoIP) GetCode() string {
|
||||
if x != nil {
|
||||
return x.Code
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type GeoIPList struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Entry []*GeoIP `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"`
|
||||
}
|
||||
|
||||
func (x *GeoIPList) Reset() {
|
||||
*x = GeoIPList{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_v2geo_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *GeoIPList) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GeoIPList) ProtoMessage() {}
|
||||
|
||||
func (x *GeoIPList) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v2geo_proto_msgTypes[3]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GeoIPList.ProtoReflect.Descriptor instead.
|
||||
func (*GeoIPList) Descriptor() ([]byte, []int) {
|
||||
return file_v2geo_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *GeoIPList) GetEntry() []*GeoIP {
|
||||
if x != nil {
|
||||
return x.Entry
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type GeoSite struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
CountryCode string `protobuf:"bytes,1,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"`
|
||||
Domain []*Domain `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"`
|
||||
// resource_hash instruct simplified config converter to load domain from geo file.
|
||||
ResourceHash []byte `protobuf:"bytes,3,opt,name=resource_hash,json=resourceHash,proto3" json:"resource_hash,omitempty"`
|
||||
Code string `protobuf:"bytes,4,opt,name=code,proto3" json:"code,omitempty"`
|
||||
}
|
||||
|
||||
func (x *GeoSite) Reset() {
|
||||
*x = GeoSite{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_v2geo_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *GeoSite) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GeoSite) ProtoMessage() {}
|
||||
|
||||
func (x *GeoSite) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v2geo_proto_msgTypes[4]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use MatchGeoSite.ProtoReflect.Descriptor instead.
|
||||
func (*GeoSite) Descriptor() ([]byte, []int) {
|
||||
return file_v2geo_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *GeoSite) GetCountryCode() string {
|
||||
if x != nil {
|
||||
return x.CountryCode
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *GeoSite) GetDomain() []*Domain {
|
||||
if x != nil {
|
||||
return x.Domain
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GeoSite) GetResourceHash() []byte {
|
||||
if x != nil {
|
||||
return x.ResourceHash
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *GeoSite) GetCode() string {
|
||||
if x != nil {
|
||||
return x.Code
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type GeoSiteList struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Entry []*GeoSite `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"`
|
||||
}
|
||||
|
||||
func (x *GeoSiteList) Reset() {
|
||||
*x = GeoSiteList{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_v2geo_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *GeoSiteList) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GeoSiteList) ProtoMessage() {}
|
||||
|
||||
func (x *GeoSiteList) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v2geo_proto_msgTypes[5]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GeoSiteList.ProtoReflect.Descriptor instead.
|
||||
func (*GeoSiteList) Descriptor() ([]byte, []int) {
|
||||
return file_v2geo_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *GeoSiteList) GetEntry() []*GeoSite {
|
||||
if x != nil {
|
||||
return x.Entry
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Domain_Attribute struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
|
||||
// Types that are assignable to TypedValue:
|
||||
//
|
||||
// *Domain_Attribute_BoolValue
|
||||
// *Domain_Attribute_IntValue
|
||||
TypedValue isDomain_Attribute_TypedValue `protobuf_oneof:"typed_value"`
|
||||
}
|
||||
|
||||
func (x *Domain_Attribute) Reset() {
|
||||
*x = Domain_Attribute{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_v2geo_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *Domain_Attribute) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Domain_Attribute) ProtoMessage() {}
|
||||
|
||||
func (x *Domain_Attribute) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_v2geo_proto_msgTypes[6]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Domain_Attribute.ProtoReflect.Descriptor instead.
|
||||
func (*Domain_Attribute) Descriptor() ([]byte, []int) {
|
||||
return file_v2geo_proto_rawDescGZIP(), []int{0, 0}
|
||||
}
|
||||
|
||||
func (x *Domain_Attribute) GetKey() string {
|
||||
if x != nil {
|
||||
return x.Key
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Domain_Attribute) GetTypedValue() isDomain_Attribute_TypedValue {
|
||||
if m != nil {
|
||||
return m.TypedValue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Domain_Attribute) GetBoolValue() bool {
|
||||
if x, ok := x.GetTypedValue().(*Domain_Attribute_BoolValue); ok {
|
||||
return x.BoolValue
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *Domain_Attribute) GetIntValue() int64 {
|
||||
if x, ok := x.GetTypedValue().(*Domain_Attribute_IntValue); ok {
|
||||
return x.IntValue
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type isDomain_Attribute_TypedValue interface {
|
||||
isDomain_Attribute_TypedValue()
|
||||
}
|
||||
|
||||
type Domain_Attribute_BoolValue struct {
|
||||
BoolValue bool `protobuf:"varint,2,opt,name=bool_value,json=boolValue,proto3,oneof"`
|
||||
}
|
||||
|
||||
type Domain_Attribute_IntValue struct {
|
||||
IntValue int64 `protobuf:"varint,3,opt,name=int_value,json=intValue,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*Domain_Attribute_BoolValue) isDomain_Attribute_TypedValue() {}
|
||||
|
||||
func (*Domain_Attribute_IntValue) isDomain_Attribute_TypedValue() {}
|
||||
|
||||
var File_v2geo_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_v2geo_proto_rawDesc = []byte{
|
||||
0x0a, 0x0b, 0x76, 0x32, 0x67, 0x65, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x97, 0x02,
|
||||
0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x20, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0c, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2e,
|
||||
0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61,
|
||||
0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
|
||||
0x12, 0x2f, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x03, 0x20,
|
||||
0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x41, 0x74, 0x74,
|
||||
0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74,
|
||||
0x65, 0x1a, 0x6c, 0x0a, 0x09, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x10,
|
||||
0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79,
|
||||
0x12, 0x1f, 0x0a, 0x0a, 0x62, 0x6f, 0x6f, 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02,
|
||||
0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x09, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75,
|
||||
0x65, 0x12, 0x1d, 0x0a, 0x09, 0x69, 0x6e, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03,
|
||||
0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65,
|
||||
0x42, 0x0d, 0x0a, 0x0b, 0x74, 0x79, 0x70, 0x65, 0x64, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22,
|
||||
0x36, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x6c, 0x61, 0x69, 0x6e,
|
||||
0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x65, 0x67, 0x65, 0x78, 0x10, 0x01, 0x12, 0x0e, 0x0a,
|
||||
0x0a, 0x52, 0x6f, 0x6f, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x10, 0x02, 0x12, 0x08, 0x0a,
|
||||
0x04, 0x46, 0x75, 0x6c, 0x6c, 0x10, 0x03, 0x22, 0x2e, 0x0a, 0x04, 0x43, 0x49, 0x44, 0x52, 0x12,
|
||||
0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70, 0x12,
|
||||
0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52,
|
||||
0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x22, 0xa3, 0x01, 0x0a, 0x05, 0x47, 0x65, 0x6f, 0x49,
|
||||
0x50, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x64,
|
||||
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79,
|
||||
0x43, 0x6f, 0x64, 0x65, 0x12, 0x19, 0x0a, 0x04, 0x63, 0x69, 0x64, 0x72, 0x18, 0x02, 0x20, 0x03,
|
||||
0x28, 0x0b, 0x32, 0x05, 0x2e, 0x43, 0x49, 0x44, 0x52, 0x52, 0x04, 0x63, 0x69, 0x64, 0x72, 0x12,
|
||||
0x23, 0x0a, 0x0d, 0x69, 0x6e, 0x76, 0x65, 0x72, 0x73, 0x65, 0x5f, 0x6d, 0x61, 0x74, 0x63, 0x68,
|
||||
0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x69, 0x6e, 0x76, 0x65, 0x72, 0x73, 0x65, 0x4d,
|
||||
0x61, 0x74, 0x63, 0x68, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
|
||||
0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x72, 0x65, 0x73,
|
||||
0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64,
|
||||
0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x29, 0x0a,
|
||||
0x09, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x05, 0x65, 0x6e,
|
||||
0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x06, 0x2e, 0x47, 0x65, 0x6f, 0x49,
|
||||
0x50, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x22, 0x86, 0x01, 0x0a, 0x07, 0x47, 0x65, 0x6f,
|
||||
0x53, 0x69, 0x74, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f,
|
||||
0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e,
|
||||
0x74, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69,
|
||||
0x6e, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
|
||||
0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f,
|
||||
0x75, 0x72, 0x63, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52,
|
||||
0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a,
|
||||
0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64,
|
||||
0x65, 0x22, 0x2d, 0x0a, 0x0b, 0x47, 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x4c, 0x69, 0x73, 0x74,
|
||||
0x12, 0x1e, 0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32,
|
||||
0x08, 0x2e, 0x47, 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79,
|
||||
0x42, 0x09, 0x5a, 0x07, 0x2e, 0x2f, 0x76, 0x32, 0x67, 0x65, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_v2geo_proto_rawDescOnce sync.Once
|
||||
file_v2geo_proto_rawDescData = file_v2geo_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_v2geo_proto_rawDescGZIP() []byte {
|
||||
file_v2geo_proto_rawDescOnce.Do(func() {
|
||||
file_v2geo_proto_rawDescData = protoimpl.X.CompressGZIP(file_v2geo_proto_rawDescData)
|
||||
})
|
||||
return file_v2geo_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_v2geo_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||
var file_v2geo_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
|
||||
var file_v2geo_proto_goTypes = []interface{}{
|
||||
(Domain_Type)(0), // 0: Domain.Type
|
||||
(*Domain)(nil), // 1: Domain
|
||||
(*CIDR)(nil), // 2: CIDR
|
||||
(*GeoIP)(nil), // 3: GeoIP
|
||||
(*GeoIPList)(nil), // 4: GeoIPList
|
||||
(*GeoSite)(nil), // 5: MatchGeoSite
|
||||
(*GeoSiteList)(nil), // 6: GeoSiteList
|
||||
(*Domain_Attribute)(nil), // 7: Domain.Attribute
|
||||
}
|
||||
var file_v2geo_proto_depIdxs = []int32{
|
||||
0, // 0: Domain.type:type_name -> Domain.Type
|
||||
7, // 1: Domain.attribute:type_name -> Domain.Attribute
|
||||
2, // 2: GeoIP.cidr:type_name -> CIDR
|
||||
3, // 3: GeoIPList.entry:type_name -> GeoIP
|
||||
1, // 4: MatchGeoSite.domain:type_name -> Domain
|
||||
5, // 5: GeoSiteList.entry:type_name -> MatchGeoSite
|
||||
6, // [6:6] is the sub-list for method output_type
|
||||
6, // [6:6] is the sub-list for method input_type
|
||||
6, // [6:6] is the sub-list for extension type_name
|
||||
6, // [6:6] is the sub-list for extension extendee
|
||||
0, // [0:6] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_v2geo_proto_init() }
|
||||
func file_v2geo_proto_init() {
|
||||
if File_v2geo_proto != nil {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_v2geo_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*Domain); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_v2geo_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*CIDR); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_v2geo_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*GeoIP); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_v2geo_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*GeoIPList); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_v2geo_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*GeoSite); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_v2geo_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*GeoSiteList); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_v2geo_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*Domain_Attribute); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
file_v2geo_proto_msgTypes[6].OneofWrappers = []interface{}{
|
||||
(*Domain_Attribute_BoolValue)(nil),
|
||||
(*Domain_Attribute_IntValue)(nil),
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_v2geo_proto_rawDesc,
|
||||
NumEnums: 1,
|
||||
NumMessages: 7,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_v2geo_proto_goTypes,
|
||||
DependencyIndexes: file_v2geo_proto_depIdxs,
|
||||
EnumInfos: file_v2geo_proto_enumTypes,
|
||||
MessageInfos: file_v2geo_proto_msgTypes,
|
||||
}.Build()
|
||||
File_v2geo_proto = out.File
|
||||
file_v2geo_proto_rawDesc = nil
|
||||
file_v2geo_proto_goTypes = nil
|
||||
file_v2geo_proto_depIdxs = nil
|
||||
}
|
||||
76
ruleset/builtins/geo/v2geo/v2geo.proto
Normal file
76
ruleset/builtins/geo/v2geo/v2geo.proto
Normal file
@@ -0,0 +1,76 @@
|
||||
syntax = "proto3";
|
||||
|
||||
option go_package = "./v2geo";
|
||||
|
||||
// This file is copied from
|
||||
// https://github.com/v2fly/v2ray-core/blob/master/app/router/routercommon/common.proto
|
||||
// with some modifications.
|
||||
|
||||
// Domain for routing decision.
|
||||
message Domain {
|
||||
// Type of domain value.
|
||||
enum Type {
|
||||
// The value is used as is.
|
||||
Plain = 0;
|
||||
// The value is used as a regular expression.
|
||||
Regex = 1;
|
||||
// The value is a root domain.
|
||||
RootDomain = 2;
|
||||
// The value is a domain.
|
||||
Full = 3;
|
||||
}
|
||||
|
||||
// Domain matching type.
|
||||
Type type = 1;
|
||||
|
||||
// Domain value.
|
||||
string value = 2;
|
||||
|
||||
message Attribute {
|
||||
string key = 1;
|
||||
|
||||
oneof typed_value {
|
||||
bool bool_value = 2;
|
||||
int64 int_value = 3;
|
||||
}
|
||||
}
|
||||
|
||||
// Attributes of this domain. May be used for filtering.
|
||||
repeated Attribute attribute = 3;
|
||||
}
|
||||
|
||||
// IP for routing decision, in CIDR form.
|
||||
message CIDR {
|
||||
// IP address, should be either 4 or 16 bytes.
|
||||
bytes ip = 1;
|
||||
|
||||
// Number of leading ones in the network mask.
|
||||
uint32 prefix = 2;
|
||||
}
|
||||
|
||||
message GeoIP {
|
||||
string country_code = 1;
|
||||
repeated CIDR cidr = 2;
|
||||
bool inverse_match = 3;
|
||||
|
||||
// resource_hash instruct simplified config converter to load domain from geo file.
|
||||
bytes resource_hash = 4;
|
||||
string code = 5;
|
||||
}
|
||||
|
||||
message GeoIPList {
|
||||
repeated GeoIP entry = 1;
|
||||
}
|
||||
|
||||
message GeoSite {
|
||||
string country_code = 1;
|
||||
repeated Domain domain = 2;
|
||||
|
||||
// resource_hash instruct simplified config converter to load domain from geo file.
|
||||
bytes resource_hash = 3;
|
||||
string code = 4;
|
||||
}
|
||||
|
||||
message GeoSiteList {
|
||||
repeated GeoSite entry = 1;
|
||||
}
|
||||
124
ruleset/expr.go
124
ruleset/expr.go
@@ -2,6 +2,7 @@ package ruleset
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
@@ -14,6 +15,8 @@ import (
|
||||
|
||||
"github.com/apernet/OpenGFW/analyzer"
|
||||
"github.com/apernet/OpenGFW/modifier"
|
||||
"github.com/apernet/OpenGFW/ruleset/builtins"
|
||||
"github.com/apernet/OpenGFW/ruleset/builtins/geo"
|
||||
)
|
||||
|
||||
// ExprRule is the external representation of an expression rule.
|
||||
@@ -45,14 +48,14 @@ type compiledExprRule struct {
|
||||
Action Action
|
||||
ModInstance modifier.Instance
|
||||
Program *vm.Program
|
||||
Analyzers map[string]struct{}
|
||||
}
|
||||
|
||||
var _ Ruleset = (*exprRuleset)(nil)
|
||||
|
||||
type exprRuleset struct {
|
||||
Rules []compiledExprRule
|
||||
Ans []analyzer.Analyzer
|
||||
Rules []compiledExprRule
|
||||
Ans []analyzer.Analyzer
|
||||
GeoMatcher *geo.GeoMatcher
|
||||
}
|
||||
|
||||
func (r *exprRuleset) Analyzers(info StreamInfo) []analyzer.Analyzer {
|
||||
@@ -83,40 +86,66 @@ func (r *exprRuleset) Match(info StreamInfo) (MatchResult, error) {
|
||||
// CompileExprRules compiles a list of expression rules into a ruleset.
|
||||
// It returns an error if any of the rules are invalid, or if any of the analyzers
|
||||
// used by the rules are unknown (not provided in the analyzer list).
|
||||
func CompileExprRules(rules []ExprRule, ans []analyzer.Analyzer, mods []modifier.Modifier) (Ruleset, error) {
|
||||
func CompileExprRules(rules []ExprRule, ans []analyzer.Analyzer, mods []modifier.Modifier, config *BuiltinConfig) (Ruleset, error) {
|
||||
var compiledRules []compiledExprRule
|
||||
fullAnMap := analyzersToMap(ans)
|
||||
fullModMap := modifiersToMap(mods)
|
||||
depAnMap := make(map[string]analyzer.Analyzer)
|
||||
geoMatcher, err := geo.NewGeoMatcher(config.GeoSiteFilename, config.GeoIpFilename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Compile all rules and build a map of analyzers that are used by the rules.
|
||||
for _, rule := range rules {
|
||||
action, ok := actionStringToAction(rule.Action)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("rule %q has invalid action %q", rule.Name, rule.Action)
|
||||
}
|
||||
visitor := &depVisitor{Analyzers: make(map[string]struct{})}
|
||||
visitor := &idVisitor{Identifiers: make(map[string]bool)}
|
||||
patcher := &idPatcher{}
|
||||
program, err := expr.Compile(rule.Expr,
|
||||
func(c *conf.Config) {
|
||||
c.Strict = false
|
||||
c.Expect = reflect.Bool
|
||||
c.Visitors = append(c.Visitors, visitor)
|
||||
c.Visitors = append(c.Visitors, visitor, patcher)
|
||||
registerBuiltinFunctions(c.Functions, geoMatcher)
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rule %q has invalid expression: %w", rule.Name, err)
|
||||
}
|
||||
for name := range visitor.Analyzers {
|
||||
a, ok := fullAnMap[name]
|
||||
if !ok && !isBuiltInAnalyzer(name) {
|
||||
return nil, fmt.Errorf("rule %q uses unknown analyzer %q", rule.Name, name)
|
||||
if patcher.Err != nil {
|
||||
return nil, fmt.Errorf("rule %q failed to patch expression: %w", rule.Name, patcher.Err)
|
||||
}
|
||||
for name := range visitor.Identifiers {
|
||||
if isBuiltInAnalyzer(name) {
|
||||
continue
|
||||
}
|
||||
// Check if it's one of the built-in functions, and if so,
|
||||
// skip it as an analyzer & do initialization if necessary.
|
||||
switch name {
|
||||
case "geoip":
|
||||
if err := geoMatcher.LoadGeoIP(); err != nil {
|
||||
return nil, fmt.Errorf("rule %q failed to load geoip: %w", rule.Name, err)
|
||||
}
|
||||
case "geosite":
|
||||
if err := geoMatcher.LoadGeoSite(); err != nil {
|
||||
return nil, fmt.Errorf("rule %q failed to load geosite: %w", rule.Name, err)
|
||||
}
|
||||
case "cidr":
|
||||
// No initialization needed for CIDR.
|
||||
default:
|
||||
a, ok := fullAnMap[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("rule %q uses unknown analyzer %q", rule.Name, name)
|
||||
}
|
||||
depAnMap[name] = a
|
||||
}
|
||||
depAnMap[name] = a
|
||||
}
|
||||
cr := compiledExprRule{
|
||||
Name: rule.Name,
|
||||
Action: action,
|
||||
Program: program,
|
||||
Analyzers: visitor.Analyzers,
|
||||
Name: rule.Name,
|
||||
Action: action,
|
||||
Program: program,
|
||||
}
|
||||
if action == ActionModify {
|
||||
mod, ok := fullModMap[rule.Modifier.Name]
|
||||
@@ -137,11 +166,36 @@ func CompileExprRules(rules []ExprRule, ans []analyzer.Analyzer, mods []modifier
|
||||
depAns = append(depAns, a)
|
||||
}
|
||||
return &exprRuleset{
|
||||
Rules: compiledRules,
|
||||
Ans: depAns,
|
||||
Rules: compiledRules,
|
||||
Ans: depAns,
|
||||
GeoMatcher: geoMatcher,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func registerBuiltinFunctions(funcMap map[string]*ast.Function, geoMatcher *geo.GeoMatcher) {
|
||||
funcMap["geoip"] = &ast.Function{
|
||||
Name: "geoip",
|
||||
Func: func(params ...any) (any, error) {
|
||||
return geoMatcher.MatchGeoIp(params[0].(string), params[1].(string)), nil
|
||||
},
|
||||
Types: []reflect.Type{reflect.TypeOf(geoMatcher.MatchGeoIp)},
|
||||
}
|
||||
funcMap["geosite"] = &ast.Function{
|
||||
Name: "geosite",
|
||||
Func: func(params ...any) (any, error) {
|
||||
return geoMatcher.MatchGeoSite(params[0].(string), params[1].(string)), nil
|
||||
},
|
||||
Types: []reflect.Type{reflect.TypeOf(geoMatcher.MatchGeoSite)},
|
||||
}
|
||||
funcMap["cidr"] = &ast.Function{
|
||||
Name: "cidr",
|
||||
Func: func(params ...any) (any, error) {
|
||||
return builtins.MatchCIDR(params[0].(string), params[1].(*net.IPNet)), nil
|
||||
},
|
||||
Types: []reflect.Type{reflect.TypeOf((func(string, string) bool)(nil)), reflect.TypeOf(builtins.MatchCIDR)},
|
||||
}
|
||||
}
|
||||
|
||||
func streamInfoToExprEnv(info StreamInfo) map[string]interface{} {
|
||||
m := map[string]interface{}{
|
||||
"id": info.ID,
|
||||
@@ -208,12 +262,40 @@ func modifiersToMap(mods []modifier.Modifier) map[string]modifier.Modifier {
|
||||
return modMap
|
||||
}
|
||||
|
||||
type depVisitor struct {
|
||||
Analyzers map[string]struct{}
|
||||
// idVisitor is a visitor that collects all identifiers in an expression.
|
||||
// This is for determining which analyzers are used by the expression.
|
||||
type idVisitor struct {
|
||||
Identifiers map[string]bool
|
||||
}
|
||||
|
||||
func (v *depVisitor) Visit(node *ast.Node) {
|
||||
func (v *idVisitor) Visit(node *ast.Node) {
|
||||
if idNode, ok := (*node).(*ast.IdentifierNode); ok {
|
||||
v.Analyzers[idNode.Value] = struct{}{}
|
||||
v.Identifiers[idNode.Value] = true
|
||||
}
|
||||
}
|
||||
|
||||
// idPatcher patches the AST during expr compilation, replacing certain values with
|
||||
// their internal representations for better runtime performance.
|
||||
type idPatcher struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (p *idPatcher) Visit(node *ast.Node) {
|
||||
switch (*node).(type) {
|
||||
case *ast.CallNode:
|
||||
callNode := (*node).(*ast.CallNode)
|
||||
switch callNode.Func.Name {
|
||||
case "cidr":
|
||||
cidrStringNode, ok := callNode.Arguments[1].(*ast.StringNode)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
cidr, err := builtins.CompileCIDR(cidrStringNode.Value)
|
||||
if err != nil {
|
||||
p.Err = err
|
||||
return
|
||||
}
|
||||
callNode.Arguments[1] = &ast.ConstantNode{Value: cidr}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,3 +92,8 @@ type Ruleset interface {
|
||||
// It must be safe for concurrent use by multiple workers.
|
||||
Match(StreamInfo) (MatchResult, error)
|
||||
}
|
||||
|
||||
type BuiltinConfig struct {
|
||||
GeoSiteFilename string
|
||||
GeoIpFilename string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user