Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dce82745d | ||
|
|
50cc94889f | ||
|
|
5d2d874089 | ||
|
|
797dce3dc2 | ||
|
|
420286a46c | ||
|
|
531a7b0ceb | ||
|
|
20e0637756 | ||
|
|
74dcc92fc6 | ||
|
|
b780ff65a4 | ||
|
|
8bd34d7798 | ||
|
|
bed34f94be | ||
|
|
bc2e21e35d | ||
|
|
a0b994ce22 | ||
|
|
8b07826de6 | ||
|
|
aa6484dfa8 | ||
|
|
29adf99dc1 | ||
|
|
71c739c18f | ||
|
|
182a6cf878 | ||
|
|
ed9e380a57 | ||
|
|
7353a16358 | ||
|
|
465373eaf1 | ||
|
|
f598cb572d |
10
.github/workflows/release.yaml
vendored
10
.github/workflows/release.yaml
vendored
@@ -1,6 +1,6 @@
|
|||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [ created ]
|
types: [published]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -12,8 +12,8 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
goos: [ linux ]
|
goos: [linux]
|
||||||
goarch: [ "386", amd64, arm64 ]
|
goarch: ["386", amd64, arm64]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: wangyoucao577/go-release-action@v1
|
- uses: wangyoucao577/go-release-action@v1
|
||||||
@@ -21,6 +21,6 @@ jobs:
|
|||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
goos: ${{ matrix.goos }}
|
goos: ${{ matrix.goos }}
|
||||||
goarch: ${{ matrix.goarch }}
|
goarch: ${{ matrix.goarch }}
|
||||||
goversion: "https://go.dev/dl/go1.21.6.linux-amd64.tar.gz"
|
goversion: "https://go.dev/dl/go1.22.0.linux-amd64.tar.gz"
|
||||||
binary_name: "OpenGFW"
|
binary_name: "OpenGFW"
|
||||||
extra_files: LICENSE README.md README.zh.md
|
extra_files: LICENSE README.md README.zh.md
|
||||||
|
|||||||
17
README.ja.md
17
README.ja.md
@@ -5,7 +5,9 @@
|
|||||||
[1]: https://img.shields.io/badge/License-MPL_2.0-brightgreen.svg
|
[1]: https://img.shields.io/badge/License-MPL_2.0-brightgreen.svg
|
||||||
[2]: LICENSE
|
[2]: LICENSE
|
||||||
|
|
||||||
OpenGFW は、Linux 上の [GFW](https://en.wikipedia.org/wiki/Great_Firewall) の柔軟で使いやすいオープンソース実装であり、多くの点で本物より強力です。これは家庭用ルーターでできるサイバー主権です。
|
OpenGFW は、あなた専用の DIY 中国のグレートファイアウォール (https://en.wikipedia.org/wiki/Great_Firewall) です。Linux 上で利用可能な柔軟で使いやすいオープンソースプログラムとして提供されています。なぜ権力者だけが楽しむのでしょうか?権力を人々に与え、検閲を民主化する時が来ました。自宅のルーターにサイバー主権のスリルをもたらし、プロのようにフィルタリングを始めましょう - あなたもビッグブラザーになることができます。
|
||||||
|
|
||||||
|
Telegram グループ: https://t.me/OpGFW
|
||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> このプロジェクトはまだ開発の初期段階です。使用は自己責任でお願いします。
|
> このプロジェクトはまだ開発の初期段階です。使用は自己責任でお願いします。
|
||||||
@@ -17,7 +19,7 @@ OpenGFW は、Linux 上の [GFW](https://en.wikipedia.org/wiki/Great_Firewall)
|
|||||||
|
|
||||||
- フル IP/TCP 再アセンブル、各種プロトコルアナライザー
|
- フル IP/TCP 再アセンブル、各種プロトコルアナライザー
|
||||||
- HTTP、TLS、QUIC、DNS、SSH、SOCKS4/5、WireGuard、その他多数
|
- HTTP、TLS、QUIC、DNS、SSH、SOCKS4/5、WireGuard、その他多数
|
||||||
- Shadowsocks の「完全に暗号化されたトラフィック」の検出など (https://gfw.report/publications/usenixsecurity23/data/paper/paper.pdf)
|
- Shadowsocks の「完全に暗号化されたトラフィック」の検出など (https://gfw.report/publications/usenixsecurity23/en/)
|
||||||
- トロイの木馬キラー (https://github.com/XTLS/Trojan-killer) に基づくトロイの木馬 (プロキシプロトコル) 検出
|
- トロイの木馬キラー (https://github.com/XTLS/Trojan-killer) に基づくトロイの木馬 (プロキシプロトコル) 検出
|
||||||
- [WIP] 機械学習に基づくトラフィック分類
|
- [WIP] 機械学習に基づくトラフィック分類
|
||||||
- IPv4 と IPv6 をフルサポート
|
- IPv4 と IPv6 をフルサポート
|
||||||
@@ -36,6 +38,7 @@ OpenGFW は、Linux 上の [GFW](https://en.wikipedia.org/wiki/Great_Firewall)
|
|||||||
- マルウェア対策
|
- マルウェア対策
|
||||||
- VPN/プロキシサービスの不正利用防止
|
- VPN/プロキシサービスの不正利用防止
|
||||||
- トラフィック分析(ログのみモード)
|
- トラフィック分析(ログのみモード)
|
||||||
|
- 独裁的な野心を実現するのを助ける
|
||||||
|
|
||||||
## 使用方法
|
## 使用方法
|
||||||
|
|
||||||
@@ -67,6 +70,8 @@ opkg install kmod-nft-queue kmod-nf-conntrack-netlink
|
|||||||
```yaml
|
```yaml
|
||||||
io:
|
io:
|
||||||
queueSize: 1024
|
queueSize: 1024
|
||||||
|
rcvBuf: 4194304
|
||||||
|
sndBuf: 4194304
|
||||||
local: true # FORWARD チェーンで OpenGFW を実行したい場合は false に設定する
|
local: true # FORWARD チェーンで OpenGFW を実行したい場合は false に設定する
|
||||||
|
|
||||||
workers:
|
workers:
|
||||||
@@ -90,6 +95,11 @@ workers:
|
|||||||
式言語の構文については、[Expr 言語定義](https://expr-lang.org/docs/language-definition)を参照してください。
|
式言語の構文については、[Expr 言語定義](https://expr-lang.org/docs/language-definition)を参照してください。
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
# ルールは、"action" または "log" の少なくとも一方が設定されていなければなりません。
|
||||||
|
- name: log horny people
|
||||||
|
log: true
|
||||||
|
expr: let sni = string(tls?.req?.sni); sni contains "porn" || sni contains "hentai"
|
||||||
|
|
||||||
- name: block v2ex http
|
- name: block v2ex http
|
||||||
action: block
|
action: block
|
||||||
expr: string(http?.req?.headers?.host) endsWith "v2ex.com"
|
expr: string(http?.req?.headers?.host) endsWith "v2ex.com"
|
||||||
@@ -102,8 +112,9 @@ workers:
|
|||||||
action: block
|
action: block
|
||||||
expr: string(quic?.req?.sni) endsWith "v2ex.com"
|
expr: string(quic?.req?.sni) endsWith "v2ex.com"
|
||||||
|
|
||||||
- name: block shadowsocks
|
- name: block and log shadowsocks
|
||||||
action: block
|
action: block
|
||||||
|
log: true
|
||||||
expr: fet != nil && fet.yes
|
expr: fet != nil && fet.yes
|
||||||
|
|
||||||
- name: block trojan
|
- name: block trojan
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -8,8 +8,9 @@
|
|||||||
**[中文文档](README.zh.md)**
|
**[中文文档](README.zh.md)**
|
||||||
**[日本語ドキュメント](README.ja.md)**
|
**[日本語ドキュメント](README.ja.md)**
|
||||||
|
|
||||||
OpenGFW is a flexible, easy-to-use, open source implementation of [GFW](https://en.wikipedia.org/wiki/Great_Firewall) on
|
OpenGFW is your very own DIY Great Firewall of China (https://en.wikipedia.org/wiki/Great_Firewall), available as a flexible, easy-to-use open source program on Linux. Why let the powers that be have all the fun? It's time to give power to the people and democratize censorship. Bring the thrill of cyber-sovereignty right into your home router and start filtering like a pro - you too can play Big Brother.
|
||||||
Linux that's in many ways more powerful than the real thing. It's cyber sovereignty you can have on a home router.
|
|
||||||
|
Telegram group: https://t.me/OpGFW
|
||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> This project is still in very early stages of development. Use at your own risk.
|
> This project is still in very early stages of development. Use at your own risk.
|
||||||
@@ -22,7 +23,7 @@ Linux that's in many ways more powerful than the real thing. It's cyber sovereig
|
|||||||
- Full IP/TCP reassembly, various protocol analyzers
|
- Full IP/TCP reassembly, various protocol analyzers
|
||||||
- HTTP, TLS, QUIC, DNS, SSH, SOCKS4/5, WireGuard, and many more to come
|
- HTTP, TLS, QUIC, DNS, SSH, SOCKS4/5, WireGuard, and many more to come
|
||||||
- "Fully encrypted traffic" detection for Shadowsocks,
|
- "Fully encrypted traffic" detection for Shadowsocks,
|
||||||
etc. (https://gfw.report/publications/usenixsecurity23/data/paper/paper.pdf)
|
etc. (https://gfw.report/publications/usenixsecurity23/en/)
|
||||||
- Trojan (proxy protocol) detection based on Trojan-killer (https://github.com/XTLS/Trojan-killer)
|
- Trojan (proxy protocol) detection based on Trojan-killer (https://github.com/XTLS/Trojan-killer)
|
||||||
- [WIP] Machine learning based traffic classification
|
- [WIP] Machine learning based traffic classification
|
||||||
- Full IPv4 and IPv6 support
|
- Full IPv4 and IPv6 support
|
||||||
@@ -41,6 +42,7 @@ Linux that's in many ways more powerful than the real thing. It's cyber sovereig
|
|||||||
- Malware protection
|
- Malware protection
|
||||||
- Abuse prevention for VPN/proxy services
|
- Abuse prevention for VPN/proxy services
|
||||||
- Traffic analysis (log only mode)
|
- Traffic analysis (log only mode)
|
||||||
|
- Help you fulfill your dictatorial ambitions
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -72,6 +74,8 @@ opkg install kmod-nft-queue kmod-nf-conntrack-netlink
|
|||||||
```yaml
|
```yaml
|
||||||
io:
|
io:
|
||||||
queueSize: 1024
|
queueSize: 1024
|
||||||
|
rcvBuf: 4194304
|
||||||
|
sndBuf: 4194304
|
||||||
local: true # set to false if you want to run OpenGFW on FORWARD chain
|
local: true # set to false if you want to run OpenGFW on FORWARD chain
|
||||||
|
|
||||||
workers:
|
workers:
|
||||||
@@ -96,6 +100,11 @@ For syntax of the expression language, please refer
|
|||||||
to [Expr Language Definition](https://expr-lang.org/docs/language-definition).
|
to [Expr Language Definition](https://expr-lang.org/docs/language-definition).
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
# A rule must have at least one of "action" or "log" field set.
|
||||||
|
- name: log horny people
|
||||||
|
log: true
|
||||||
|
expr: let sni = string(tls?.req?.sni); sni contains "porn" || sni contains "hentai"
|
||||||
|
|
||||||
- name: block v2ex http
|
- name: block v2ex http
|
||||||
action: block
|
action: block
|
||||||
expr: string(http?.req?.headers?.host) endsWith "v2ex.com"
|
expr: string(http?.req?.headers?.host) endsWith "v2ex.com"
|
||||||
@@ -108,8 +117,9 @@ to [Expr Language Definition](https://expr-lang.org/docs/language-definition).
|
|||||||
action: block
|
action: block
|
||||||
expr: string(quic?.req?.sni) endsWith "v2ex.com"
|
expr: string(quic?.req?.sni) endsWith "v2ex.com"
|
||||||
|
|
||||||
- name: block shadowsocks
|
- name: block and log shadowsocks
|
||||||
action: block
|
action: block
|
||||||
|
log: true
|
||||||
expr: fet != nil && fet.yes
|
expr: fet != nil && fet.yes
|
||||||
|
|
||||||
- name: block trojan
|
- name: block trojan
|
||||||
|
|||||||
18
README.zh.md
18
README.zh.md
@@ -5,8 +5,9 @@
|
|||||||
[1]: https://img.shields.io/badge/License-MPL_2.0-brightgreen.svg
|
[1]: https://img.shields.io/badge/License-MPL_2.0-brightgreen.svg
|
||||||
[2]: LICENSE
|
[2]: LICENSE
|
||||||
|
|
||||||
OpenGFW 是一个 Linux 上灵活、易用、开源的 [GFW](https://zh.wikipedia.org/wiki/%E9%98%B2%E7%81%AB%E9%95%BF%E5%9F%8E)
|
OpenGFW 是一个 Linux 上灵活、易用、开源的 DIY [GFW](https://zh.wikipedia.org/wiki/%E9%98%B2%E7%81%AB%E9%95%BF%E5%9F%8E) 实现,并且在许多方面比真正的 GFW 更强大。为何让那些掌权者独享乐趣?是时候把权力归还给人民,人人有墙建了。立即安装可以部署在家用路由器上的网络主权 - 你也能是老大哥。
|
||||||
实现,并且在许多方面比真正的 GFW 更强大。可以部署在家用路由器上的网络主权。
|
|
||||||
|
Telegram 群组: https://t.me/OpGFW
|
||||||
|
|
||||||
> [!CAUTION]
|
> [!CAUTION]
|
||||||
> 本项目仍处于早期开发阶段。测试时自行承担风险。
|
> 本项目仍处于早期开发阶段。测试时自行承担风险。
|
||||||
@@ -18,7 +19,7 @@ OpenGFW 是一个 Linux 上灵活、易用、开源的 [GFW](https://zh.wikipedi
|
|||||||
|
|
||||||
- 完整的 IP/TCP 重组,各种协议解析器
|
- 完整的 IP/TCP 重组,各种协议解析器
|
||||||
- HTTP, TLS, QUIC, DNS, SSH, SOCKS4/5, WireGuard, 更多协议正在开发中
|
- HTTP, TLS, QUIC, DNS, SSH, SOCKS4/5, WireGuard, 更多协议正在开发中
|
||||||
- Shadowsocks 等 "全加密流量" 检测 (https://gfw.report/publications/usenixsecurity23/data/paper/paper.pdf)
|
- Shadowsocks 等 "全加密流量" 检测 (https://gfw.report/publications/usenixsecurity23/zh/)
|
||||||
- 基于 Trojan-killer 的 Trojan 检测 (https://github.com/XTLS/Trojan-killer)
|
- 基于 Trojan-killer 的 Trojan 检测 (https://github.com/XTLS/Trojan-killer)
|
||||||
- [开发中] 基于机器学习的流量分类
|
- [开发中] 基于机器学习的流量分类
|
||||||
- 同等支持 IPv4 和 IPv6
|
- 同等支持 IPv4 和 IPv6
|
||||||
@@ -37,6 +38,7 @@ OpenGFW 是一个 Linux 上灵活、易用、开源的 [GFW](https://zh.wikipedi
|
|||||||
- 恶意软件防护
|
- 恶意软件防护
|
||||||
- VPN/代理服务滥用防护
|
- VPN/代理服务滥用防护
|
||||||
- 流量分析 (纯日志模式)
|
- 流量分析 (纯日志模式)
|
||||||
|
- 助你实现你的独裁野心
|
||||||
|
|
||||||
## 使用
|
## 使用
|
||||||
|
|
||||||
@@ -68,6 +70,8 @@ opkg install kmod-nft-queue kmod-nf-conntrack-netlink
|
|||||||
```yaml
|
```yaml
|
||||||
io:
|
io:
|
||||||
queueSize: 1024
|
queueSize: 1024
|
||||||
|
rcvBuf: 4194304
|
||||||
|
sndBuf: 4194304
|
||||||
local: true # 如果需要在 FORWARD 链上运行 OpenGFW,请设置为 false
|
local: true # 如果需要在 FORWARD 链上运行 OpenGFW,请设置为 false
|
||||||
|
|
||||||
workers:
|
workers:
|
||||||
@@ -91,6 +95,11 @@ workers:
|
|||||||
规则的语法请参考 [Expr Language Definition](https://expr-lang.org/docs/language-definition)。
|
规则的语法请参考 [Expr Language Definition](https://expr-lang.org/docs/language-definition)。
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
# 每条规则必须至少包含 action 或 log 中的一个。
|
||||||
|
- name: log horny people
|
||||||
|
log: true
|
||||||
|
expr: let sni = string(tls?.req?.sni); sni contains "porn" || sni contains "hentai"
|
||||||
|
|
||||||
- name: block v2ex http
|
- name: block v2ex http
|
||||||
action: block
|
action: block
|
||||||
expr: string(http?.req?.headers?.host) endsWith "v2ex.com"
|
expr: string(http?.req?.headers?.host) endsWith "v2ex.com"
|
||||||
@@ -103,8 +112,9 @@ workers:
|
|||||||
action: block
|
action: block
|
||||||
expr: string(quic?.req?.sni) endsWith "v2ex.com"
|
expr: string(quic?.req?.sni) endsWith "v2ex.com"
|
||||||
|
|
||||||
- name: block shadowsocks
|
- name: block and log shadowsocks
|
||||||
action: block
|
action: block
|
||||||
|
log: true
|
||||||
expr: fet != nil && fet.yes
|
expr: fet != nil && fet.yes
|
||||||
|
|
||||||
- name: block trojan
|
- name: block trojan
|
||||||
|
|||||||
@@ -143,8 +143,11 @@ func isTLSorHTTP(bytes []byte) bool {
|
|||||||
if len(bytes) < 3 {
|
if len(bytes) < 3 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if bytes[0] == 0x16 && bytes[1] == 0x03 && bytes[2] <= 0x03 {
|
// "We observe that the GFW exempts any connection whose first
|
||||||
// TLS handshake for TLS 1.0-1.3
|
// three bytes match the following regular expression:
|
||||||
|
// [\x16-\x17]\x03[\x00-\x09]" - from the paper in Section 4.3
|
||||||
|
if bytes[0] >= 0x16 && bytes[0] <= 0x17 &&
|
||||||
|
bytes[1] == 0x03 && bytes[2] <= 0x09 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
// HTTP request
|
// HTTP request
|
||||||
|
|||||||
41
cmd/root.go
41
cmd/root.go
@@ -168,8 +168,10 @@ type cliConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type cliConfigIO struct {
|
type cliConfigIO struct {
|
||||||
QueueSize uint32 `mapstructure:"queueSize"`
|
QueueSize uint32 `mapstructure:"queueSize"`
|
||||||
Local bool `mapstructure:"local"`
|
ReadBuffer int `mapstructure:"rcvBuf"`
|
||||||
|
WriteBuffer int `mapstructure:"sndBuf"`
|
||||||
|
Local bool `mapstructure:"local"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type cliConfigWorkers struct {
|
type cliConfigWorkers struct {
|
||||||
@@ -192,8 +194,10 @@ func (c *cliConfig) fillLogger(config *engine.Config) error {
|
|||||||
|
|
||||||
func (c *cliConfig) fillIO(config *engine.Config) error {
|
func (c *cliConfig) fillIO(config *engine.Config) error {
|
||||||
nfio, err := io.NewNFQueuePacketIO(io.NFQueuePacketIOConfig{
|
nfio, err := io.NewNFQueuePacketIO(io.NFQueuePacketIOConfig{
|
||||||
QueueSize: c.IO.QueueSize,
|
QueueSize: c.IO.QueueSize,
|
||||||
Local: c.IO.Local,
|
ReadBuffer: c.IO.ReadBuffer,
|
||||||
|
WriteBuffer: c.IO.WriteBuffer,
|
||||||
|
Local: c.IO.Local,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return configError{Field: "io", Err: err}
|
return configError{Field: "io", Err: err}
|
||||||
@@ -254,6 +258,7 @@ func runMain(cmd *cobra.Command, args []string) {
|
|||||||
logger.Fatal("failed to load rules", zap.Error(err))
|
logger.Fatal("failed to load rules", zap.Error(err))
|
||||||
}
|
}
|
||||||
rsConfig := &ruleset.BuiltinConfig{
|
rsConfig := &ruleset.BuiltinConfig{
|
||||||
|
Logger: &rulesetLogger{},
|
||||||
GeoSiteFilename: config.Ruleset.GeoSite,
|
GeoSiteFilename: config.Ruleset.GeoSite,
|
||||||
GeoIpFilename: config.Ruleset.GeoIp,
|
GeoIpFilename: config.Ruleset.GeoIp,
|
||||||
}
|
}
|
||||||
@@ -371,14 +376,6 @@ func (l *engineLogger) UDPStreamAction(info ruleset.StreamInfo, action ruleset.A
|
|||||||
zap.Bool("noMatch", noMatch))
|
zap.Bool("noMatch", noMatch))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *engineLogger) MatchError(info ruleset.StreamInfo, err error) {
|
|
||||||
logger.Error("match error",
|
|
||||||
zap.Int64("id", info.ID),
|
|
||||||
zap.String("src", info.SrcString()),
|
|
||||||
zap.String("dst", info.DstString()),
|
|
||||||
zap.Error(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *engineLogger) ModifyError(info ruleset.StreamInfo, err error) {
|
func (l *engineLogger) ModifyError(info ruleset.StreamInfo, err error) {
|
||||||
logger.Error("modify error",
|
logger.Error("modify error",
|
||||||
zap.Int64("id", info.ID),
|
zap.Int64("id", info.ID),
|
||||||
@@ -408,6 +405,26 @@ func (l *engineLogger) AnalyzerErrorf(streamID int64, name string, format string
|
|||||||
zap.String("msg", fmt.Sprintf(format, args...)))
|
zap.String("msg", fmt.Sprintf(format, args...)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type rulesetLogger struct{}
|
||||||
|
|
||||||
|
func (l *rulesetLogger) Log(info ruleset.StreamInfo, name string) {
|
||||||
|
logger.Info("ruleset log",
|
||||||
|
zap.String("name", name),
|
||||||
|
zap.Int64("id", info.ID),
|
||||||
|
zap.String("src", info.SrcString()),
|
||||||
|
zap.String("dst", info.DstString()),
|
||||||
|
zap.Any("props", info.Props))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *rulesetLogger) MatchError(info ruleset.StreamInfo, name string, err error) {
|
||||||
|
logger.Error("ruleset match error",
|
||||||
|
zap.String("name", name),
|
||||||
|
zap.Int64("id", info.ID),
|
||||||
|
zap.String("src", info.SrcString()),
|
||||||
|
zap.String("dst", info.DstString()),
|
||||||
|
zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
func envOrDefaultString(key, def string) string {
|
func envOrDefaultString(key, def string) string {
|
||||||
if v := os.Getenv(key); v != "" {
|
if v := os.Getenv(key); v != "" {
|
||||||
return v
|
return v
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ type Logger interface {
|
|||||||
UDPStreamPropUpdate(info ruleset.StreamInfo, close bool)
|
UDPStreamPropUpdate(info ruleset.StreamInfo, close bool)
|
||||||
UDPStreamAction(info ruleset.StreamInfo, action ruleset.Action, noMatch bool)
|
UDPStreamAction(info ruleset.StreamInfo, action ruleset.Action, noMatch bool)
|
||||||
|
|
||||||
MatchError(info ruleset.StreamInfo, err error)
|
|
||||||
ModifyError(info ruleset.StreamInfo, err error)
|
ModifyError(info ruleset.StreamInfo, err error)
|
||||||
|
|
||||||
AnalyzerDebugf(streamID int64, name string, format string, args ...interface{})
|
AnalyzerDebugf(streamID int64, name string, format string, args ...interface{})
|
||||||
|
|||||||
@@ -148,10 +148,7 @@ func (s *tcpStream) ReassembledSG(sg reassembly.ScatterGather, ac reassembly.Ass
|
|||||||
s.virgin = false
|
s.virgin = false
|
||||||
s.logger.TCPStreamPropUpdate(s.info, false)
|
s.logger.TCPStreamPropUpdate(s.info, false)
|
||||||
// Match properties against ruleset
|
// Match properties against ruleset
|
||||||
result, err := s.ruleset.Match(s.info)
|
result := s.ruleset.Match(s.info)
|
||||||
if err != nil {
|
|
||||||
s.logger.MatchError(s.info, err)
|
|
||||||
}
|
|
||||||
action := result.Action
|
action := result.Action
|
||||||
if action != ruleset.ActionMaybe && action != ruleset.ActionModify {
|
if action != ruleset.ActionMaybe && action != ruleset.ActionModify {
|
||||||
verdict := actionToTCPVerdict(action)
|
verdict := actionToTCPVerdict(action)
|
||||||
|
|||||||
@@ -201,10 +201,7 @@ func (s *udpStream) Feed(udp *layers.UDP, rev bool, uc *udpContext) {
|
|||||||
s.virgin = false
|
s.virgin = false
|
||||||
s.logger.UDPStreamPropUpdate(s.info, false)
|
s.logger.UDPStreamPropUpdate(s.info, false)
|
||||||
// Match properties against ruleset
|
// Match properties against ruleset
|
||||||
result, err := s.ruleset.Match(s.info)
|
result := s.ruleset.Match(s.info)
|
||||||
if err != nil {
|
|
||||||
s.logger.MatchError(s.info, err)
|
|
||||||
}
|
|
||||||
action := result.Action
|
action := result.Action
|
||||||
if action == ruleset.ActionModify {
|
if action == ruleset.ActionModify {
|
||||||
// Call the modifier instance
|
// Call the modifier instance
|
||||||
@@ -214,6 +211,7 @@ func (s *udpStream) Feed(udp *layers.UDP, rev bool, uc *udpContext) {
|
|||||||
s.logger.ModifyError(s.info, errInvalidModifier)
|
s.logger.ModifyError(s.info, errInvalidModifier)
|
||||||
action = ruleset.ActionMaybe
|
action = ruleset.ActionMaybe
|
||||||
} else {
|
} else {
|
||||||
|
var err error
|
||||||
uc.Packet, err = udpMI.Process(udp.Payload)
|
uc.Packet, err = udpMI.Process(udp.Payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Modifier error, fallback to maybe
|
// Modifier error, fallback to maybe
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -16,6 +16,7 @@ require (
|
|||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
go.uber.org/zap v1.26.0
|
go.uber.org/zap v1.26.0
|
||||||
golang.org/x/crypto v0.19.0
|
golang.org/x/crypto v0.19.0
|
||||||
|
golang.org/x/sys v0.17.0
|
||||||
google.golang.org/protobuf v1.31.0
|
google.golang.org/protobuf v1.31.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
@@ -43,7 +44,6 @@ require (
|
|||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
golang.org/x/net v0.19.0 // indirect
|
golang.org/x/net v0.19.0 // indirect
|
||||||
golang.org/x/sync v0.5.0 // indirect
|
golang.org/x/sync v0.5.0 // indirect
|
||||||
golang.org/x/sys v0.17.0 // indirect
|
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/coreos/go-iptables/iptables"
|
"github.com/coreos/go-iptables/iptables"
|
||||||
"github.com/florianl/go-nfqueue"
|
"github.com/florianl/go-nfqueue"
|
||||||
"github.com/mdlayher/netlink"
|
"github.com/mdlayher/netlink"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -96,8 +97,10 @@ type nfqueuePacketIO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NFQueuePacketIOConfig struct {
|
type NFQueuePacketIOConfig struct {
|
||||||
QueueSize uint32
|
QueueSize uint32
|
||||||
Local bool
|
ReadBuffer int
|
||||||
|
WriteBuffer int
|
||||||
|
Local bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNFQueuePacketIO(config NFQueuePacketIOConfig) (PacketIO, error) {
|
func NewNFQueuePacketIO(config NFQueuePacketIOConfig) (PacketIO, error) {
|
||||||
@@ -127,6 +130,20 @@ func NewNFQueuePacketIO(config NFQueuePacketIOConfig) (PacketIO, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if config.ReadBuffer > 0 {
|
||||||
|
err = n.Con.SetReadBuffer(config.ReadBuffer)
|
||||||
|
if err != nil {
|
||||||
|
_ = n.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if config.WriteBuffer > 0 {
|
||||||
|
err = n.Con.SetWriteBuffer(config.WriteBuffer)
|
||||||
|
if err != nil {
|
||||||
|
_ = n.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
return &nfqueuePacketIO{
|
return &nfqueuePacketIO{
|
||||||
n: n,
|
n: n,
|
||||||
local: config.Local,
|
local: config.Local,
|
||||||
@@ -138,9 +155,10 @@ func NewNFQueuePacketIO(config NFQueuePacketIOConfig) (PacketIO, error) {
|
|||||||
func (n *nfqueuePacketIO) Register(ctx context.Context, cb PacketCallback) error {
|
func (n *nfqueuePacketIO) Register(ctx context.Context, cb PacketCallback) error {
|
||||||
err := n.n.RegisterWithErrorFunc(ctx,
|
err := n.n.RegisterWithErrorFunc(ctx,
|
||||||
func(a nfqueue.Attribute) int {
|
func(a nfqueue.Attribute) int {
|
||||||
if a.PacketID == nil || a.Ct == nil || a.Payload == nil || len(*a.Payload) < 20 {
|
if ok, verdict := n.packetAttributeSanityCheck(a); !ok {
|
||||||
// Invalid packet, ignore
|
if a.PacketID != nil {
|
||||||
// 20 is the minimum possible size of an IP packet
|
_ = n.n.SetVerdict(*a.PacketID, verdict)
|
||||||
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
p := &nfqueuePacket{
|
p := &nfqueuePacket{
|
||||||
@@ -151,6 +169,12 @@ func (n *nfqueuePacketIO) Register(ctx context.Context, cb PacketCallback) error
|
|||||||
return okBoolToInt(cb(p, nil))
|
return okBoolToInt(cb(p, nil))
|
||||||
},
|
},
|
||||||
func(e error) int {
|
func(e error) int {
|
||||||
|
if opErr := (*netlink.OpError)(nil); errors.As(e, &opErr) {
|
||||||
|
if errors.Is(opErr.Err, unix.ENOBUFS) {
|
||||||
|
// Kernel buffer temporarily full, ignore
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
return okBoolToInt(cb(nil, e))
|
return okBoolToInt(cb(nil, e))
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -170,6 +194,25 @@ func (n *nfqueuePacketIO) Register(ctx context.Context, cb PacketCallback) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *nfqueuePacketIO) packetAttributeSanityCheck(a nfqueue.Attribute) (ok bool, verdict int) {
|
||||||
|
if a.PacketID == nil {
|
||||||
|
// Re-inject to NFQUEUE is actually not possible in this condition
|
||||||
|
return false, -1
|
||||||
|
}
|
||||||
|
if a.Payload == nil || len(*a.Payload) < 20 {
|
||||||
|
// 20 is the minimum possible size of an IP packet
|
||||||
|
return false, nfqueue.NfDrop
|
||||||
|
}
|
||||||
|
if a.Ct == nil {
|
||||||
|
// Multicast packets may not have a conntrack, but only appear in local mode
|
||||||
|
if n.local {
|
||||||
|
return false, nfqueue.NfAccept
|
||||||
|
}
|
||||||
|
return false, nfqueue.NfDrop
|
||||||
|
}
|
||||||
|
return true, -1
|
||||||
|
}
|
||||||
|
|
||||||
func (n *nfqueuePacketIO) SetVerdict(p Packet, v Verdict, newPacket []byte) error {
|
func (n *nfqueuePacketIO) SetVerdict(p Packet, v Verdict, newPacket []byte) error {
|
||||||
nP, ok := p.(*nfqueuePacket)
|
nP, ok := p.(*nfqueuePacket)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
type ExprRule struct {
|
type ExprRule struct {
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
Action string `yaml:"action"`
|
Action string `yaml:"action"`
|
||||||
|
Log bool `yaml:"log"`
|
||||||
Modifier ModifierEntry `yaml:"modifier"`
|
Modifier ModifierEntry `yaml:"modifier"`
|
||||||
Expr string `yaml:"expr"`
|
Expr string `yaml:"expr"`
|
||||||
}
|
}
|
||||||
@@ -45,7 +46,8 @@ func ExprRulesFromYAML(file string) ([]ExprRule, error) {
|
|||||||
// compiledExprRule is the internal, compiled representation of an expression rule.
|
// compiledExprRule is the internal, compiled representation of an expression rule.
|
||||||
type compiledExprRule struct {
|
type compiledExprRule struct {
|
||||||
Name string
|
Name string
|
||||||
Action Action
|
Action *Action // fallthrough if nil
|
||||||
|
Log bool
|
||||||
ModInstance modifier.Instance
|
ModInstance modifier.Instance
|
||||||
Program *vm.Program
|
Program *vm.Program
|
||||||
}
|
}
|
||||||
@@ -55,6 +57,7 @@ var _ Ruleset = (*exprRuleset)(nil)
|
|||||||
type exprRuleset struct {
|
type exprRuleset struct {
|
||||||
Rules []compiledExprRule
|
Rules []compiledExprRule
|
||||||
Ans []analyzer.Analyzer
|
Ans []analyzer.Analyzer
|
||||||
|
Logger Logger
|
||||||
GeoMatcher *geo.GeoMatcher
|
GeoMatcher *geo.GeoMatcher
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,25 +65,31 @@ func (r *exprRuleset) Analyzers(info StreamInfo) []analyzer.Analyzer {
|
|||||||
return r.Ans
|
return r.Ans
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *exprRuleset) Match(info StreamInfo) (MatchResult, error) {
|
func (r *exprRuleset) Match(info StreamInfo) MatchResult {
|
||||||
env := streamInfoToExprEnv(info)
|
env := streamInfoToExprEnv(info)
|
||||||
for _, rule := range r.Rules {
|
for _, rule := range r.Rules {
|
||||||
v, err := vm.Run(rule.Program, env)
|
v, err := vm.Run(rule.Program, env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return MatchResult{
|
// Log the error and continue to the next rule.
|
||||||
Action: ActionMaybe,
|
r.Logger.MatchError(info, rule.Name, err)
|
||||||
}, fmt.Errorf("rule %q failed to run: %w", rule.Name, err)
|
continue
|
||||||
}
|
}
|
||||||
if vBool, ok := v.(bool); ok && vBool {
|
if vBool, ok := v.(bool); ok && vBool {
|
||||||
return MatchResult{
|
if rule.Log {
|
||||||
Action: rule.Action,
|
r.Logger.Log(info, rule.Name)
|
||||||
ModInstance: rule.ModInstance,
|
}
|
||||||
}, nil
|
if rule.Action != nil {
|
||||||
|
return MatchResult{
|
||||||
|
Action: *rule.Action,
|
||||||
|
ModInstance: rule.ModInstance,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// No match
|
||||||
return MatchResult{
|
return MatchResult{
|
||||||
Action: ActionMaybe,
|
Action: ActionMaybe,
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CompileExprRules compiles a list of expression rules into a ruleset.
|
// CompileExprRules compiles a list of expression rules into a ruleset.
|
||||||
@@ -97,11 +106,18 @@ func CompileExprRules(rules []ExprRule, ans []analyzer.Analyzer, mods []modifier
|
|||||||
}
|
}
|
||||||
// Compile all rules and build a map of analyzers that are used by the rules.
|
// Compile all rules and build a map of analyzers that are used by the rules.
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
action, ok := actionStringToAction(rule.Action)
|
if rule.Action == "" && !rule.Log {
|
||||||
if !ok {
|
return nil, fmt.Errorf("rule %q must have at least one of action or log", rule.Name)
|
||||||
return nil, fmt.Errorf("rule %q has invalid action %q", rule.Name, rule.Action)
|
|
||||||
}
|
}
|
||||||
visitor := &idVisitor{Identifiers: make(map[string]bool)}
|
var action *Action
|
||||||
|
if rule.Action != "" {
|
||||||
|
a, ok := actionStringToAction(rule.Action)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("rule %q has invalid action %q", rule.Name, rule.Action)
|
||||||
|
}
|
||||||
|
action = &a
|
||||||
|
}
|
||||||
|
visitor := &idVisitor{Variables: make(map[string]bool), Identifiers: make(map[string]bool)}
|
||||||
patcher := &idPatcher{}
|
patcher := &idPatcher{}
|
||||||
program, err := expr.Compile(rule.Expr,
|
program, err := expr.Compile(rule.Expr,
|
||||||
func(c *conf.Config) {
|
func(c *conf.Config) {
|
||||||
@@ -118,7 +134,8 @@ func CompileExprRules(rules []ExprRule, ans []analyzer.Analyzer, mods []modifier
|
|||||||
return nil, fmt.Errorf("rule %q failed to patch expression: %w", rule.Name, patcher.Err)
|
return nil, fmt.Errorf("rule %q failed to patch expression: %w", rule.Name, patcher.Err)
|
||||||
}
|
}
|
||||||
for name := range visitor.Identifiers {
|
for name := range visitor.Identifiers {
|
||||||
if isBuiltInAnalyzer(name) {
|
// Skip built-in analyzers & user-defined variables
|
||||||
|
if isBuiltInAnalyzer(name) || visitor.Variables[name] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Check if it's one of the built-in functions, and if so,
|
// Check if it's one of the built-in functions, and if so,
|
||||||
@@ -145,9 +162,10 @@ func CompileExprRules(rules []ExprRule, ans []analyzer.Analyzer, mods []modifier
|
|||||||
cr := compiledExprRule{
|
cr := compiledExprRule{
|
||||||
Name: rule.Name,
|
Name: rule.Name,
|
||||||
Action: action,
|
Action: action,
|
||||||
|
Log: rule.Log,
|
||||||
Program: program,
|
Program: program,
|
||||||
}
|
}
|
||||||
if action == ActionModify {
|
if action != nil && *action == ActionModify {
|
||||||
mod, ok := fullModMap[rule.Modifier.Name]
|
mod, ok := fullModMap[rule.Modifier.Name]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("rule %q uses unknown modifier %q", rule.Name, rule.Modifier.Name)
|
return nil, fmt.Errorf("rule %q uses unknown modifier %q", rule.Name, rule.Modifier.Name)
|
||||||
@@ -168,6 +186,7 @@ func CompileExprRules(rules []ExprRule, ans []analyzer.Analyzer, mods []modifier
|
|||||||
return &exprRuleset{
|
return &exprRuleset{
|
||||||
Rules: compiledRules,
|
Rules: compiledRules,
|
||||||
Ans: depAns,
|
Ans: depAns,
|
||||||
|
Logger: config.Logger,
|
||||||
GeoMatcher: geoMatcher,
|
GeoMatcher: geoMatcher,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
@@ -265,11 +284,14 @@ func modifiersToMap(mods []modifier.Modifier) map[string]modifier.Modifier {
|
|||||||
// idVisitor is a visitor that collects all identifiers in an expression.
|
// idVisitor is a visitor that collects all identifiers in an expression.
|
||||||
// This is for determining which analyzers are used by the expression.
|
// This is for determining which analyzers are used by the expression.
|
||||||
type idVisitor struct {
|
type idVisitor struct {
|
||||||
|
Variables map[string]bool
|
||||||
Identifiers map[string]bool
|
Identifiers map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *idVisitor) Visit(node *ast.Node) {
|
func (v *idVisitor) Visit(node *ast.Node) {
|
||||||
if idNode, ok := (*node).(*ast.IdentifierNode); ok {
|
if varNode, ok := (*node).(*ast.VariableDeclaratorNode); ok {
|
||||||
|
v.Variables[varNode.Name] = true
|
||||||
|
} else if idNode, ok := (*node).(*ast.IdentifierNode); ok {
|
||||||
v.Identifiers[idNode.Value] = true
|
v.Identifiers[idNode.Value] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -284,6 +306,10 @@ func (p *idPatcher) Visit(node *ast.Node) {
|
|||||||
switch (*node).(type) {
|
switch (*node).(type) {
|
||||||
case *ast.CallNode:
|
case *ast.CallNode:
|
||||||
callNode := (*node).(*ast.CallNode)
|
callNode := (*node).(*ast.CallNode)
|
||||||
|
if callNode.Func == nil {
|
||||||
|
// Ignore invalid call nodes
|
||||||
|
return
|
||||||
|
}
|
||||||
switch callNode.Func.Name {
|
switch callNode.Func.Name {
|
||||||
case "cidr":
|
case "cidr":
|
||||||
cidrStringNode, ok := callNode.Arguments[1].(*ast.StringNode)
|
cidrStringNode, ok := callNode.Arguments[1].(*ast.StringNode)
|
||||||
|
|||||||
@@ -90,10 +90,17 @@ type Ruleset interface {
|
|||||||
Analyzers(StreamInfo) []analyzer.Analyzer
|
Analyzers(StreamInfo) []analyzer.Analyzer
|
||||||
// Match matches a stream against the ruleset and returns the result.
|
// Match matches a stream against the ruleset and returns the result.
|
||||||
// It must be safe for concurrent use by multiple workers.
|
// It must be safe for concurrent use by multiple workers.
|
||||||
Match(StreamInfo) (MatchResult, error)
|
Match(StreamInfo) MatchResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger is the logging interface for the ruleset.
|
||||||
|
type Logger interface {
|
||||||
|
Log(info StreamInfo, name string)
|
||||||
|
MatchError(info StreamInfo, name string, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type BuiltinConfig struct {
|
type BuiltinConfig struct {
|
||||||
|
Logger Logger
|
||||||
GeoSiteFilename string
|
GeoSiteFilename string
|
||||||
GeoIpFilename string
|
GeoIpFilename string
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user