Compare commits

...

108 Commits

Author SHA1 Message Date
Toby
278d731b6f
Merge pull request #147 from kpetku/feat-expose-netlink-config-options
feat: netlink queueNum/table config options
2024-10-27 18:55:30 -07:00
Haruue
0e97c9f086
feat: connmark accept/drop config options 2024-10-28 10:17:46 +09:00
Haruue
5f4df7e806
chore: rm nfqueueNum parameter in setupNft() 2024-10-27 15:44:13 +09:00
Haruue
d8d7c5b477
chore: allow set nfqueue num to 0 2024-10-27 15:44:04 +09:00
Keith Petkus
d3f1785ac9 feat: netlink queueNum/table config options 2024-08-08 13:24:49 -04:00
Toby
1de95ed53e
Merge pull request #136 from apernet/wip-pcapgo
Add pcap back
2024-05-08 17:40:55 -07:00
Haruue
1934c065ec
feat(pcap): impl realtime wait() with time offset 2024-05-08 19:45:10 +08:00
Haruue
301f9af3d4
Revert "ci: install pcap for build 2"
This reverts commit 0daaa32fc6c77fa4566266986d9a6f84722e6903.
2024-05-08 19:26:19 +08:00
Haruue
cb0427bfbb
Revert "ci: install pcap for build"
This reverts commit 5e15fd6dd937fa040254fe368ee2b6645d3f50ff.
2024-05-08 19:26:15 +08:00
Haruue
7456e5907e
refactor(pcap): switch to pcapgo 2024-05-08 19:22:17 +08:00
Haruue
8cab86b924
Reapply "Merge pull request #132 from eddc005/feat-pcap"
This reverts commit 2ac8783eb66328e580b5958c696e82c53cb254dd.
2024-05-08 19:13:49 +08:00
Toby
3ec5456e86
Merge pull request #135 from apernet/revert-pcap
Revert pcap
2024-05-07 23:46:46 -07:00
Toby
b51ea5fa07 Revert "Merge pull request #134 from apernet/ci-cgo"
This reverts commit 5014523ae01d25b9fb69d5cd457f7b062bf71a7a, reversing
changes made to c453020349c4245c4884f4f1f6238098e4bf4906.
2024-05-07 23:44:00 -07:00
Toby
2ac8783eb6 Revert "Merge pull request #132 from eddc005/feat-pcap"
This reverts commit c453020349c4245c4884f4f1f6238098e4bf4906, reversing
changes made to 5723490a6c45eb5f2e03fa60cb7fe3bfc048995e.
2024-05-07 23:43:54 -07:00
Toby
5014523ae0
Merge pull request #134 from apernet/ci-cgo
ci: enable cgo
2024-05-07 23:40:17 -07:00
Toby
dabcc9566c ci: enable cgo 2024-05-07 23:38:28 -07:00
Toby
c453020349
Merge pull request #132 from eddc005/feat-pcap
feat: add support for pcap replay
2024-05-07 23:18:03 -07:00
Toby
0daaa32fc6 ci: install pcap for build 2 2024-05-07 23:13:58 -07:00
Toby
5e15fd6dd9 ci: install pcap for build 2024-05-07 23:12:24 -07:00
Toby
76c0f47832 chore: do not default replay.realtime to true 2024-05-07 23:05:06 -07:00
Toby
70fee14103 chore: format 2024-05-07 22:50:58 -07:00
eddc005
abd7725fed close pcap properly and implement ProtectedDialContext 2024-05-07 21:50:06 +01:00
eddc005
f01b79e625 rebase and remove replayDelay 2024-05-06 23:04:54 +01:00
eddc005
94387450cf feat: add support for pcap replay 2024-05-06 22:53:11 +01:00
Toby
5723490a6c
Merge pull request #133 from apernet/fix-timestamp
fix: provide correct timestamp for TCP reassembler
2024-05-06 14:38:35 -07:00
Toby
d7506264ad fix: provide correct timestamp for TCP reassembler 2024-05-06 14:35:31 -07:00
Toby
245ac46b65
Merge pull request #130 from apernet/fix-geo-leak
fix: do not reload geoip/geosite when reloading ruleset to prevent leaking references to streams
2024-04-10 22:57:18 -07:00
Toby
107e29ee20 fix: do not reload geoip/geosite when reloading ruleset to prevent leaking references to streams 2024-04-10 21:30:37 -07:00
Toby
5f447d4e31
Merge pull request #124 from apernet/wip-tcp-flush
feat: TCP timeout flush
2024-04-09 11:07:20 -07:00
Toby
347667a2bd feat: TCP timeout flush 2024-04-08 11:54:35 -07:00
Toby
393c29bd2d
Merge pull request #123 from apernet/wip-lookup
feat: dns lookup function
2024-04-07 17:49:33 -07:00
Toby
9c0893c512 feat: added protected dial support, removed multi-IO support for simplicity 2024-04-06 14:42:45 -07:00
Toby
ae34b4856a feat: dns lookup function 2024-04-03 20:02:57 -07:00
Toby
d7737e9211
Merge pull request #119 from apernet/update-readme
docs: update readme feature list
2024-04-01 21:50:07 -07:00
Toby
dd9ecc3dd7 docs: update readme feature list 2024-04-01 21:49:06 -07:00
Toby
980d59ed2e
Merge pull request #118 from apernet/ci-release
ci: rework release workflow
2024-04-01 20:55:22 -07:00
Toby
af14adf313 ci: rework release workflow 2024-04-01 20:54:24 -07:00
Toby
ab28fc238d
Merge pull request #114 from KujouRinka/feat-openvpn
feat: add openVPN analyzer
2024-04-01 20:47:30 -07:00
Toby
e535769086 fix: make sure rx_pkt_cnt & tx_pkt_cnt both always exist 2024-03-30 14:30:20 -07:00
Toby
ecd60d0ff1 chore: improve case spelling 2024-03-30 13:29:19 -07:00
Toby
98264d9e27 chore: format 2024-03-30 13:23:46 -07:00
Toby
bb5d4e32ff
Merge pull request #116 from apernet/update-trojan
feat: further improve trojan detection
2024-03-29 23:19:36 -07:00
Toby
ca574393d3 feat: further improve trojan detection 2024-03-29 23:02:20 -07:00
KujouRinka
0e2ee36865 refactor: merge openvpn_tcp and openvpn_udp to openvpn 2024-03-30 12:00:17 +08:00
Toby
b02738cde8
Merge pull request #115 from apernet/update-readme
docs: move to website
2024-03-29 13:08:14 -07:00
Toby
0735fa831d docs: move to website 2024-03-29 13:06:29 -07:00
KujouRinka
2232b553b3 chore: fix typo 2024-03-29 21:37:09 +08:00
KujouRinka
b2f6dec909 feat: add openVPN analyzer 2024-03-29 20:29:30 +08:00
Toby
47a3c9875c
Merge pull request #112 from apernet/docs-dep
docs: update dependency install command for openwrt
2024-03-24 11:42:10 -07:00
Toby
4e604904af docs: update dependency install command for openwrt 2024-03-24 11:41:17 -07:00
Toby
bf2988116a
Merge pull request #109 from apernet/wip-io-rst
feat: io tcp reset support (forward only)
2024-03-21 18:43:43 -07:00
Haruue
ef1416274d
refactor(io): nft/ipt generator func 2024-03-22 02:04:25 +08:00
Toby
57c818038c feat: io tcp reset support (forward only) 2024-03-20 19:01:26 -07:00
Toby
6ad7714c9a
Merge pull request #108 from apernet/fix-trojan-doc
docs: remove trojan-killer ref
2024-03-20 18:41:45 -07:00
Toby
ff9c4ccf79 docs: remove trojan-killer ref 2024-03-20 18:40:49 -07:00
Toby
e1d9406fdb
Merge pull request #107 from apernet/fix-trojan
fix: trojan analyzer heuristics
2024-03-20 18:39:05 -07:00
Toby
b8e5079b8a fix: trojan analyzer heuristics 2024-03-20 18:37:43 -07:00
Toby
f3b72895ad
Merge pull request #106 from apernet/wip-new-trojan
feat: new heuristics for trojan analyzer
2024-03-20 18:11:09 -07:00
Toby
0732dfa7a5 docs: no longer use trojan-killer 2024-03-20 18:10:22 -07:00
Toby
9d96acd8db feat: new heuristics for trojan analyzer 2024-03-20 18:07:26 -07:00
Toby
d1775184ce
Merge pull request #102 from macie/badge_fix
docs: Fix status badge URL
2024-03-14 00:37:37 -07:00
macie
05d56616fc
ci: Add readable name for workflow
It is visible on Workflows list inside GH Actions tab.
2024-03-14 08:14:30 +01:00
macie
ede70e1a87
docs: Fix badge URL 2024-03-14 08:06:27 +01:00
Toby
920783bd65
Merge pull request #98 from macie/ci
CI/CD: New workflow for testing and static analysis
2024-03-13 19:29:36 -07:00
macie
3a45461c19
test: Remove broken integration test
This tests fails when there are no specific binary files, but there is
no method to get the files during test.

Marking the tests as a skipped will gives us a false sense of security. So it's
better to remove it at all.
2024-03-12 12:15:10 +01:00
macie
3022bde81b
fix: Linter errors
Fixed:
- "ineffective break statement. Did you mean to break out of the outer loop?" (SA4011)
- "channels used with os/signal.Notify should be buffered" (SA1017)
- "os.Kill cannot be trapped (did you mean syscall.SIGTERM?)" (SA1016)
- "func envOrDefaultBool is unused" (U1000)
- "should use time.Since instead of time.Now().Sub" (S1012)
2024-03-12 12:14:58 +01:00
macie
d98136bac7
ci: Add quality check
Runs tests and linters after each commit.
2024-03-11 22:19:26 +01:00
macie
c0e2483f6c
test: Add basic tests for packet parsing
Tests performed on real-like packets secure expected behavior.
2024-03-11 21:25:34 +01:00
macie
3bd02ed46e
refactor: Improve parsing docs
Reveal intentions by:
- extracting magic numbers into constants
- changing function names with >1 responsibilities
- documenting non-obvious behaviors.
2024-03-11 20:35:01 +01:00
Toby
4257788f33
Merge pull request #92 from apernet/wip-doc-openwrt-ipt
docs: instruction for OpenWrt 22.02 or earlier
2024-03-09 09:23:53 -08:00
Haruue
e77c2fabea
docs: instruction for OpenWrt 22.02 or earlier 2024-03-09 21:42:49 +08:00
Toby
1dce82745d
Merge pull request #85 from apernet/wip-buf
feat: netlink rcv/snd buffer config options
2024-02-29 11:23:08 -08:00
Toby
50cc94889f feat: netlink rcv/snd buffer config options 2024-02-28 17:45:24 -08:00
Toby
5d2d874089
Merge pull request #82 from apernet/update-fet
feat: update FET analyzer to better reflect what's described in the paper
2024-02-26 15:28:33 -08:00
Toby
797dce3dc2 feat: update FET analyzer to better reflect what's described in the paper 2024-02-26 15:27:35 -08:00
Toby
420286a46c
Merge pull request #81 from apernet/update-gfwreport
chore: update gfw report links
2024-02-26 15:17:33 -08:00
Toby
531a7b0ceb chore: update gfw report links 2024-02-26 15:17:07 -08:00
Toby
20e0637756
Merge pull request #79 from apernet/update-ci
fix: release workflow
2024-02-26 10:50:44 -08:00
Toby
74dcc92fc6 fix: release workflow 2024-02-26 10:49:19 -08:00
Toby
b780ff65a4
Merge pull request #76 from apernet/fix-enobufs
fix: engine exit with "netlink receive: recvmsg: no buffer space available" when too many packets hit NFQUEUE
2024-02-26 10:40:08 -08:00
Haruue
8bd34d7798
chore: go mod tidy 2024-02-26 16:48:39 +08:00
Haruue
bed34f94be
fix: engine exit when too many packets hit NFQUEUE 2024-02-26 16:46:50 +08:00
Toby
bc2e21e35d
Merge pull request #75 from apernet/fix-missing-verdict
fix: verdict is missing for multicast packets
2024-02-26 00:12:42 -08:00
Haruue
a0b994ce22
fix: verdict is missing for multicast packets 2024-02-26 15:45:07 +08:00
Toby
8b07826de6
Merge pull request #71 from apernet/wip-log
feat: logging support in ruleset
2024-02-23 19:51:18 -08:00
Toby
aa6484dfa8
Merge pull request #73 from apernet/wip-readme2
docs: update README
2024-02-23 18:31:30 -08:00
Toby
29adf99dc1 docs: update README 2024-02-23 18:31:16 -08:00
Toby
71c739c18f
Merge pull request #72 from apernet/update-readme
chore: better README
2024-02-23 15:43:25 -08:00
Toby
182a6cf878 chore: better README 2024-02-23 15:43:08 -08:00
Toby
ed9e380a57 fix: variable support & update example in doc 2024-02-23 14:37:05 -08:00
Toby
7353a16358 feat: logging support in ruleset 2024-02-23 14:13:35 -08:00
Toby
465373eaf1
Merge pull request #66 from apernet/readme-tg
docs: add telegram group link
2024-02-17 22:34:56 -08:00
Toby
f598cb572d docs: add telegram group link 2024-02-17 22:34:38 -08:00
Toby
54f62ce0bb
Merge pull request #64 from apernet/wip-quic
feat: QUIC analyzer
2024-02-17 22:08:59 -08:00
Haruue Icymoon
22bbf0d9c7
fix: typo in test 2024-02-18 12:26:03 +08:00
Toby
7e511e94de fix merge 2024-02-17 14:39:12 -08:00
Toby
e177837301
Merge branch 'master' into wip-quic 2024-02-17 14:38:19 -08:00
Toby
be672a97ab
Merge pull request #65 from apernet/update-gopkt
feat: update gopacket to latest master
2024-02-17 14:34:23 -08:00
Toby
5c77cede3d feat: update gopacket to latest master 2024-02-17 14:28:57 -08:00
Rinka
ebff4308e4
Add CIDR support for expr (#62)
* feat: add cidr support for expr

* docs: add example for cidr

* minor code tweaks

---------

Co-authored-by: Toby <tobyxdd@gmail.com>
2024-02-17 14:21:12 -08:00
rootmelo92118
94cfe7b2c1
Add the configuration for the path of geoip and geosite. (#57)
* Update README.md

* Update README.zh.md

* Update README.ja.md

* minor tweaks

---------

Co-authored-by: Toby <tobyxdd@gmail.com>
2024-02-17 14:03:50 -08:00
Toby
ef352450a2 docs: add QUIC 2024-02-17 13:56:33 -08:00
Toby
4ede93ce7b
fix: incorrect "virgin" handling causing rules with only built-in keywords to fail (#61) 2024-02-16 19:08:19 -08:00
Toby
c1e90960dd feat: quic analyzer (client side only) 2024-02-11 22:25:37 -08:00
Toby
7a52228ec6
docs: add a section about openwrt (#53) 2024-02-11 13:12:49 -08:00
Toby
6d33a0d51c
fix: incorrect verdict handling that caused packets to pass through even after they had been blocked (#52) 2024-02-11 13:05:05 -08:00
Toby
27c9b91a61
feat: nftables support (#50)
* feat: nftables support

* fix: format
2024-02-11 13:04:49 -08:00
TAKAHASHI Shuuji
36bb4b796d
docs: update README.ja.md (#49) 2024-02-05 20:44:56 -08:00
41 changed files with 3065 additions and 1289 deletions

47
.github/workflows/check.yaml vendored Normal file
View File

@ -0,0 +1,47 @@
name: Quality check
on:
push:
branches:
- "*"
pull_request:
permissions:
contents: read
jobs:
static-analysis:
name: Static analysis
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 'stable'
- run: go vet ./...
- name: staticcheck
uses: dominikh/staticcheck-action@v1.3.0
with:
install-go: false
tests:
name: Tests
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 'stable'
- run: go test ./...

View File

@ -1,6 +1,7 @@
name: Release
on:
release:
types: [ created ]
types: [published]
permissions:
contents: write
@ -12,15 +13,27 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
goos: [ linux ]
goarch: [ "386", amd64, arm64 ]
goos: [linux]
goarch: ["386", amd64, arm64]
steps:
- uses: actions/checkout@v4
- uses: wangyoucao577/go-release-action@v1
- name: Setup Go
uses: actions/setup-go@v5
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
goversion: "https://go.dev/dl/go1.21.6.linux-amd64.tar.gz"
binary_name: "OpenGFW"
extra_files: LICENSE README.md README.zh.md
go-version: "1.22"
- name: Build
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: 0
run: |
mkdir -p build
go build -o build/OpenGFW-${GOOS}-${GOARCH} -ldflags "-s -w" .
- name: Upload
uses: softprops/action-gh-release@v2
with:
files: build/*

3
.gitignore vendored
View File

@ -205,3 +205,6 @@ $RECYCLE.BIN/
*.lnk
# End of https://www.toptal.com/developers/gitignore/api/windows,macos,linux,go,goland+all,visualstudiocode
# Internal tools not ready for public use yet
tools/flowseq/

View File

@ -1,25 +1,26 @@
# ![OpenGFW](docs/logo.png)
[![Quality check status](https://github.com/apernet/OpenGFW/actions/workflows/check.yaml/badge.svg)](https://github.com/apernet/OpenGFW/actions/workflows/check.yaml)
[![License][1]][2]
[1]: https://img.shields.io/badge/License-MPL_2.0-brightgreen.svg
[2]: LICENSE
OpenGFW は、Linux 上の [GFW](https://en.wikipedia.org/wiki/Great_Firewall) の柔軟で使いやすいオープンソース実装であり、多くの点で本物より強力です。これは家庭用ルーターでできるサイバー主権です。
OpenGFW は、あなた専用の DIY 中国のグレートファイアウォール (https://en.wikipedia.org/wiki/Great_Firewall) です。Linux 上で利用可能な柔軟で使いやすいオープンソースプログラムとして提供されています。なぜ権力者だけが楽しむのでしょうか?権力を人々に与え、検閲を民主化する時が来ました。自宅のルーターにサイバー主権のスリルをもたらし、プロのようにフィルタリングを始めましょう - あなたもビッグブラザーになることができます。
**ドキュメントウェブサイト: https://gfw.dev/**
Telegram グループ: https://t.me/OpGFW
> [!CAUTION]
> このプロジェクトはまだ開発の初期段階です。使用は自己責任でお願いします。
> [!NOTE]
> 私たちはこのプロジェクト、特により多くのプロトコル用のアナライザーの実装を手伝ってくれるコントリビューターを探しています!!!
> 本プロジェクトはまだ初期開発段階にあります。テスト時のリスクは自己責任でお願いします。私たちは、このプロジェクトを一緒に改善するために貢献者を探しています。
## 特徴
- フル IP/TCP 再アセンブル、各種プロトコルアナライザー
- HTTP、TLS、DNS、SSH、SOCKS4/5、WireGuard、その他多数
- Shadowsocks の"完全に暗号化されたトラフィック"の検出、
など。 (https://gfw.report/publications/usenixsecurity23/data/paper/paper.pdf)
- トロイの木馬キラー (https://github.com/XTLS/Trojan-killer) に基づくトロイの木馬 (プロキシプロトコル) 検出
- HTTP、TLS、QUIC、DNS、SSH、SOCKS4/5、WireGuard、OpenVPN、その他多数
- Shadowsocks、VMess の「完全に暗号化されたトラフィック」の検出など (https://gfw.report/publications/usenixsecurity23/en/)
- Trojan プロキシプロトコルの検出
- [WIP] 機械学習に基づくトラフィック分類
- IPv4 と IPv6 をフルサポート
- フローベースのマルチコア負荷分散
@ -27,7 +28,7 @@ OpenGFW は、Linux 上の [GFW](https://en.wikipedia.org/wiki/Great_Firewall)
- [expr](https://github.com/expr-lang/expr) に基づく強力なルールエンジン
- ルールのホットリロード (`SIGHUP` を送信してリロード)
- 柔軟なアナライザ&モディファイアフレームワーク
- 拡張可能な IO 実装(今のところ NFQueue のみ)
- 拡張可能な IO 実装 (今のところ NFQueue のみ)
- [WIP] ウェブ UI
## ユースケース
@ -37,90 +38,4 @@ OpenGFW は、Linux 上の [GFW](https://en.wikipedia.org/wiki/Great_Firewall)
- マルウェア対策
- VPN/プロキシサービスの不正利用防止
- トラフィック分析(ログのみモード)
## 使用方法
### ビルド
```shell
go build
```
### 実行
```shell
export OPENGFW_LOG_LEVEL=debug
./OpenGFW -c config.yaml rules.yaml
```
### 設定例
```yaml
io:
queueSize: 1024
local: true # FORWARD チェーンで OpenGFW を実行したい場合は false に設定する
workers:
count: 4
queueSize: 16
tcpMaxBufferedPagesTotal: 4096
tcpMaxBufferedPagesPerConn: 64
udpMaxStreams: 4096
```
### ルール例
サポートされているすべてのプロトコルと、それぞれのプロトコルがどのようなフィールドを持っているかについてのドキュメントはまだ準備できておりません。
一旦は、"analyzer "ディレクトリの下にあるコードを直接チェックする必要があります。
式言語の構文については、[Expr 言語定義](https://expr-lang.org/docs/language-definition)を参照してください。
```yaml
- name: block v2ex http
action: block
expr: string(http?.req?.headers?.host) endsWith "v2ex.com"
- name: block v2ex https
action: block
expr: string(tls?.req?.sni) endsWith "v2ex.com"
- name: block shadowsocks
action: block
expr: fet != nil && fet.yes
- name: block trojan
action: block
expr: trojan != nil && trojan.yes
- name: v2ex dns poisoning
action: modify
modifier:
name: dns
args:
a: "0.0.0.0"
aaaa: "::"
expr: dns != nil && dns.qr && any(dns.questions, {.name endsWith "v2ex.com"})
- 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")
```
#### サポートされるアクション
- `allow`: 接続を許可し、それ以上の処理は行わない。
- `block`: 接続をブロックし。
- `drop`: UDP の場合、ルールのトリガーとなったパケットをドロップし、同じフローに含まれる以降のパケットの処理を継続する。TCP の場合は、`block` と同じ。
- `modify`: UDP の場合、与えられた修飾子を使って、ルールをトリガしたパケットを修正し、同じフロー内の今後のパケットを処理し続ける。TCP の場合は、`allow` と同じ。
- 独裁的な野心を実現するのを助ける

111
README.md
View File

@ -1,5 +1,6 @@
# ![OpenGFW](docs/logo.png)
[![Quality check status](https://github.com/apernet/OpenGFW/actions/workflows/check.yaml/badge.svg)](https://github.com/apernet/OpenGFW/actions/workflows/check.yaml)
[![License][1]][2]
[1]: https://img.shields.io/badge/License-MPL_2.0-brightgreen.svg
@ -8,22 +9,22 @@
**[中文文档](README.zh.md)**
**[日本語ドキュメント](README.ja.md)**
OpenGFW is a flexible, easy-to-use, open source implementation of [GFW](https://en.wikipedia.org/wiki/Great_Firewall) on
Linux that's in many ways more powerful than the real thing. It's cyber sovereignty you can have on a home router.
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.
**Documentation site: https://gfw.dev/**
Telegram group: https://t.me/OpGFW
> [!CAUTION]
> This project is still in very early stages of development. Use at your own risk.
> [!NOTE]
> We are looking for contributors to help us with this project, especially implementing analyzers for more protocols!!!
> This project is still in very early stages of development. Use at your own risk. We are looking for contributors to help us improve and expand the project.
## Features
- Full IP/TCP reassembly, various protocol analyzers
- HTTP, TLS, DNS, SSH, SOCKS4/5, WireGuard, and many more to come
- "Fully encrypted traffic" detection for Shadowsocks,
etc. (https://gfw.report/publications/usenixsecurity23/data/paper/paper.pdf)
- Trojan (proxy protocol) detection based on Trojan-killer (https://github.com/XTLS/Trojan-killer)
- HTTP, TLS, QUIC, DNS, SSH, SOCKS4/5, WireGuard, OpenVPN, and many more to come
- "Fully encrypted traffic" detection for Shadowsocks, VMess,
etc. (https://gfw.report/publications/usenixsecurity23/en/)
- Trojan (proxy protocol) detection
- [WIP] Machine learning based traffic classification
- Full IPv4 and IPv6 support
- Flow-based multicore load balancing
@ -41,92 +42,4 @@ Linux that's in many ways more powerful than the real thing. It's cyber sovereig
- Malware protection
- Abuse prevention for VPN/proxy services
- Traffic analysis (log only mode)
## Usage
### Build
```shell
go build
```
### Run
```shell
export OPENGFW_LOG_LEVEL=debug
./OpenGFW -c config.yaml rules.yaml
```
### Example config
```yaml
io:
queueSize: 1024
local: true # set to false if you want to run OpenGFW on FORWARD chain
workers:
count: 4
queueSize: 16
tcpMaxBufferedPagesTotal: 4096
tcpMaxBufferedPagesPerConn: 64
udpMaxStreams: 4096
```
### Example rules
[Analyzer properties](docs/Analyzers.md)
For syntax of the expression language, please refer
to [Expr Language Definition](https://expr-lang.org/docs/language-definition).
```yaml
- name: block v2ex http
action: block
expr: string(http?.req?.headers?.host) endsWith "v2ex.com"
- name: block v2ex https
action: block
expr: string(tls?.req?.sni) endsWith "v2ex.com"
- name: block shadowsocks
action: block
expr: fet != nil && fet.yes
- name: block trojan
action: block
expr: trojan != nil && trojan.yes
- name: v2ex dns poisoning
action: modify
modifier:
name: dns
args:
a: "0.0.0.0"
aaaa: "::"
expr: dns != nil && dns.qr && any(dns.questions, {.name endsWith "v2ex.com"})
- 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")
```
#### Supported actions
- `allow`: Allow the connection, no further processing.
- `block`: Block the connection, no further processing.
- `drop`: For UDP, drop the packet that triggered the rule, continue processing future packets in the same flow. For
TCP, same as `block`.
- `modify`: For UDP, modify the packet that triggered the rule using the given modifier, continue processing future
packets in the same flow. For TCP, same as `allow`.
- Help you fulfill your dictatorial ambitions

View File

@ -1,25 +1,26 @@
# ![OpenGFW](docs/logo.png)
[![Quality check status](https://github.com/apernet/OpenGFW/actions/workflows/check.yaml/badge.svg)](https://github.com/apernet/OpenGFW/actions/workflows/check.yaml)
[![License][1]][2]
[1]: https://img.shields.io/badge/License-MPL_2.0-brightgreen.svg
[2]: LICENSE
OpenGFW 是一个 Linux 上灵活、易用、开源的 [GFW](https://zh.wikipedia.org/wiki/%E9%98%B2%E7%81%AB%E9%95%BF%E5%9F%8E)
实现,并且在许多方面比真正的 GFW 更强大。可以部署在家用路由器上的网络主权。
OpenGFW 是一个 Linux 上灵活、易用、开源的 DIY [GFW](https://zh.wikipedia.org/wiki/%E9%98%B2%E7%81%AB%E9%95%BF%E5%9F%8E) 实现,并且在许多方面比真正的 GFW 更强大。为何让那些掌权者独享乐趣?是时候把权力归还给人民,人人有墙建了。立即安装可以部署在家用路由器上的网络主权 - 你也能是老大哥。
**文档网站: https://gfw.dev/**
Telegram 群组: https://t.me/OpGFW
> [!CAUTION]
> 本项目仍处于早期开发阶段。测试时自行承担风险。
> [!NOTE]
> 我们正在寻求贡献者一起完善本项目,尤其是实现更多协议的解析器!
> 本项目仍处于早期开发阶段。测试时自行承担风险。我们正在寻求贡献者一起完善本项目。
## 功能
- 完整的 IP/TCP 重组,各种协议解析器
- HTTP, TLS, DNS, SSH, SOCKS4/5, WireGuard, 更多协议正在开发中
- Shadowsocks 等 "全加密流量" 检测 (https://gfw.report/publications/usenixsecurity23/data/paper/paper.pdf)
- 基于 Trojan-killer 的 Trojan 检测 (https://github.com/XTLS/Trojan-killer)
- HTTP, TLS, QUIC, DNS, SSH, SOCKS4/5, WireGuard, OpenVPN, 更多协议正在开发中
- Shadowsocks, VMess 等 "全加密流量" 检测 (https://gfw.report/publications/usenixsecurity23/zh/)
- Trojan 协议检测
- [开发中] 基于机器学习的流量分类
- 同等支持 IPv4 和 IPv6
- 基于流的多核负载均衡
@ -37,89 +38,4 @@ OpenGFW 是一个 Linux 上灵活、易用、开源的 [GFW](https://zh.wikipedi
- 恶意软件防护
- VPN/代理服务滥用防护
- 流量分析 (纯日志模式)
## 使用
### 构建
```shell
go build
```
### 运行
```shell
export OPENGFW_LOG_LEVEL=debug
./OpenGFW -c config.yaml rules.yaml
```
### 样例配置
```yaml
io:
queueSize: 1024
local: true # 如果需要在 FORWARD 链上运行 OpenGFW请设置为 false
workers:
count: 4
queueSize: 16
tcpMaxBufferedPagesTotal: 4096
tcpMaxBufferedPagesPerConn: 64
udpMaxStreams: 4096
```
### 样例规则
[解析器属性](docs/Analyzers.md)
规则的语法请参考 [Expr Language Definition](https://expr-lang.org/docs/language-definition)。
```yaml
- name: block v2ex http
action: block
expr: string(http?.req?.headers?.host) endsWith "v2ex.com"
- name: block v2ex https
action: block
expr: string(tls?.req?.sni) endsWith "v2ex.com"
- name: block shadowsocks
action: block
expr: fet != nil && fet.yes
- name: block trojan
action: block
expr: trojan != nil && trojan.yes
- name: v2ex dns poisoning
action: modify
modifier:
name: dns
args:
a: "0.0.0.0"
aaaa: "::"
expr: dns != nil && dns.qr && any(dns.questions, {.name endsWith "v2ex.com"})
- 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")
```
#### 支持的 action
- `allow`: 放行连接,不再处理后续的包。
- `block`: 阻断连接,不再处理后续的包。
- `drop`: 对于 UDP丢弃触发规则的包但继续处理同一流中的后续包。对于 TCP效果同 `block`
- `modify`: 对于 UDP用指定的修改器修改触发规则的包然后继续处理同一流中的后续包。对于 TCP效果同 `allow`
- 助你实现你的独裁野心

224
analyzer/internal/tls.go Normal file
View File

@ -0,0 +1,224 @@
package internal
import (
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/analyzer/utils"
)
// TLS record types.
const (
RecordTypeHandshake = 0x16
)
// TLS handshake message types.
const (
TypeClientHello = 0x01
TypeServerHello = 0x02
)
// TLS extension numbers.
const (
extServerName = 0x0000
extALPN = 0x0010
extSupportedVersions = 0x002b
extEncryptedClientHello = 0xfe0d
)
func ParseTLSClientHelloMsgData(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 ParseTLSServerHelloMsgData(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 extServerName:
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 extALPN:
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 extSupportedVersions:
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 extEncryptedClientHello:
// We can't parse ECH for now, just set a flag
m["ech"] = true
}
return true
}

View File

@ -143,8 +143,11 @@ func isTLSorHTTP(bytes []byte) bool {
if len(bytes) < 3 {
return false
}
if bytes[0] == 0x16 && bytes[1] == 0x03 && bytes[2] <= 0x03 {
// TLS handshake for TLS 1.0-1.3
// "We observe that the GFW exempts any connection whose first
// 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
}
// HTTP request

64
analyzer/tcp/http_test.go Normal file
View File

@ -0,0 +1,64 @@
package tcp
import (
"reflect"
"strings"
"testing"
"github.com/apernet/OpenGFW/analyzer"
)
func TestHTTPParsing_Request(t *testing.T) {
testCases := map[string]analyzer.PropMap{
"GET / HTTP/1.1\r\n": {
"method": "GET", "path": "/", "version": "HTTP/1.1",
},
"POST /hello?a=1&b=2 HTTP/1.0\r\n": {
"method": "POST", "path": "/hello?a=1&b=2", "version": "HTTP/1.0",
},
"PUT /world HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody": {
"method": "PUT", "path": "/world", "version": "HTTP/1.1", "headers": analyzer.PropMap{"content-length": "4"},
},
"DELETE /goodbye HTTP/2.0\r\n": {
"method": "DELETE", "path": "/goodbye", "version": "HTTP/2.0",
},
}
for tc, want := range testCases {
t.Run(strings.Split(tc, " ")[0], func(t *testing.T) {
tc, want := tc, want
t.Parallel()
u, _ := newHTTPStream(nil).Feed(false, false, false, 0, []byte(tc))
got := u.M.Get("req")
if !reflect.DeepEqual(got, want) {
t.Errorf("\"%s\" parsed = %v, want %v", tc, got, want)
}
})
}
}
func TestHTTPParsing_Response(t *testing.T) {
testCases := map[string]analyzer.PropMap{
"HTTP/1.0 200 OK\r\nContent-Length: 4\r\n\r\nbody": {
"version": "HTTP/1.0", "status": 200,
"headers": analyzer.PropMap{"content-length": "4"},
},
"HTTP/2.0 204 No Content\r\n\r\n": {
"version": "HTTP/2.0", "status": 204,
},
}
for tc, want := range testCases {
t.Run(strings.Split(tc, " ")[0], func(t *testing.T) {
tc, want := tc, want
t.Parallel()
u, _ := newHTTPStream(nil).Feed(true, false, false, 0, []byte(tc))
got := u.M.Get("resp")
if !reflect.DeepEqual(got, want) {
t.Errorf("\"%s\" parsed = %v, want %v", tc, got, want)
}
})
}
}

View File

@ -208,10 +208,10 @@ func (s *socksStream) parseSocks5ReqMethod() utils.LSMAction {
switch method {
case Socks5AuthNotRequired:
s.authReqMethod = Socks5AuthNotRequired
break
return utils.LSMActionNext
case Socks5AuthPassword:
s.authReqMethod = Socks5AuthPassword
break
return utils.LSMActionNext
default:
// TODO: more auth method to support
}

View File

@ -2,6 +2,7 @@ package tcp
import (
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/analyzer/internal"
"github.com/apernet/OpenGFW/analyzer/utils"
)
@ -43,12 +44,12 @@ type tlsStream struct {
func newTLSStream(logger analyzer.Logger) *tlsStream {
s := &tlsStream{logger: logger, reqBuf: &utils.ByteBuffer{}, respBuf: &utils.ByteBuffer{}}
s.reqLSM = utils.NewLinearStateMachine(
s.tlsClientHelloSanityCheck,
s.parseClientHello,
s.tlsClientHelloPreprocess,
s.parseClientHelloData,
)
s.respLSM = utils.NewLinearStateMachine(
s.tlsServerHelloSanityCheck,
s.parseServerHello,
s.tlsServerHelloPreprocess,
s.parseServerHelloData,
)
return s
}
@ -88,261 +89,132 @@ func (s *tlsStream) Feed(rev, start, end bool, skip int, data []byte) (u *analyz
return update, cancelled || (s.reqDone && s.respDone)
}
func (s *tlsStream) tlsClientHelloSanityCheck() utils.LSMAction {
data, ok := s.reqBuf.Get(9, true)
// tlsClientHelloPreprocess validates ClientHello message.
//
// During validation, message header and first handshake header may be removed
// from `s.reqBuf`.
func (s *tlsStream) tlsClientHelloPreprocess() utils.LSMAction {
// headers size: content type (1 byte) + legacy protocol version (2 bytes) +
// + content length (2 bytes) + message type (1 byte) +
// + handshake length (3 bytes)
const headersSize = 9
// minimal data size: protocol version (2 bytes) + random (32 bytes) +
// + session ID (1 byte) + cipher suites (4 bytes) +
// + compression methods (2 bytes) + no extensions
const minDataSize = 41
header, ok := s.reqBuf.Get(headersSize, true)
if !ok {
// not a full header yet
return utils.LSMActionPause
}
if data[0] != 0x16 || data[5] != 0x01 {
// Not a TLS handshake, or not a client hello
if header[0] != internal.RecordTypeHandshake || header[5] != internal.TypeClientHello {
return utils.LSMActionCancel
}
s.clientHelloLen = int(data[6])<<16 | int(data[7])<<8 | int(data[8])
if s.clientHelloLen < 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.clientHelloLen = int(header[6])<<16 | int(header[7])<<8 | int(header[8])
if s.clientHelloLen < minDataSize {
return utils.LSMActionCancel
}
// TODO: something is missing. See:
// const messageHeaderSize = 4
// fullMessageLen := int(header[3])<<8 | int(header[4])
// msgNo := fullMessageLen / int(messageHeaderSize+s.serverHelloLen)
// if msgNo != 1 {
// // what here?
// }
// if messageNo != int(messageNo) {
// // what here?
// }
return utils.LSMActionNext
}
func (s *tlsStream) tlsServerHelloSanityCheck() utils.LSMAction {
data, ok := s.respBuf.Get(9, true)
// tlsServerHelloPreprocess validates ServerHello message.
//
// During validation, message header and first handshake header may be removed
// from `s.reqBuf`.
func (s *tlsStream) tlsServerHelloPreprocess() utils.LSMAction {
// header size: content type (1 byte) + legacy protocol version (2 byte) +
// + content length (2 byte) + message type (1 byte) +
// + handshake length (3 byte)
const headersSize = 9
// minimal data size: server version (2 byte) + random (32 byte) +
// + session ID (>=1 byte) + cipher suite (2 byte) +
// + compression method (1 byte) + no extensions
const minDataSize = 38
header, ok := s.respBuf.Get(headersSize, true)
if !ok {
// not a full header yet
return utils.LSMActionPause
}
if data[0] != 0x16 || data[5] != 0x02 {
// Not a TLS handshake, or not a server hello
if header[0] != internal.RecordTypeHandshake || header[5] != internal.TypeServerHello {
return utils.LSMActionCancel
}
s.serverHelloLen = int(data[6])<<16 | int(data[7])<<8 | int(data[8])
if s.serverHelloLen < 38 {
// 2 (Protocol Version) +
// 32 (Random) +
// 1 (Session ID Length) +
// 2 (Cipher Suite) +
// 1 (Compression Method) +
// No extensions
// This should be the bare minimum for a server hello
s.serverHelloLen = int(header[6])<<16 | int(header[7])<<8 | int(header[8])
if s.serverHelloLen < minDataSize {
return utils.LSMActionCancel
}
// TODO: something is missing. See example:
// const messageHeaderSize = 4
// fullMessageLen := int(header[3])<<8 | int(header[4])
// msgNo := fullMessageLen / int(messageHeaderSize+s.serverHelloLen)
// if msgNo != 1 {
// // what here?
// }
// if messageNo != int(messageNo) {
// // what here?
// }
return utils.LSMActionNext
}
func (s *tlsStream) parseClientHello() utils.LSMAction {
// parseClientHelloData converts valid ClientHello message data (without
// headers) into `analyzer.PropMap`.
//
// Parsing error may leave `s.reqBuf` in an unusable state.
func (s *tlsStream) parseClientHelloData() utils.LSMAction {
chBuf, ok := s.reqBuf.GetSubBuffer(s.clientHelloLen, true)
if !ok {
// 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.ParseTLSClientHelloMsgData(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 {
// parseServerHelloData converts valid ServerHello message data (without
// headers) into `analyzer.PropMap`.
//
// Parsing error may leave `s.respBuf` in an unusable state.
func (s *tlsStream) parseServerHelloData() utils.LSMAction {
shBuf, ok := s.respBuf.GetSubBuffer(s.serverHelloLen, true)
if !ok {
// 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.ParseTLSServerHelloMsgData(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 {

69
analyzer/tcp/tls_test.go Normal file
View File

@ -0,0 +1,69 @@
package tcp
import (
"reflect"
"testing"
"github.com/apernet/OpenGFW/analyzer"
)
func TestTlsStreamParsing_ClientHello(t *testing.T) {
// example packet taken from <https://tls12.xargs.org/#client-hello/annotated>
clientHello := []byte{
0x16, 0x03, 0x01, 0x00, 0xa5, 0x01, 0x00, 0x00, 0xa1, 0x03, 0x03, 0x00,
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c,
0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x00, 0x00, 0x20, 0xcc, 0xa8,
0xcc, 0xa9, 0xc0, 0x2f, 0xc0, 0x30, 0xc0, 0x2b, 0xc0, 0x2c, 0xc0, 0x13,
0xc0, 0x09, 0xc0, 0x14, 0xc0, 0x0a, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f,
0x00, 0x35, 0xc0, 0x12, 0x00, 0x0a, 0x01, 0x00, 0x00, 0x58, 0x00, 0x00,
0x00, 0x18, 0x00, 0x16, 0x00, 0x00, 0x13, 0x65, 0x78, 0x61, 0x6d, 0x70,
0x6c, 0x65, 0x2e, 0x75, 0x6c, 0x66, 0x68, 0x65, 0x69, 0x6d, 0x2e, 0x6e,
0x65, 0x74, 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0a, 0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00,
0x19, 0x00, 0x0b, 0x00, 0x02, 0x01, 0x00, 0x00, 0x0d, 0x00, 0x12, 0x00,
0x10, 0x04, 0x01, 0x04, 0x03, 0x05, 0x01, 0x05, 0x03, 0x06, 0x01, 0x06,
0x03, 0x02, 0x01, 0x02, 0x03, 0xff, 0x01, 0x00, 0x01, 0x00, 0x00, 0x12,
0x00, 0x00,
}
want := analyzer.PropMap{
"ciphers": []uint16{52392, 52393, 49199, 49200, 49195, 49196, 49171, 49161, 49172, 49162, 156, 157, 47, 53, 49170, 10},
"compression": []uint8{0},
"random": []uint8{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31},
"session": []uint8{},
"sni": "example.ulfheim.net",
"version": uint16(771),
}
s := newTLSStream(nil)
u, _ := s.Feed(false, false, false, 0, clientHello)
got := u.M.Get("req")
if !reflect.DeepEqual(got, want) {
t.Errorf("%d B parsed = %v, want %v", len(clientHello), got, want)
}
}
func TestTlsStreamParsing_ServerHello(t *testing.T) {
// example packet taken from <https://tls12.xargs.org/#server-hello/annotated>
serverHello := []byte{
0x16, 0x03, 0x03, 0x00, 0x31, 0x02, 0x00, 0x00, 0x2d, 0x03, 0x03, 0x70,
0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c,
0x7d, 0x7e, 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88,
0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 0x00, 0xc0, 0x13, 0x00, 0x00,
0x05, 0xff, 0x01, 0x00, 0x01, 0x00,
}
want := analyzer.PropMap{
"cipher": uint16(49171),
"compression": uint8(0),
"random": []uint8{112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143},
"session": []uint8{},
"version": uint16(771),
}
s := newTLSStream(nil)
u, _ := s.Feed(true, false, false, 0, serverHello)
got := u.M.Get("resp")
if !reflect.DeepEqual(got, want) {
t.Errorf("%d B parsed = %v, want %v", len(serverHello), got, want)
}
}

View File

@ -9,22 +9,15 @@ import (
var _ analyzer.TCPAnalyzer = (*TrojanAnalyzer)(nil)
// CCS stands for "Change Cipher Spec"
var trojanCCS = []byte{20, 3, 3, 0, 1, 1}
var ccsPattern = []byte{20, 3, 3, 0, 1, 1}
const (
trojanUpLB = 650
trojanUpUB = 1000
trojanDownLB1 = 170
trojanDownUB1 = 180
trojanDownLB2 = 3000
trojanDownUB2 = 7500
)
// TrojanAnalyzer uses a very simple packet length based check to determine
// if a TLS connection is actually the Trojan proxy protocol.
// The algorithm is from the following project, with small modifications:
// https://github.com/XTLS/Trojan-killer
// Warning: Experimental only. This method is known to have significant false positives and false negatives.
// TrojanAnalyzer uses length-based heuristics to detect Trojan traffic based on
// its "TLS-in-TLS" nature. The heuristics are trained using a decision tree with
// about 20k Trojan samples and 30k non-Trojan samples. The tree is then converted
// to code using a custom tool and inlined here (isTrojanSeq function).
// Accuracy: 1% false positive rate, 10% false negative rate.
// We do NOT recommend directly blocking all positive connections, as this may
// break legitimate TLS connections.
type TrojanAnalyzer struct{}
func (a *TrojanAnalyzer) Name() string {
@ -32,7 +25,7 @@ func (a *TrojanAnalyzer) Name() string {
}
func (a *TrojanAnalyzer) Limit() int {
return 16384
return 512000
}
func (a *TrojanAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) analyzer.TCPStream {
@ -40,10 +33,12 @@ func (a *TrojanAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) a
}
type trojanStream struct {
logger analyzer.Logger
active bool
upCount int
downCount int
logger analyzer.Logger
first bool
count bool
rev bool
seq [4]int
seqIndex int
}
func newTrojanStream(logger analyzer.Logger) *trojanStream {
@ -57,35 +52,466 @@ func (s *trojanStream) Feed(rev, start, end bool, skip int, data []byte) (u *ana
if len(data) == 0 {
return nil, false
}
if !rev && !s.active && len(data) >= 6 && bytes.Equal(data[:6], trojanCCS) {
// Client CCS encountered, start counting
s.active = true
if s.first {
s.first = false
// Stop if it's not a valid TLS connection
if !(!rev && len(data) >= 3 && data[0] >= 0x16 && data[0] <= 0x17 &&
data[1] == 0x03 && data[2] <= 0x09) {
return nil, true
}
}
if s.active {
if rev {
// Down direction
s.downCount += len(data)
if !rev && !s.count && len(data) >= 6 && bytes.Equal(data[:6], ccsPattern) {
// Client Change Cipher Spec encountered, start counting
s.count = true
}
if s.count {
if rev == s.rev {
// Same direction as last time, just update the number
s.seq[s.seqIndex] += len(data)
} else {
// Up direction
if s.upCount >= trojanUpLB && s.upCount <= trojanUpUB &&
((s.downCount >= trojanDownLB1 && s.downCount <= trojanDownUB1) ||
(s.downCount >= trojanDownLB2 && s.downCount <= trojanDownUB2)) {
// Different direction, bump the index
s.seqIndex += 1
if s.seqIndex == 4 {
return &analyzer.PropUpdate{
Type: analyzer.PropUpdateReplace,
M: analyzer.PropMap{
"up": s.upCount,
"down": s.downCount,
"yes": true,
"seq": s.seq,
"yes": isTrojanSeq(s.seq),
},
}, true
}
s.upCount += len(data)
s.seq[s.seqIndex] += len(data)
s.rev = rev
}
}
// Give up when either direction is over the limit
return nil, s.upCount > trojanUpUB || s.downCount > trojanDownUB2
return nil, false
}
func (s *trojanStream) Close(limited bool) *analyzer.PropUpdate {
return nil
}
func isTrojanSeq(seq [4]int) bool {
length1 := seq[0]
length2 := seq[1]
length3 := seq[2]
length4 := seq[3]
if length2 <= 2431 {
if length2 <= 157 {
if length1 <= 156 {
if length3 <= 108 {
return false
} else {
return false
}
} else {
if length1 <= 892 {
if length3 <= 40 {
return false
} else {
if length3 <= 788 {
if length4 <= 185 {
if length1 <= 411 {
return true
} else {
return false
}
} else {
if length2 <= 112 {
return false
} else {
return true
}
}
} else {
if length3 <= 1346 {
if length1 <= 418 {
return false
} else {
return true
}
} else {
return false
}
}
}
} else {
if length2 <= 120 {
if length2 <= 63 {
return false
} else {
if length4 <= 653 {
return false
} else {
return false
}
}
} else {
return false
}
}
}
} else {
if length1 <= 206 {
if length1 <= 185 {
if length1 <= 171 {
return false
} else {
if length4 <= 211 {
return false
} else {
return false
}
}
} else {
if length2 <= 251 {
return true
} else {
return false
}
}
} else {
if length2 <= 286 {
if length1 <= 1123 {
if length3 <= 70 {
return false
} else {
if length1 <= 659 {
if length3 <= 370 {
return true
} else {
return false
}
} else {
if length4 <= 272 {
return false
} else {
return true
}
}
}
} else {
if length4 <= 537 {
if length2 <= 276 {
if length3 <= 1877 {
return false
} else {
return false
}
} else {
return false
}
} else {
if length1 <= 1466 {
if length1 <= 1435 {
return false
} else {
return true
}
} else {
if length2 <= 193 {
return false
} else {
return false
}
}
}
}
} else {
if length1 <= 284 {
if length1 <= 277 {
if length2 <= 726 {
return false
} else {
if length2 <= 768 {
return true
} else {
return false
}
}
} else {
if length2 <= 782 {
if length4 <= 783 {
return true
} else {
return false
}
} else {
return false
}
}
} else {
if length2 <= 492 {
if length2 <= 396 {
if length2 <= 322 {
return false
} else {
return false
}
} else {
if length4 <= 971 {
return false
} else {
return true
}
}
} else {
if length2 <= 2128 {
if length2 <= 1418 {
return false
} else {
return false
}
} else {
if length3 <= 103 {
return false
} else {
return false
}
}
}
}
}
}
}
} else {
if length2 <= 6232 {
if length3 <= 85 {
if length2 <= 3599 {
return false
} else {
if length1 <= 613 {
return false
} else {
return false
}
}
} else {
if length3 <= 220 {
if length4 <= 1173 {
if length1 <= 874 {
if length4 <= 337 {
if length4 <= 68 {
return true
} else {
return true
}
} else {
if length1 <= 667 {
return true
} else {
return true
}
}
} else {
if length3 <= 108 {
if length1 <= 1930 {
return true
} else {
return true
}
} else {
if length2 <= 5383 {
return false
} else {
return true
}
}
}
} else {
return false
}
} else {
if length1 <= 664 {
if length3 <= 411 {
if length3 <= 383 {
if length4 <= 346 {
return true
} else {
return false
}
} else {
if length1 <= 445 {
return true
} else {
return false
}
}
} else {
if length2 <= 3708 {
if length4 <= 307 {
return true
} else {
return false
}
} else {
if length2 <= 4656 {
return false
} else {
return false
}
}
}
} else {
if length1 <= 1055 {
if length3 <= 580 {
if length1 <= 724 {
return true
} else {
return false
}
} else {
if length1 <= 678 {
return false
} else {
return true
}
}
} else {
if length2 <= 5352 {
if length3 <= 1586 {
return false
} else {
return false
}
} else {
if length4 <= 2173 {
return true
} else {
return false
}
}
}
}
}
}
} else {
if length2 <= 9408 {
if length1 <= 670 {
if length4 <= 76 {
if length3 <= 175 {
return true
} else {
return true
}
} else {
if length2 <= 9072 {
if length3 <= 314 {
if length3 <= 179 {
return false
} else {
return false
}
} else {
if length4 <= 708 {
return false
} else {
return false
}
}
} else {
return true
}
}
} else {
if length1 <= 795 {
if length2 <= 6334 {
if length2 <= 6288 {
return true
} else {
return false
}
} else {
if length4 <= 6404 {
if length2 <= 8194 {
return true
} else {
return true
}
} else {
if length2 <= 8924 {
return false
} else {
return true
}
}
}
} else {
if length3 <= 732 {
if length1 <= 1397 {
if length3 <= 179 {
return false
} else {
return false
}
} else {
if length1 <= 1976 {
return false
} else {
return false
}
}
} else {
if length1 <= 2840 {
if length1 <= 2591 {
return false
} else {
return true
}
} else {
return false
}
}
}
}
} else {
if length4 <= 30 {
return false
} else {
if length2 <= 13314 {
if length4 <= 1786 {
if length2 <= 13018 {
if length4 <= 869 {
return false
} else {
return false
}
} else {
return true
}
} else {
if length3 <= 775 {
return false
} else {
return false
}
}
} else {
if length4 <= 73 {
return false
} else {
if length3 <= 640 {
if length3 <= 237 {
return false
} else {
return false
}
} else {
if length2 <= 43804 {
return false
} else {
return false
}
}
}
}
}
}
}
}
}

View 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.

View File

@ -0,0 +1 @@
The code here is from https://github.com/cuonglm/quicsni with various modifications.

View 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
}

View 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
}

View 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)
}

View 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
}

View 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
}

384
analyzer/udp/openvpn.go Normal file
View File

@ -0,0 +1,384 @@
package udp
import (
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/analyzer/utils"
)
var (
_ analyzer.UDPAnalyzer = (*OpenVPNAnalyzer)(nil)
_ analyzer.TCPAnalyzer = (*OpenVPNAnalyzer)(nil)
)
var (
_ analyzer.UDPStream = (*openvpnUDPStream)(nil)
_ analyzer.TCPStream = (*openvpnTCPStream)(nil)
)
// Ref paper:
// https://www.usenix.org/system/files/sec22fall_xue-diwen.pdf
// OpenVPN Opcodes definitions from:
// https://github.com/OpenVPN/openvpn/blob/master/src/openvpn/ssl_pkt.h
const (
OpenVPNControlHardResetClientV1 = 1
OpenVPNControlHardResetServerV1 = 2
OpenVPNControlSoftResetV1 = 3
OpenVPNControlV1 = 4
OpenVPNAckV1 = 5
OpenVPNDataV1 = 6
OpenVPNControlHardResetClientV2 = 7
OpenVPNControlHardResetServerV2 = 8
OpenVPNDataV2 = 9
OpenVPNControlHardResetClientV3 = 10
OpenVPNControlWkcV1 = 11
)
const (
OpenVPNMinPktLen = 6
OpenVPNTCPPktDefaultLimit = 256
OpenVPNUDPPktDefaultLimit = 256
)
type OpenVPNAnalyzer struct{}
func (a *OpenVPNAnalyzer) Name() string {
return "openvpn"
}
func (a *OpenVPNAnalyzer) Limit() int {
return 0
}
func (a *OpenVPNAnalyzer) NewUDP(info analyzer.UDPInfo, logger analyzer.Logger) analyzer.UDPStream {
return newOpenVPNUDPStream(logger)
}
func (a *OpenVPNAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) analyzer.TCPStream {
return newOpenVPNTCPStream(logger)
}
type openvpnPkt struct {
pktLen uint16 // 16 bits, TCP proto only
opcode byte // 5 bits
_keyId byte // 3 bits, not used
// We don't care about the rest of the packet
// payload []byte
}
type openvpnStream struct {
logger analyzer.Logger
reqUpdated bool
reqLSM *utils.LinearStateMachine
reqDone bool
respUpdated bool
respLSM *utils.LinearStateMachine
respDone bool
rxPktCnt int
txPktCnt int
pktLimit int
reqPktParse func() (*openvpnPkt, utils.LSMAction)
respPktParse func() (*openvpnPkt, utils.LSMAction)
lastOpcode byte
}
func (o *openvpnStream) parseCtlHardResetClient() utils.LSMAction {
pkt, action := o.reqPktParse()
if action != utils.LSMActionNext {
return action
}
if pkt.opcode != OpenVPNControlHardResetClientV1 &&
pkt.opcode != OpenVPNControlHardResetClientV2 &&
pkt.opcode != OpenVPNControlHardResetClientV3 {
return utils.LSMActionCancel
}
o.lastOpcode = pkt.opcode
return utils.LSMActionNext
}
func (o *openvpnStream) parseCtlHardResetServer() utils.LSMAction {
if o.lastOpcode != OpenVPNControlHardResetClientV1 &&
o.lastOpcode != OpenVPNControlHardResetClientV2 &&
o.lastOpcode != OpenVPNControlHardResetClientV3 {
return utils.LSMActionCancel
}
pkt, action := o.respPktParse()
if action != utils.LSMActionNext {
return action
}
if pkt.opcode != OpenVPNControlHardResetServerV1 &&
pkt.opcode != OpenVPNControlHardResetServerV2 {
return utils.LSMActionCancel
}
o.lastOpcode = pkt.opcode
return utils.LSMActionNext
}
func (o *openvpnStream) parseReq() utils.LSMAction {
pkt, action := o.reqPktParse()
if action != utils.LSMActionNext {
return action
}
if pkt.opcode != OpenVPNControlSoftResetV1 &&
pkt.opcode != OpenVPNControlV1 &&
pkt.opcode != OpenVPNAckV1 &&
pkt.opcode != OpenVPNDataV1 &&
pkt.opcode != OpenVPNDataV2 &&
pkt.opcode != OpenVPNControlWkcV1 {
return utils.LSMActionCancel
}
o.txPktCnt += 1
o.reqUpdated = true
return utils.LSMActionPause
}
func (o *openvpnStream) parseResp() utils.LSMAction {
pkt, action := o.respPktParse()
if action != utils.LSMActionNext {
return action
}
if pkt.opcode != OpenVPNControlSoftResetV1 &&
pkt.opcode != OpenVPNControlV1 &&
pkt.opcode != OpenVPNAckV1 &&
pkt.opcode != OpenVPNDataV1 &&
pkt.opcode != OpenVPNDataV2 &&
pkt.opcode != OpenVPNControlWkcV1 {
return utils.LSMActionCancel
}
o.rxPktCnt += 1
o.respUpdated = true
return utils.LSMActionPause
}
type openvpnUDPStream struct {
openvpnStream
curPkt []byte
// We don't introduce `invalidCount` here to decrease the false positive rate
// invalidCount int
}
func newOpenVPNUDPStream(logger analyzer.Logger) *openvpnUDPStream {
s := &openvpnUDPStream{
openvpnStream: openvpnStream{
logger: logger,
pktLimit: OpenVPNUDPPktDefaultLimit,
},
}
s.respPktParse = s.parsePkt
s.reqPktParse = s.parsePkt
s.reqLSM = utils.NewLinearStateMachine(
s.parseCtlHardResetClient,
s.parseReq,
)
s.respLSM = utils.NewLinearStateMachine(
s.parseCtlHardResetServer,
s.parseResp,
)
return s
}
func (o *openvpnUDPStream) Feed(rev bool, data []byte) (u *analyzer.PropUpdate, d bool) {
if len(data) == 0 {
return nil, false
}
var update *analyzer.PropUpdate
var cancelled bool
o.curPkt = data
if rev {
o.respUpdated = false
cancelled, o.respDone = o.respLSM.Run()
if o.respUpdated {
update = &analyzer.PropUpdate{
Type: analyzer.PropUpdateReplace,
M: analyzer.PropMap{"rx_pkt_cnt": o.rxPktCnt, "tx_pkt_cnt": o.txPktCnt},
}
o.respUpdated = false
}
} else {
o.reqUpdated = false
cancelled, o.reqDone = o.reqLSM.Run()
if o.reqUpdated {
update = &analyzer.PropUpdate{
Type: analyzer.PropUpdateReplace,
M: analyzer.PropMap{"rx_pkt_cnt": o.rxPktCnt, "tx_pkt_cnt": o.txPktCnt},
}
o.reqUpdated = false
}
}
return update, cancelled || (o.reqDone && o.respDone) || o.rxPktCnt+o.txPktCnt > o.pktLimit
}
func (o *openvpnUDPStream) Close(limited bool) *analyzer.PropUpdate {
return nil
}
// Parse OpenVPN UDP packet.
func (o *openvpnUDPStream) parsePkt() (p *openvpnPkt, action utils.LSMAction) {
if o.curPkt == nil {
return nil, utils.LSMActionPause
}
if !OpenVPNCheckForValidOpcode(o.curPkt[0] >> 3) {
return nil, utils.LSMActionCancel
}
// Parse packet header
p = &openvpnPkt{}
p.opcode = o.curPkt[0] >> 3
p._keyId = o.curPkt[0] & 0x07
o.curPkt = nil
return p, utils.LSMActionNext
}
type openvpnTCPStream struct {
openvpnStream
reqBuf *utils.ByteBuffer
respBuf *utils.ByteBuffer
}
func newOpenVPNTCPStream(logger analyzer.Logger) *openvpnTCPStream {
s := &openvpnTCPStream{
openvpnStream: openvpnStream{
logger: logger,
pktLimit: OpenVPNTCPPktDefaultLimit,
},
reqBuf: &utils.ByteBuffer{},
respBuf: &utils.ByteBuffer{},
}
s.respPktParse = func() (*openvpnPkt, utils.LSMAction) {
return s.parsePkt(true)
}
s.reqPktParse = func() (*openvpnPkt, utils.LSMAction) {
return s.parsePkt(false)
}
s.reqLSM = utils.NewLinearStateMachine(
s.parseCtlHardResetClient,
s.parseReq,
)
s.respLSM = utils.NewLinearStateMachine(
s.parseCtlHardResetServer,
s.parseResp,
)
return s
}
func (o *openvpnTCPStream) Feed(rev, start, end bool, skip int, data []byte) (u *analyzer.PropUpdate, d bool) {
if skip != 0 {
return nil, true
}
if len(data) == 0 {
return nil, false
}
var update *analyzer.PropUpdate
var cancelled bool
if rev {
o.respBuf.Append(data)
o.respUpdated = false
cancelled, o.respDone = o.respLSM.Run()
if o.respUpdated {
update = &analyzer.PropUpdate{
Type: analyzer.PropUpdateReplace,
M: analyzer.PropMap{"rx_pkt_cnt": o.rxPktCnt, "tx_pkt_cnt": o.txPktCnt},
}
o.respUpdated = false
}
} else {
o.reqBuf.Append(data)
o.reqUpdated = false
cancelled, o.reqDone = o.reqLSM.Run()
if o.reqUpdated {
update = &analyzer.PropUpdate{
Type: analyzer.PropUpdateMerge,
M: analyzer.PropMap{"rx_pkt_cnt": o.rxPktCnt, "tx_pkt_cnt": o.txPktCnt},
}
o.reqUpdated = false
}
}
return update, cancelled || (o.reqDone && o.respDone) || o.rxPktCnt+o.txPktCnt > o.pktLimit
}
func (o *openvpnTCPStream) Close(limited bool) *analyzer.PropUpdate {
o.reqBuf.Reset()
o.respBuf.Reset()
return nil
}
// Parse OpenVPN TCP packet.
func (o *openvpnTCPStream) parsePkt(rev bool) (p *openvpnPkt, action utils.LSMAction) {
var buffer *utils.ByteBuffer
if rev {
buffer = o.respBuf
} else {
buffer = o.reqBuf
}
// Parse packet length
pktLen, ok := buffer.GetUint16(false, false)
if !ok {
return nil, utils.LSMActionPause
}
if pktLen < OpenVPNMinPktLen {
return nil, utils.LSMActionCancel
}
pktOp, ok := buffer.Get(3, false)
if !ok {
return nil, utils.LSMActionPause
}
if !OpenVPNCheckForValidOpcode(pktOp[2] >> 3) {
return nil, utils.LSMActionCancel
}
pkt, ok := buffer.Get(int(pktLen)+2, true)
if !ok {
return nil, utils.LSMActionPause
}
pkt = pkt[2:]
// Parse packet header
p = &openvpnPkt{}
p.pktLen = pktLen
p.opcode = pkt[0] >> 3
p._keyId = pkt[0] & 0x07
return p, utils.LSMActionNext
}
func OpenVPNCheckForValidOpcode(opcode byte) bool {
switch opcode {
case OpenVPNControlHardResetClientV1,
OpenVPNControlHardResetServerV1,
OpenVPNControlSoftResetV1,
OpenVPNControlV1,
OpenVPNAckV1,
OpenVPNDataV1,
OpenVPNControlHardResetClientV2,
OpenVPNControlHardResetServerV2,
OpenVPNDataV2,
OpenVPNControlHardResetClientV3,
OpenVPNControlWkcV1:
return true
}
return false
}

81
analyzer/udp/quic.go Normal file
View File

@ -0,0 +1,81 @@
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) {
// minimal data size: protocol version (2 bytes) + random (32 bytes) +
// + session ID (1 byte) + cipher suites (4 bytes) +
// + compression methods (2 bytes) + no extensions
const minDataSize = 41
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 { // FIXME: isn't length checked inside quic.ReadCryptoPayload? Also, what about error handling?
s.invalidCount++
return nil, s.invalidCount >= quicInvalidCountThreshold
}
if pl[0] != internal.TypeClientHello {
s.invalidCount++
return nil, s.invalidCount >= quicInvalidCountThreshold
}
chLen := int(pl[1])<<16 | int(pl[2])<<8 | int(pl[3])
if chLen < minDataSize {
s.invalidCount++
return nil, s.invalidCount >= quicInvalidCountThreshold
}
m := internal.ParseTLSClientHelloMsgData(&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
}

58
analyzer/udp/quic_test.go Normal file
View File

@ -0,0 +1,58 @@
package udp
import (
"reflect"
"testing"
"github.com/apernet/OpenGFW/analyzer"
)
func TestQuicStreamParsing_ClientHello(t *testing.T) {
// example packet taken from <https://quic.xargs.org/#client-initial-packet/annotated>
clientHello := make([]byte, 1200)
clientInitial := []byte{
0xcd, 0x00, 0x00, 0x00, 0x01, 0x08, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
0x06, 0x07, 0x05, 0x63, 0x5f, 0x63, 0x69, 0x64, 0x00, 0x41, 0x03, 0x98,
0x1c, 0x36, 0xa7, 0xed, 0x78, 0x71, 0x6b, 0xe9, 0x71, 0x1b, 0xa4, 0x98,
0xb7, 0xed, 0x86, 0x84, 0x43, 0xbb, 0x2e, 0x0c, 0x51, 0x4d, 0x4d, 0x84,
0x8e, 0xad, 0xcc, 0x7a, 0x00, 0xd2, 0x5c, 0xe9, 0xf9, 0xaf, 0xa4, 0x83,
0x97, 0x80, 0x88, 0xde, 0x83, 0x6b, 0xe6, 0x8c, 0x0b, 0x32, 0xa2, 0x45,
0x95, 0xd7, 0x81, 0x3e, 0xa5, 0x41, 0x4a, 0x91, 0x99, 0x32, 0x9a, 0x6d,
0x9f, 0x7f, 0x76, 0x0d, 0xd8, 0xbb, 0x24, 0x9b, 0xf3, 0xf5, 0x3d, 0x9a,
0x77, 0xfb, 0xb7, 0xb3, 0x95, 0xb8, 0xd6, 0x6d, 0x78, 0x79, 0xa5, 0x1f,
0xe5, 0x9e, 0xf9, 0x60, 0x1f, 0x79, 0x99, 0x8e, 0xb3, 0x56, 0x8e, 0x1f,
0xdc, 0x78, 0x9f, 0x64, 0x0a, 0xca, 0xb3, 0x85, 0x8a, 0x82, 0xef, 0x29,
0x30, 0xfa, 0x5c, 0xe1, 0x4b, 0x5b, 0x9e, 0xa0, 0xbd, 0xb2, 0x9f, 0x45,
0x72, 0xda, 0x85, 0xaa, 0x3d, 0xef, 0x39, 0xb7, 0xef, 0xaf, 0xff, 0xa0,
0x74, 0xb9, 0x26, 0x70, 0x70, 0xd5, 0x0b, 0x5d, 0x07, 0x84, 0x2e, 0x49,
0xbb, 0xa3, 0xbc, 0x78, 0x7f, 0xf2, 0x95, 0xd6, 0xae, 0x3b, 0x51, 0x43,
0x05, 0xf1, 0x02, 0xaf, 0xe5, 0xa0, 0x47, 0xb3, 0xfb, 0x4c, 0x99, 0xeb,
0x92, 0xa2, 0x74, 0xd2, 0x44, 0xd6, 0x04, 0x92, 0xc0, 0xe2, 0xe6, 0xe2,
0x12, 0xce, 0xf0, 0xf9, 0xe3, 0xf6, 0x2e, 0xfd, 0x09, 0x55, 0xe7, 0x1c,
0x76, 0x8a, 0xa6, 0xbb, 0x3c, 0xd8, 0x0b, 0xbb, 0x37, 0x55, 0xc8, 0xb7,
0xeb, 0xee, 0x32, 0x71, 0x2f, 0x40, 0xf2, 0x24, 0x51, 0x19, 0x48, 0x70,
0x21, 0xb4, 0xb8, 0x4e, 0x15, 0x65, 0xe3, 0xca, 0x31, 0x96, 0x7a, 0xc8,
0x60, 0x4d, 0x40, 0x32, 0x17, 0x0d, 0xec, 0x28, 0x0a, 0xee, 0xfa, 0x09,
0x5d, 0x08, 0xb3, 0xb7, 0x24, 0x1e, 0xf6, 0x64, 0x6a, 0x6c, 0x86, 0xe5,
0xc6, 0x2c, 0xe0, 0x8b, 0xe0, 0x99,
}
copy(clientHello, clientInitial)
want := analyzer.PropMap{
"alpn": []string{"ping/1.0"},
"ciphers": []uint16{4865, 4866, 4867},
"compression": []uint8{0},
"random": []uint8{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31},
"session": []uint8{},
"sni": "example.ulfheim.net",
"supported_versions": []uint16{772},
"version": uint16(771),
}
s := quicStream{}
u, _ := s.Feed(false, clientHello)
got := u.M.Get("req")
if !reflect.DeepEqual(got, want) {
t.Errorf("%d B parsed = %v, want %v", len(clientHello), got, want)
}
}

View File

@ -5,9 +5,9 @@ import (
"fmt"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"time"
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/analyzer/tcp"
@ -17,6 +17,7 @@ import (
"github.com/apernet/OpenGFW/modifier"
modUDP "github.com/apernet/OpenGFW/modifier/udp"
"github.com/apernet/OpenGFW/ruleset"
"github.com/apernet/OpenGFW/ruleset/builtins/geo"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@ -42,6 +43,7 @@ var logger *zap.Logger
// Flags
var (
cfgFile string
pcapFile string
logLevel string
logFormat string
)
@ -93,6 +95,8 @@ var analyzers = []analyzer.Analyzer{
&tcp.TLSAnalyzer{},
&tcp.TrojanAnalyzer{},
&udp.DNSAnalyzer{},
&udp.OpenVPNAnalyzer{},
&udp.QUICAnalyzer{},
&udp.WireGuardAnalyzer{},
}
@ -115,6 +119,7 @@ func init() {
func initFlags() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file")
rootCmd.PersistentFlags().StringVarP(&pcapFile, "pcap", "p", "", "pcap file (optional)")
rootCmd.PersistentFlags().StringVarP(&logLevel, "log-level", "l", envOrDefaultString(appLogLevelEnv, "info"), "log level")
rootCmd.PersistentFlags().StringVarP(&logFormat, "log-format", "f", envOrDefaultString(appLogFormatEnv, "console"), "log format")
}
@ -164,19 +169,33 @@ type cliConfig struct {
IO cliConfigIO `mapstructure:"io"`
Workers cliConfigWorkers `mapstructure:"workers"`
Ruleset cliConfigRuleset `mapstructure:"ruleset"`
Replay cliConfigReplay `mapstructure:"replay"`
}
type cliConfigIO struct {
QueueSize uint32 `mapstructure:"queueSize"`
Local bool `mapstructure:"local"`
QueueSize uint32 `mapstructure:"queueSize"`
QueueNum *uint16 `mapstructure:"queueNum"`
Table string `mapstructure:"table"`
ConnMarkAccept uint32 `mapstructure:"connMarkAccept"`
ConnMarkDrop uint32 `mapstructure:"connMarkDrop"`
ReadBuffer int `mapstructure:"rcvBuf"`
WriteBuffer int `mapstructure:"sndBuf"`
Local bool `mapstructure:"local"`
RST bool `mapstructure:"rst"`
}
type cliConfigReplay struct {
Realtime bool `mapstructure:"realtime"`
}
type cliConfigWorkers struct {
Count int `mapstructure:"count"`
QueueSize int `mapstructure:"queueSize"`
TCPMaxBufferedPagesTotal int `mapstructure:"tcpMaxBufferedPagesTotal"`
TCPMaxBufferedPagesPerConn int `mapstructure:"tcpMaxBufferedPagesPerConn"`
UDPMaxStreams int `mapstructure:"udpMaxStreams"`
Count int `mapstructure:"count"`
QueueSize int `mapstructure:"queueSize"`
TCPMaxBufferedPagesTotal int `mapstructure:"tcpMaxBufferedPagesTotal"`
TCPMaxBufferedPagesPerConn int `mapstructure:"tcpMaxBufferedPagesPerConn"`
TCPTimeout time.Duration `mapstructure:"tcpTimeout"`
UDPMaxStreams int `mapstructure:"udpMaxStreams"`
}
type cliConfigRuleset struct {
@ -190,14 +209,35 @@ func (c *cliConfig) fillLogger(config *engine.Config) error {
}
func (c *cliConfig) fillIO(config *engine.Config) error {
nfio, err := io.NewNFQueuePacketIO(io.NFQueuePacketIOConfig{
QueueSize: c.IO.QueueSize,
Local: c.IO.Local,
})
var ioImpl io.PacketIO
var err error
if pcapFile != "" {
// Setup IO for pcap file replay
logger.Info("replaying from pcap file", zap.String("pcap file", pcapFile))
ioImpl, err = io.NewPcapPacketIO(io.PcapPacketIOConfig{
PcapFile: pcapFile,
Realtime: c.Replay.Realtime,
})
} else {
// Setup IO for nfqueue
ioImpl, err = io.NewNFQueuePacketIO(io.NFQueuePacketIOConfig{
QueueSize: c.IO.QueueSize,
QueueNum: c.IO.QueueNum,
Table: c.IO.Table,
ConnMarkAccept: c.IO.ConnMarkAccept,
ConnMarkDrop: c.IO.ConnMarkDrop,
ReadBuffer: c.IO.ReadBuffer,
WriteBuffer: c.IO.WriteBuffer,
Local: c.IO.Local,
RST: c.IO.RST,
})
}
if err != nil {
return configError{Field: "io", Err: err}
}
config.IOs = []io.PacketIO{nfio}
config.IO = ioImpl
return nil
}
@ -206,6 +246,7 @@ func (c *cliConfig) fillWorkers(config *engine.Config) error {
config.WorkerQueueSize = c.Workers.QueueSize
config.WorkerTCPMaxBufferedPagesTotal = c.Workers.TCPMaxBufferedPagesTotal
config.WorkerTCPMaxBufferedPagesPerConn = c.Workers.TCPMaxBufferedPagesPerConn
config.WorkerTCPTimeout = c.Workers.TCPTimeout
config.WorkerUDPMaxStreams = c.Workers.UDPMaxStreams
return nil
}
@ -240,12 +281,7 @@ func runMain(cmd *cobra.Command, args []string) {
if err != nil {
logger.Fatal("failed to parse config", zap.Error(err))
}
defer func() {
// Make sure to close all IOs on exit
for _, i := range engineConfig.IOs {
_ = i.Close()
}
}()
defer engineConfig.IO.Close() // Make sure to close IO on exit
// Ruleset
rawRs, err := ruleset.ExprRulesFromYAML(args[0])
@ -253,8 +289,9 @@ func runMain(cmd *cobra.Command, args []string) {
logger.Fatal("failed to load rules", zap.Error(err))
}
rsConfig := &ruleset.BuiltinConfig{
GeoSiteFilename: config.Ruleset.GeoSite,
GeoIpFilename: config.Ruleset.GeoIp,
Logger: &rulesetLogger{},
GeoMatcher: geo.NewGeoMatcher(config.Ruleset.GeoSite, config.Ruleset.GeoIp),
ProtectedDialContext: engineConfig.IO.ProtectedDialContext,
}
rs, err := ruleset.CompileExprRules(rawRs, analyzers, modifiers, rsConfig)
if err != nil {
@ -272,15 +309,15 @@ func runMain(cmd *cobra.Command, args []string) {
ctx, cancelFunc := context.WithCancel(context.Background())
go func() {
// Graceful shutdown
shutdownChan := make(chan os.Signal)
signal.Notify(shutdownChan, os.Interrupt, os.Kill)
shutdownChan := make(chan os.Signal, 1)
signal.Notify(shutdownChan, os.Interrupt, syscall.SIGTERM)
<-shutdownChan
logger.Info("shutting down gracefully...")
cancelFunc()
}()
go func() {
// Rule reload
reloadChan := make(chan os.Signal)
reloadChan := make(chan os.Signal, 1)
signal.Notify(reloadChan, syscall.SIGHUP)
for {
<-reloadChan
@ -336,12 +373,26 @@ func (l *engineLogger) TCPStreamPropUpdate(info ruleset.StreamInfo, close bool)
}
func (l *engineLogger) TCPStreamAction(info ruleset.StreamInfo, action ruleset.Action, noMatch bool) {
logger.Info("TCP stream action",
zap.Int64("id", info.ID),
zap.String("src", info.SrcString()),
zap.String("dst", info.DstString()),
zap.String("action", action.String()),
zap.Bool("noMatch", noMatch))
if noMatch {
logger.Debug("TCP stream no match",
zap.Int64("id", info.ID),
zap.String("src", info.SrcString()),
zap.String("dst", info.DstString()),
zap.String("action", action.String()))
} else {
logger.Info("TCP stream action",
zap.Int64("id", info.ID),
zap.String("src", info.SrcString()),
zap.String("dst", info.DstString()),
zap.String("action", action.String()))
}
}
func (l *engineLogger) TCPFlush(workerID, flushed, closed int) {
logger.Debug("TCP flush",
zap.Int("workerID", workerID),
zap.Int("flushed", flushed),
zap.Int("closed", closed))
}
func (l *engineLogger) UDPStreamNew(workerID int, info ruleset.StreamInfo) {
@ -362,20 +413,19 @@ func (l *engineLogger) UDPStreamPropUpdate(info ruleset.StreamInfo, close bool)
}
func (l *engineLogger) UDPStreamAction(info ruleset.StreamInfo, action ruleset.Action, noMatch bool) {
logger.Info("UDP stream action",
zap.Int64("id", info.ID),
zap.String("src", info.SrcString()),
zap.String("dst", info.DstString()),
zap.String("action", action.String()),
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))
if noMatch {
logger.Debug("UDP stream no match",
zap.Int64("id", info.ID),
zap.String("src", info.SrcString()),
zap.String("dst", info.DstString()),
zap.String("action", action.String()))
} else {
logger.Info("UDP stream action",
zap.Int64("id", info.ID),
zap.String("src", info.SrcString()),
zap.String("dst", info.DstString()),
zap.String("action", action.String()))
}
}
func (l *engineLogger) ModifyError(info ruleset.StreamInfo, err error) {
@ -407,17 +457,29 @@ func (l *engineLogger) AnalyzerErrorf(streamID int64, name string, format string
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 {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func envOrDefaultBool(key string, def bool) bool {
if v := os.Getenv(key); v != "" {
b, _ := strconv.ParseBool(v)
return b
}
return def
}

View File

@ -1,422 +0,0 @@
# Analyzers
Analyzers are one of the main components of OpenGFW. Their job is to analyze a connection, see if it's a protocol they
support, and if so, extract information from that connection and provide properties for the rule engine to match against
user-provided rules. OpenGFW will automatically analyze which analyzers are referenced in the given rules and enable
only those that are needed.
This document lists the properties provided by each analyzer that can be used by rules.
## DNS (TCP & UDP)
For queries:
```json
{
"dns": {
"aa": false,
"id": 41953,
"opcode": 0,
"qr": false,
"questions": [
{
"class": 1,
"name": "www.google.com",
"type": 1
}
],
"ra": false,
"rcode": 0,
"rd": true,
"tc": false,
"z": 0
}
}
```
For responses:
```json
{
"dns": {
"aa": false,
"answers": [
{
"a": "142.251.32.36",
"class": 1,
"name": "www.google.com",
"ttl": 255,
"type": 1
}
],
"id": 41953,
"opcode": 0,
"qr": true,
"questions": [
{
"class": 1,
"name": "www.google.com",
"type": 1
}
],
"ra": true,
"rcode": 0,
"rd": true,
"tc": false,
"z": 0
}
}
```
Example for blocking DNS queries for `www.google.com`:
```yaml
- name: Block Google DNS
action: drop
expr: dns != nil && !dns.qr && any(dns.questions, {.name == "www.google.com"})
```
## FET (Fully Encrypted Traffic)
Check https://www.usenix.org/system/files/usenixsecurity23-wu-mingshi.pdf for more information.
```json
{
"fet": {
"ex1": 3.7560976,
"ex2": true,
"ex3": 0.9512195,
"ex4": 39,
"ex5": false,
"yes": false
}
}
```
Example for blocking fully encrypted traffic:
```yaml
- name: Block suspicious proxy traffic
action: block
expr: fet != nil && fet.yes
```
## HTTP
```json
{
"http": {
"req": {
"headers": {
"accept": "*/*",
"host": "ipinfo.io",
"user-agent": "curl/7.81.0"
},
"method": "GET",
"path": "/",
"version": "HTTP/1.1"
},
"resp": {
"headers": {
"access-control-allow-origin": "*",
"content-length": "333",
"content-type": "application/json; charset=utf-8",
"date": "Wed, 24 Jan 2024 05:41:44 GMT",
"referrer-policy": "strict-origin-when-cross-origin",
"server": "nginx/1.24.0",
"strict-transport-security": "max-age=2592000; includeSubDomains",
"via": "1.1 google",
"x-content-type-options": "nosniff",
"x-envoy-upstream-service-time": "2",
"x-frame-options": "SAMEORIGIN",
"x-xss-protection": "1; mode=block"
},
"status": 200,
"version": "HTTP/1.1"
}
}
}
```
Example for blocking HTTP requests to `ipinfo.io`:
```yaml
- name: Block ipinfo.io HTTP
action: block
expr: http != nil && http.req != nil && http.req.headers != nil && http.req.headers.host == "ipinfo.io"
```
## SSH
```json
{
"ssh": {
"server": {
"comments": "Ubuntu-3ubuntu0.6",
"protocol": "2.0",
"software": "OpenSSH_8.9p1"
},
"client": {
"comments": "IMHACKER",
"protocol": "2.0",
"software": "OpenSSH_8.9p1"
}
}
}
```
Example for blocking all SSH connections:
```yaml
- name: Block SSH
action: block
expr: ssh != nil
```
## TLS
```json
{
"tls": {
"req": {
"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
],
"compression": "AA==",
"random": "UqfPi+EmtMgusILrKcELvVWwpOdPSM/My09nPXl84dg=",
"session": "jCTrpAzHpwrfuYdYx4FEjZwbcQxCuZ52HGIoOcbw1vA=",
"sni": "ipinfo.io",
"supported_versions": [
772,
771
],
"version": 771,
"ech": true
},
"resp": {
"cipher": 4866,
"compression": 0,
"random": "R/Cy1m9pktuBMZQIHahD8Y83UWPRf8j8luwNQep9yJI=",
"session": "jCTrpAzHpwrfuYdYx4FEjZwbcQxCuZ52HGIoOcbw1vA=",
"supported_versions": 772,
"version": 771
}
}
}
```
Example for blocking TLS connections to `ipinfo.io`:
```yaml
- name: Block ipinfo.io TLS
action: block
expr: tls != nil && tls.req != nil && tls.req.sni == "ipinfo.io"
```
## Trojan (proxy protocol)
Check https://github.com/XTLS/Trojan-killer for more information.
```json
{
"trojan": {
"down": 4712,
"up": 671,
"yes": true
}
}
```
Example for blocking Trojan connections:
```yaml
- name: Block Trojan
action: block
expr: trojan != nil && trojan.yes
```
## SOCKS
SOCKS4:
```json5
{
"socks": {
"version": 4,
"req": {
"cmd": 1,
"addr_type": 1, // same as socks5
"addr": "1.1.1.1",
// for socks4a
// "addr_type": 3,
// "addr": "google.com",
"port": 443,
"auth": {
"user_id": "user"
}
},
"resp": {
"rep": 90, // 0x5A(90) granted
"addr_type": 1,
"addr": "1.1.1.1",
"port": 443
}
}
}
```
SOCKS5 without auth:
```json5
{
"socks": {
"version": 5,
"req": {
"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
}
},
"resp": {
"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
}
}
}
}
```
SOCKS5 with auth:
```json5
{
"socks": {
"version": 5,
"req": {
"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
"username": "user",
"password": "pass"
}
},
"resp": {
"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
}
}
}
}
```
Example for blocking connections to `google.com:80` and user `foobar`:
```yaml
- name: Block SOCKS google.com:80
action: block
expr: string(socks?.req?.addr) endsWith "google.com" && socks?.req?.port == 80
- name: Block SOCKS user foobar
action: block
expr: socks?.req?.auth?.method == 2 && socks?.req?.auth?.username == "foobar"
```
## WireGuard
```json5
{
"wireguard": {
"message_type": 1, // 0x1: handshake_initiation, 0x2: handshake_response, 0x3: packet_cookie_reply, 0x4: packet_data
"handshake_initiation": {
"sender_index": 0x12345678
},
"handshake_response": {
"sender_index": 0x12345678,
"receiver_index": 0x87654321,
"receiver_index_matched": true
},
"packet_data": {
"receiver_index": 0x12345678,
"receiver_index_matched": true
},
"packet_cookie_reply": {
"receiver_index": 0x12345678,
"receiver_index_matched": true
}
}
}
```
Example for blocking WireGuard traffic:
```yaml
# false positive: high
- name: Block all WireGuard-like traffic
action: block
expr: wireguard != nil
# false positive: medium
- name: Block WireGuard by handshake_initiation
action: drop
expr: wireguard?.handshake_initiation != nil
# false positive: low
- name: Block WireGuard by handshake_response
action: drop
expr: wireguard?.handshake_response?.receiver_index_matched == true
# false positive: pretty low
- name: Block WireGuard by packet_data
action: block
expr: wireguard?.packet_data?.receiver_index_matched == true
```

View File

@ -15,7 +15,7 @@ var _ Engine = (*engine)(nil)
type engine struct {
logger Logger
ioList []io.PacketIO
io io.PacketIO
workers []*worker
}
@ -34,6 +34,7 @@ func NewEngine(config Config) (Engine, error) {
Ruleset: config.Ruleset,
TCPMaxBufferedPagesTotal: config.WorkerTCPMaxBufferedPagesTotal,
TCPMaxBufferedPagesPerConn: config.WorkerTCPMaxBufferedPagesPerConn,
TCPTimeout: config.WorkerTCPTimeout,
UDPMaxStreams: config.WorkerUDPMaxStreams,
})
if err != nil {
@ -42,7 +43,7 @@ func NewEngine(config Config) (Engine, error) {
}
return &engine{
logger: config.Logger,
ioList: config.IOs,
io: config.IO,
workers: workers,
}, nil
}
@ -57,28 +58,30 @@ func (e *engine) UpdateRuleset(r ruleset.Ruleset) error {
}
func (e *engine) Run(ctx context.Context) error {
workerCtx, workerCancel := context.WithCancel(ctx)
defer workerCancel() // Stop workers
// Register IO shutdown
ioCtx, ioCancel := context.WithCancel(ctx)
defer ioCancel() // Stop workers & IOs
e.io.SetCancelFunc(ioCancel)
defer ioCancel() // Stop IO
// Start workers
for _, w := range e.workers {
go w.Run(ioCtx)
go w.Run(workerCtx)
}
// Register callbacks
errChan := make(chan error, len(e.ioList))
for _, i := range e.ioList {
ioEntry := i // Make sure dispatch() uses the correct ioEntry
err := ioEntry.Register(ioCtx, func(p io.Packet, err error) bool {
if err != nil {
errChan <- err
return false
}
return e.dispatch(ioEntry, p)
})
// Register IO callback
errChan := make(chan error, 1)
err := e.io.Register(ioCtx, func(p io.Packet, err error) bool {
if err != nil {
return err
errChan <- err
return false
}
return e.dispatch(p)
})
if err != nil {
return err
}
// Block until IO errors or context is cancelled
@ -87,12 +90,13 @@ func (e *engine) Run(ctx context.Context) error {
return err
case <-ctx.Done():
return nil
case <-ioCtx.Done():
return nil
}
}
// dispatch dispatches a packet to a worker.
// This must be safe for concurrent use, as it may be called from multiple IOs.
func (e *engine) dispatch(ioEntry io.PacketIO, p io.Packet) bool {
func (e *engine) dispatch(p io.Packet) bool {
data := p.Data()
ipVersion := data[0] >> 4
var layerType gopacket.LayerType
@ -102,17 +106,19 @@ func (e *engine) dispatch(ioEntry io.PacketIO, p io.Packet) bool {
layerType = layers.LayerTypeIPv6
} else {
// Unsupported network layer
_ = ioEntry.SetVerdict(p, io.VerdictAcceptStream, nil)
_ = e.io.SetVerdict(p, io.VerdictAcceptStream, nil)
return true
}
// Convert to gopacket.Packet
packet := gopacket.NewPacket(data, layerType, gopacket.DecodeOptions{Lazy: true, NoCopy: true})
packet.Metadata().Timestamp = p.Timestamp()
// Load balance by stream ID
index := p.StreamID() % uint32(len(e.workers))
packet := gopacket.NewPacket(data, layerType, gopacket.DecodeOptions{Lazy: true, NoCopy: true})
e.workers[index].Feed(&workerPacket{
StreamID: p.StreamID(),
Packet: packet,
SetVerdict: func(v io.Verdict, b []byte) error {
return ioEntry.SetVerdict(p, v, b)
return e.io.SetVerdict(p, v, b)
},
})
return true

View File

@ -2,6 +2,7 @@ package engine
import (
"context"
"time"
"github.com/apernet/OpenGFW/io"
"github.com/apernet/OpenGFW/ruleset"
@ -18,13 +19,14 @@ type Engine interface {
// Config is the configuration for the engine.
type Config struct {
Logger Logger
IOs []io.PacketIO
IO io.PacketIO
Ruleset ruleset.Ruleset
Workers int // Number of workers. Zero or negative means auto (number of CPU cores).
WorkerQueueSize int
WorkerTCPMaxBufferedPagesTotal int
WorkerTCPMaxBufferedPagesPerConn int
WorkerTCPTimeout time.Duration
WorkerUDPMaxStreams int
}
@ -36,12 +38,12 @@ type Logger interface {
TCPStreamNew(workerID int, info ruleset.StreamInfo)
TCPStreamPropUpdate(info ruleset.StreamInfo, close bool)
TCPStreamAction(info ruleset.StreamInfo, action ruleset.Action, noMatch bool)
TCPFlush(workerID, flushed, closed int)
UDPStreamNew(workerID int, info ruleset.StreamInfo)
UDPStreamPropUpdate(info ruleset.StreamInfo, close bool)
UDPStreamAction(info ruleset.StreamInfo, action ruleset.Action, noMatch bool)
MatchError(info ruleset.StreamInfo, err error)
ModifyError(info ruleset.StreamInfo, err error)
AnalyzerDebugf(streamID int64, name string, format string, args ...interface{})

View File

@ -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) {
@ -146,13 +148,12 @@ func (s *tcpStream) ReassembledSG(sg reassembly.ScatterGather, ac reassembly.Ass
s.virgin = false
s.logger.TCPStreamPropUpdate(s.info, false)
// Match properties against ruleset
result, err := s.ruleset.Match(s.info)
if err != nil {
s.logger.MatchError(s.info, err)
}
result := s.ruleset.Match(s.info)
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 +161,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)
}

View File

@ -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) {
@ -199,10 +201,7 @@ func (s *udpStream) Feed(udp *layers.UDP, rev bool, uc *udpContext) {
s.virgin = false
s.logger.UDPStreamPropUpdate(s.info, false)
// Match properties against ruleset
result, err := s.ruleset.Match(s.info)
if err != nil {
s.logger.MatchError(s.info, err)
}
result := s.ruleset.Match(s.info)
action := result.Action
if action == ruleset.ActionModify {
// Call the modifier instance
@ -212,6 +211,7 @@ func (s *udpStream) Feed(udp *layers.UDP, rev bool, uc *udpContext) {
s.logger.ModifyError(s.info, errInvalidModifier)
action = ruleset.ActionMaybe
} else {
var err error
uc.Packet, err = udpMI.Process(udp.Payload)
if err != nil {
// Modifier error, fallback to maybe
@ -221,8 +221,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 +232,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)
}

View File

@ -2,6 +2,7 @@ package engine
import (
"context"
"time"
"github.com/apernet/OpenGFW/io"
"github.com/apernet/OpenGFW/ruleset"
@ -14,9 +15,12 @@ import (
const (
defaultChanSize = 64
defaultTCPMaxBufferedPagesTotal = 4096
defaultTCPMaxBufferedPagesPerConnection = 64
defaultTCPMaxBufferedPagesTotal = 65536
defaultTCPMaxBufferedPagesPerConnection = 16
defaultTCPTimeout = 10 * time.Minute
defaultUDPMaxStreams = 4096
tcpFlushInterval = 1 * time.Minute
)
type workerPacket struct {
@ -33,6 +37,7 @@ type worker struct {
tcpStreamFactory *tcpStreamFactory
tcpStreamPool *reassembly.StreamPool
tcpAssembler *reassembly.Assembler
tcpTimeout time.Duration
udpStreamFactory *udpStreamFactory
udpStreamManager *udpStreamManager
@ -47,6 +52,7 @@ type workerConfig struct {
Ruleset ruleset.Ruleset
TCPMaxBufferedPagesTotal int
TCPMaxBufferedPagesPerConn int
TCPTimeout time.Duration
UDPMaxStreams int
}
@ -60,6 +66,9 @@ func (c *workerConfig) fillDefaults() {
if c.TCPMaxBufferedPagesPerConn <= 0 {
c.TCPMaxBufferedPagesPerConn = defaultTCPMaxBufferedPagesPerConnection
}
if c.TCPTimeout <= 0 {
c.TCPTimeout = defaultTCPTimeout
}
if c.UDPMaxStreams <= 0 {
c.UDPMaxStreams = defaultUDPMaxStreams
}
@ -98,6 +107,7 @@ func newWorker(config workerConfig) (*worker, error) {
tcpStreamFactory: tcpSF,
tcpStreamPool: tcpStreamPool,
tcpAssembler: tcpAssembler,
tcpTimeout: config.TCPTimeout,
udpStreamFactory: udpSF,
udpStreamManager: udpSM,
modSerializeBuffer: gopacket.NewSerializeBuffer(),
@ -111,6 +121,10 @@ func (w *worker) Feed(p *workerPacket) {
func (w *worker) Run(ctx context.Context) {
w.logger.WorkerStart(w.id)
defer w.logger.WorkerStop(w.id)
tcpFlushTicker := time.NewTicker(tcpFlushInterval)
defer tcpFlushTicker.Stop()
for {
select {
case <-ctx.Done():
@ -122,6 +136,8 @@ func (w *worker) Run(ctx context.Context) {
}
v, b := w.handle(wPkt.StreamID, wPkt.Packet)
_ = wPkt.SetVerdict(v, b)
case <-tcpFlushTicker.C:
w.flushTCP(w.tcpTimeout)
}
}
}
@ -176,6 +192,11 @@ func (w *worker) handleTCP(ipFlow gopacket.Flow, pMeta *gopacket.PacketMetadata,
return io.Verdict(ctx.Verdict)
}
func (w *worker) flushTCP(timeout time.Duration) {
flushed, closed := w.tcpAssembler.FlushCloseOlderThan(time.Now().Add(-timeout))
w.logger.TCPFlush(w.id, flushed, closed)
}
func (w *worker) handleUDP(streamID uint32, ipFlow gopacket.Flow, udp *layers.UDP) (io.Verdict, []byte) {
ctx := &udpContext{
Verdict: udpVerdictAccept,

13
go.mod
View File

@ -1,25 +1,26 @@
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/expr-lang/expr v1.16.3
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
golang.org/x/sys v0.17.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
@ -29,7 +30,6 @@ 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
@ -41,7 +41,6 @@ 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/text v0.14.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
)

39
go.sum
View File

@ -7,21 +7,28 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
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/expr-lang/expr v1.16.3 h1:NLldf786GffptcXNxxJx5dQ+FzeWDKChBDqOOwyK8to=
github.com/expr-lang/expr v1.16.3/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=
@ -31,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=
@ -40,12 +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=
@ -72,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=
@ -95,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=
@ -112,6 +136,8 @@ 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=
@ -120,6 +146,7 @@ google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs
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=

View File

@ -2,6 +2,8 @@ package io
import (
"context"
"net"
"time"
)
type Verdict int
@ -23,13 +25,14 @@ const (
type Packet interface {
// StreamID is the ID of the stream the packet belongs to.
StreamID() uint32
// Timestamp is the time the packet was received.
Timestamp() time.Time
// Data is the raw packet data, starting with the IP header.
Data() []byte
}
// PacketCallback is called for each packet received.
// Return false to "unregister" and stop receiving packets.
// It must be safe for concurrent use.
type PacketCallback func(Packet, error) bool
type PacketIO interface {
@ -39,8 +42,15 @@ type PacketIO interface {
Register(context.Context, PacketCallback) error
// SetVerdict sets the verdict for a packet.
SetVerdict(Packet, Verdict, []byte) error
// ProtectedDialContext is like net.DialContext, but the connection is "protected"
// in the sense that the packets sent/received through the connection must bypass
// the packet IO and not be processed by the callback.
ProtectedDialContext(ctx context.Context, network, address string) (net.Conn, error)
// Close closes the packet IO.
Close() error
// SetCancelFunc gives packet IO access to context cancel function, enabling it to
// trigger a shutdown
SetCancelFunc(cancelFunc context.CancelFunc) error
}
type ErrInvalidPacket struct {

View File

@ -4,36 +4,88 @@ import (
"context"
"encoding/binary"
"errors"
"fmt"
"net"
"os/exec"
"strconv"
"strings"
"syscall"
"time"
"github.com/coreos/go-iptables/iptables"
"github.com/florianl/go-nfqueue"
"github.com/mdlayher/netlink"
"golang.org/x/sys/unix"
)
const (
nfqueueNum = 100
nfqueueDefaultQueueNum = 100
nfqueueMaxPacketLen = 0xFFFF
nfqueueDefaultQueueSize = 128
nfqueueConnMarkAccept = 1001
nfqueueConnMarkDrop = 1002
nfqueueDefaultConnMarkAccept = 1001
nftFamily = "inet"
nftDefaultTable = "opengfw"
)
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"}},
{"filter", "FORWARD", []string{"-j", "NFQUEUE", "--queue-num", strconv.Itoa(nfqueueNum), "--queue-bypass"}},
func (n *nfqueuePacketIO) generateNftRules() (*nftTableSpec, error) {
if n.local && n.rst {
return nil, errors.New("tcp rst is not supported in local mode")
}
table := &nftTableSpec{
Family: nftFamily,
Table: n.table,
}
table.Defines = append(table.Defines, fmt.Sprintf("define ACCEPT_CTMARK=%d", n.connMarkAccept))
table.Defines = append(table.Defines, fmt.Sprintf("define DROP_CTMARK=%d", n.connMarkDrop))
table.Defines = append(table.Defines, fmt.Sprintf("define QUEUE_NUM=%d", n.queueNum))
if n.local {
table.Chains = []nftChainSpec{
{Chain: "INPUT", Header: "type filter hook input priority filter; policy accept;"},
{Chain: "OUTPUT", Header: "type filter hook output priority filter; policy accept;"},
}
} else {
table.Chains = []nftChainSpec{
{Chain: "FORWARD", Header: "type filter hook forward priority filter; policy accept;"},
}
}
for i := range table.Chains {
c := &table.Chains[i]
c.Rules = append(c.Rules, "meta mark $ACCEPT_CTMARK ct mark set $ACCEPT_CTMARK") // Bypass protected connections
c.Rules = append(c.Rules, "ct mark $ACCEPT_CTMARK counter accept")
if n.rst {
c.Rules = append(c.Rules, "ip protocol tcp ct mark $DROP_CTMARK counter reject with tcp reset")
}
c.Rules = append(c.Rules, "ct mark $DROP_CTMARK counter drop")
c.Rules = append(c.Rules, "counter queue num $QUEUE_NUM bypass")
}
return table, nil
}
var iptRulesLocal = []iptRule{
{"filter", "INPUT", []string{"-m", "connmark", "--mark", strconv.Itoa(nfqueueConnMarkAccept), "-j", "ACCEPT"}},
{"filter", "INPUT", []string{"-m", "connmark", "--mark", strconv.Itoa(nfqueueConnMarkDrop), "-j", "DROP"}},
{"filter", "INPUT", []string{"-j", "NFQUEUE", "--queue-num", strconv.Itoa(nfqueueNum), "--queue-bypass"}},
func (n *nfqueuePacketIO) generateIptRules() ([]iptRule, error) {
if n.local && n.rst {
return nil, errors.New("tcp rst is not supported in local mode")
}
var chains []string
if n.local {
chains = []string{"INPUT", "OUTPUT"}
} else {
chains = []string{"FORWARD"}
}
rules := make([]iptRule, 0, 4*len(chains))
for _, chain := range chains {
// Bypass protected connections
rules = append(rules, iptRule{"filter", chain, []string{"-m", "mark", "--mark", strconv.Itoa(n.connMarkAccept), "-j", "CONNMARK", "--set-mark", strconv.Itoa(n.connMarkAccept)}})
rules = append(rules, iptRule{"filter", chain, []string{"-m", "connmark", "--mark", strconv.Itoa(n.connMarkAccept), "-j", "ACCEPT"}})
if n.rst {
rules = append(rules, iptRule{"filter", chain, []string{"-p", "tcp", "-m", "connmark", "--mark", strconv.Itoa(n.connMarkDrop), "-j", "REJECT", "--reject-with", "tcp-reset"}})
}
rules = append(rules, iptRule{"filter", chain, []string{"-m", "connmark", "--mark", strconv.Itoa(n.connMarkDrop), "-j", "DROP"}})
rules = append(rules, iptRule{"filter", chain, []string{"-j", "NFQUEUE", "--queue-num", strconv.Itoa(n.queueNum), "--queue-bypass"}})
}
{"filter", "OUTPUT", []string{"-m", "connmark", "--mark", strconv.Itoa(nfqueueConnMarkAccept), "-j", "ACCEPT"}},
{"filter", "OUTPUT", []string{"-m", "connmark", "--mark", strconv.Itoa(nfqueueConnMarkDrop), "-j", "DROP"}},
{"filter", "OUTPUT", []string{"-j", "NFQUEUE", "--queue-num", strconv.Itoa(nfqueueNum), "--queue-bypass"}},
return rules, nil
}
var _ PacketIO = (*nfqueuePacketIO)(nil)
@ -41,32 +93,74 @@ var _ PacketIO = (*nfqueuePacketIO)(nil)
var errNotNFQueuePacket = errors.New("not an NFQueue packet")
type nfqueuePacketIO struct {
n *nfqueue.Nfqueue
local bool
ipt4 *iptables.IPTables
ipt6 *iptables.IPTables
iptSet bool // whether iptables rules are set
n *nfqueue.Nfqueue
local bool
rst bool
rSet bool // whether the nftables/iptables rules have been set
queueNum int
table string // nftable name
connMarkAccept int
connMarkDrop int
// iptables not nil = use iptables instead of nftables
ipt4 *iptables.IPTables
ipt6 *iptables.IPTables
protectedDialer *net.Dialer
}
type NFQueuePacketIOConfig struct {
QueueSize uint32
Local bool
QueueSize uint32
QueueNum *uint16
Table string
ConnMarkAccept uint32
ConnMarkDrop uint32
ReadBuffer int
WriteBuffer int
Local bool
RST bool
}
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
if config.QueueNum == nil {
queueNum := uint16(nfqueueDefaultQueueNum)
config.QueueNum = &queueNum
}
ipt6, err := iptables.NewWithProtocol(iptables.ProtocolIPv6)
if err != nil {
return nil, err
if config.Table == "" {
config.Table = nftDefaultTable
}
if config.ConnMarkAccept == 0 {
config.ConnMarkAccept = nfqueueDefaultConnMarkAccept
}
if config.ConnMarkDrop == 0 {
config.ConnMarkDrop = config.ConnMarkAccept + 1
if config.ConnMarkDrop == 0 {
// Overflow
config.ConnMarkDrop = 1
}
}
if config.ConnMarkAccept == config.ConnMarkDrop {
return nil, errors.New("connMarkAccept and connMarkDrop cannot be the same")
}
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,
NfQueue: *config.QueueNum,
MaxPacketLen: nfqueueMaxPacketLen,
MaxQueueLen: config.QueueSize,
Copymode: nfqueue.NfQnlCopyPacket,
@ -75,20 +169,52 @@ func NewNFQueuePacketIO(config NFQueuePacketIOConfig) (PacketIO, error) {
if err != nil {
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{
n: n,
local: config.Local,
ipt4: ipt4,
ipt6: ipt6,
n: n,
local: config.Local,
rst: config.RST,
queueNum: int(*config.QueueNum),
table: config.Table,
connMarkAccept: int(config.ConnMarkAccept),
connMarkDrop: int(config.ConnMarkDrop),
ipt4: ipt4,
ipt6: ipt6,
protectedDialer: &net.Dialer{
Control: func(network, address string, c syscall.RawConn) error {
var err error
cErr := c.Control(func(fd uintptr) {
err = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_MARK, int(config.ConnMarkAccept))
})
if cErr != nil {
return cErr
}
return err
},
},
}, nil
}
func (n *nfqueuePacketIO) Register(ctx context.Context, cb PacketCallback) error {
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
// 20 is the minimum possible size of an IP packet
if ok, verdict := n.packetAttributeSanityCheck(a); !ok {
if a.PacketID != nil {
_ = n.n.SetVerdict(*a.PacketID, verdict)
}
return 0
}
p := &nfqueuePacket{
@ -96,24 +222,59 @@ func (n *nfqueuePacketIO) Register(ctx context.Context, cb PacketCallback) error
streamID: ctIDFromCtBytes(*a.Ct),
data: *a.Payload,
}
// Use timestamp from attribute if available, otherwise use current time as fallback
if a.Timestamp != nil {
p.timestamp = *a.Timestamp
} else {
p.timestamp = time.Now()
}
return okBoolToInt(cb(p, nil))
},
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))
})
if err != nil {
return err
}
if !n.iptSet {
err = n.setupIpt(n.local, false)
if !n.rSet {
if n.ipt4 != nil {
err = n.setupIpt(false)
} else {
err = n.setupNft(false)
}
if err != nil {
return err
}
n.iptSet = true
n.rSet = true
}
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 {
nP, ok := p.(*nfqueuePacket)
if !ok {
@ -125,25 +286,62 @@ func (n *nfqueuePacketIO) SetVerdict(p Packet, v Verdict, newPacket []byte) erro
case VerdictAcceptModify:
return n.n.SetVerdictModPacket(nP.id, nfqueue.NfAccept, newPacket)
case VerdictAcceptStream:
return n.n.SetVerdictWithConnMark(nP.id, nfqueue.NfAccept, nfqueueConnMarkAccept)
return n.n.SetVerdictWithConnMark(nP.id, nfqueue.NfAccept, n.connMarkAccept)
case VerdictDrop:
return n.n.SetVerdict(nP.id, nfqueue.NfDrop)
case VerdictDropStream:
return n.n.SetVerdictWithConnMark(nP.id, nfqueue.NfDrop, nfqueueConnMarkDrop)
return n.n.SetVerdictWithConnMark(nP.id, nfqueue.NfDrop, n.connMarkDrop)
default:
// Invalid verdict, ignore for now
return nil
}
}
func (n *nfqueuePacketIO) setupIpt(local, remove bool) error {
var rules []iptRule
if local {
rules = iptRulesLocal
} else {
rules = iptRulesForward
func (n *nfqueuePacketIO) ProtectedDialContext(ctx context.Context, network, address string) (net.Conn, error) {
return n.protectedDialer.DialContext(ctx, network, address)
}
func (n *nfqueuePacketIO) Close() error {
if n.rSet {
if n.ipt4 != nil {
_ = n.setupIpt(true)
} else {
_ = n.setupNft(true)
}
n.rSet = false
}
return n.n.Close()
}
// nfqueue IO does not issue shutdown
func (n *nfqueuePacketIO) SetCancelFunc(cancelFunc context.CancelFunc) error {
return nil
}
func (n *nfqueuePacketIO) setupNft(remove bool) error {
rules, err := n.generateNftRules()
if err != nil {
return err
}
rulesText := rules.String()
if remove {
err = nftDelete(nftFamily, n.table)
} else {
// Delete first to make sure no leftover rules
_ = nftDelete(nftFamily, n.table)
err = nftAdd(rulesText)
}
if err != nil {
return err
}
return nil
}
func (n *nfqueuePacketIO) setupIpt(remove bool) error {
rules, err := n.generateIptRules()
if err != nil {
return err
}
var err error
if remove {
err = iptsBatchDeleteIfExists([]*iptables.IPTables{n.ipt4, n.ipt6}, rules)
} else {
@ -155,28 +353,23 @@ func (n *nfqueuePacketIO) setupIpt(local, remove bool) error {
return nil
}
func (n *nfqueuePacketIO) Close() error {
if n.iptSet {
err := n.setupIpt(n.local, true)
if err != nil {
return err
}
}
return n.n.Close()
}
var _ Packet = (*nfqueuePacket)(nil)
type nfqueuePacket struct {
id uint32
streamID uint32
data []byte
id uint32
streamID uint32
timestamp time.Time
data []byte
}
func (p *nfqueuePacket) StreamID() uint32 {
return p.streamID
}
func (p *nfqueuePacket) Timestamp() time.Time {
return p.timestamp
}
func (p *nfqueuePacket) Data() []byte {
return p.data
}
@ -189,6 +382,61 @@ 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 nftTableSpec struct {
Defines []string
Family, Table string
Chains []nftChainSpec
}
func (t *nftTableSpec) String() string {
chains := make([]string, 0, len(t.Chains))
for _, c := range t.Chains {
chains = append(chains, c.String())
}
return fmt.Sprintf(`
%s
table %s %s {
%s
}
`, strings.Join(t.Defines, "\n"), t.Family, t.Table, strings.Join(chains, ""))
}
type nftChainSpec struct {
Chain string
Header string
Rules []string
}
func (c *nftChainSpec) String() string {
return fmt.Sprintf(`
chain %s {
%s
%s
}
`, c.Chain, c.Header, strings.Join(c.Rules, "\n\x20\x20\x20\x20"))
}
type iptRule struct {
Table, Chain string
RuleSpec []string

136
io/pcap.go Normal file
View File

@ -0,0 +1,136 @@
package io
import (
"context"
"hash/crc32"
"io"
"net"
"os"
"sort"
"strings"
"time"
"github.com/google/gopacket"
"github.com/google/gopacket/pcapgo"
)
var _ PacketIO = (*pcapPacketIO)(nil)
type pcapPacketIO struct {
pcapFile io.ReadCloser
pcap *pcapgo.Reader
timeOffset *time.Duration
ioCancel context.CancelFunc
config PcapPacketIOConfig
dialer *net.Dialer
}
type PcapPacketIOConfig struct {
PcapFile string
Realtime bool
}
func NewPcapPacketIO(config PcapPacketIOConfig) (PacketIO, error) {
pcapFile, err := os.Open(config.PcapFile)
if err != nil {
return nil, err
}
handle, err := pcapgo.NewReader(pcapFile)
if err != nil {
return nil, err
}
return &pcapPacketIO{
pcapFile: pcapFile,
pcap: handle,
timeOffset: nil,
ioCancel: nil,
config: config,
dialer: &net.Dialer{},
}, nil
}
func (p *pcapPacketIO) Register(ctx context.Context, cb PacketCallback) error {
go func() {
packetSource := gopacket.NewPacketSource(p.pcap, p.pcap.LinkType())
for packet := range packetSource.Packets() {
p.wait(packet)
networkLayer := packet.NetworkLayer()
if networkLayer != nil {
src, dst := networkLayer.NetworkFlow().Endpoints()
endpoints := []string{src.String(), dst.String()}
sort.Strings(endpoints)
id := crc32.Checksum([]byte(strings.Join(endpoints, ",")), crc32.IEEETable)
cb(&pcapPacket{
streamID: id,
timestamp: packet.Metadata().Timestamp,
data: packet.LinkLayer().LayerPayload(),
}, nil)
}
}
// Give the workers a chance to finish everything
time.Sleep(time.Second)
// Stop the engine when all packets are finished
p.ioCancel()
}()
return nil
}
// A normal dialer is sufficient as pcap IO does not mess up with the networking
func (p *pcapPacketIO) ProtectedDialContext(ctx context.Context, network, address string) (net.Conn, error) {
return p.dialer.DialContext(ctx, network, address)
}
func (p *pcapPacketIO) SetVerdict(pkt Packet, v Verdict, newPacket []byte) error {
return nil
}
func (p *pcapPacketIO) SetCancelFunc(cancelFunc context.CancelFunc) error {
p.ioCancel = cancelFunc
return nil
}
func (p *pcapPacketIO) Close() error {
return p.pcapFile.Close()
}
// Intentionally slow down the replay
// In realtime mode, this is to match the timestamps in the capture
func (p *pcapPacketIO) wait(packet gopacket.Packet) {
if !p.config.Realtime {
return
}
if p.timeOffset == nil {
offset := time.Since(packet.Metadata().Timestamp)
p.timeOffset = &offset
} else {
t := time.Until(packet.Metadata().Timestamp.Add(*p.timeOffset))
time.Sleep(t)
}
}
var _ Packet = (*pcapPacket)(nil)
type pcapPacket struct {
streamID uint32
timestamp time.Time
data []byte
}
func (p *pcapPacket) StreamID() uint32 {
return p.streamID
}
func (p *pcapPacket) Timestamp() time.Time {
return p.timestamp
}
func (p *pcapPacket) Data() []byte {
return p.data
}

18
ruleset/builtins/cidr.go Normal file
View 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
}

View File

@ -49,7 +49,7 @@ func (l *V2GeoLoader) shouldDownload(filename string) bool {
if os.IsNotExist(err) {
return true
}
dt := time.Now().Sub(info.ModTime())
dt := time.Since(info.ModTime())
if l.UpdateInterval == 0 {
return dt > geoDefaultUpdateInterval
} else {

View File

@ -14,14 +14,12 @@ type GeoMatcher struct {
ipMatcherLock sync.Mutex
}
func NewGeoMatcher(geoSiteFilename, geoIpFilename string) (*GeoMatcher, error) {
geoLoader := NewDefaultGeoLoader(geoSiteFilename, geoIpFilename)
func NewGeoMatcher(geoSiteFilename, geoIpFilename string) *GeoMatcher {
return &GeoMatcher{
geoLoader: geoLoader,
geoLoader: NewDefaultGeoLoader(geoSiteFilename, geoIpFilename),
geoSiteMatcher: make(map[string]hostMatcher),
geoIpMatcher: make(map[string]hostMatcher),
}, nil
}
}
func (g *GeoMatcher) MatchGeoIp(ip, condition string) bool {

View File

@ -1,54 +0,0 @@
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},
},
},
})
}

View File

@ -1,10 +1,15 @@
package ruleset
import (
"context"
"fmt"
"net"
"os"
"reflect"
"strings"
"time"
"github.com/expr-lang/expr/builtin"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/ast"
@ -14,13 +19,14 @@ import (
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/modifier"
"github.com/apernet/OpenGFW/ruleset/builtins/geo"
"github.com/apernet/OpenGFW/ruleset/builtins"
)
// ExprRule is the external representation of an expression rule.
type ExprRule struct {
Name string `yaml:"name"`
Action string `yaml:"action"`
Log bool `yaml:"log"`
Modifier ModifierEntry `yaml:"modifier"`
Expr string `yaml:"expr"`
}
@ -43,7 +49,8 @@ func ExprRulesFromYAML(file string) ([]ExprRule, error) {
// compiledExprRule is the internal, compiled representation of an expression rule.
type compiledExprRule struct {
Name string
Action Action
Action *Action // fallthrough if nil
Log bool
ModInstance modifier.Instance
Program *vm.Program
}
@ -51,34 +58,40 @@ type compiledExprRule struct {
var _ Ruleset = (*exprRuleset)(nil)
type exprRuleset struct {
Rules []compiledExprRule
Ans []analyzer.Analyzer
GeoMatcher *geo.GeoMatcher
Rules []compiledExprRule
Ans []analyzer.Analyzer
Logger Logger
}
func (r *exprRuleset) Analyzers(info StreamInfo) []analyzer.Analyzer {
return r.Ans
}
func (r *exprRuleset) Match(info StreamInfo) (MatchResult, error) {
func (r *exprRuleset) Match(info StreamInfo) MatchResult {
env := streamInfoToExprEnv(info)
for _, rule := range r.Rules {
v, err := vm.Run(rule.Program, env)
if err != nil {
return MatchResult{
Action: ActionMaybe,
}, fmt.Errorf("rule %q failed to run: %w", rule.Name, err)
// Log the error and continue to the next rule.
r.Logger.MatchError(info, rule.Name, err)
continue
}
if vBool, ok := v.(bool); ok && vBool {
return MatchResult{
Action: rule.Action,
ModInstance: rule.ModInstance,
}, nil
if rule.Log {
r.Logger.Log(info, rule.Name)
}
if rule.Action != nil {
return MatchResult{
Action: *rule.Action,
ModInstance: rule.ModInstance,
}
}
}
}
// No match
return MatchResult{
Action: ActionMaybe,
}, nil
}
}
// CompileExprRules compiles a list of expression rules into a ruleset.
@ -89,57 +102,66 @@ func CompileExprRules(rules []ExprRule, ans []analyzer.Analyzer, mods []modifier
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
}
funcMap := buildFunctionMap(config)
// 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)
if rule.Action == "" && !rule.Log {
return nil, fmt.Errorf("rule %q must have at least one of action or log", rule.Name)
}
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{FuncMap: funcMap}
program, err := expr.Compile(rule.Expr,
func(c *conf.Config) {
c.Strict = false
c.Expect = reflect.Bool
c.Visitors = append(c.Visitors, visitor)
registerBuiltinFunctions(c.Functions, geoMatcher)
c.Visitors = append(c.Visitors, visitor, patcher)
for name, f := range funcMap {
c.Functions[name] = &builtin.Function{
Name: name,
Func: f.Func,
Types: f.Types,
}
}
},
)
if err != nil {
return nil, fmt.Errorf("rule %q has invalid expression: %w", rule.Name, err)
}
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) {
// Skip built-in analyzers & user-defined variables
if isBuiltInAnalyzer(name) || visitor.Variables[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)
}
default:
a, ok := fullAnMap[name]
if !ok {
return nil, fmt.Errorf("rule %q uses unknown analyzer %q", rule.Name, name)
if f, ok := funcMap[name]; ok {
// Built-in function, initialize if necessary
if f.InitFunc != nil {
if err := f.InitFunc(); err != nil {
return nil, fmt.Errorf("rule %q failed to initialize function %q: %w", rule.Name, name, err)
}
}
} else if a, ok := fullAnMap[name]; ok {
// Analyzer, add to dependency map
depAnMap[name] = a
}
}
cr := compiledExprRule{
Name: rule.Name,
Action: action,
Log: rule.Log,
Program: program,
}
if action == ActionModify {
if action != nil && *action == ActionModify {
mod, ok := fullModMap[rule.Modifier.Name]
if !ok {
return nil, fmt.Errorf("rule %q uses unknown modifier %q", rule.Name, rule.Modifier.Name)
@ -158,29 +180,12 @@ func CompileExprRules(rules []ExprRule, ans []analyzer.Analyzer, mods []modifier
depAns = append(depAns, a)
}
return &exprRuleset{
Rules: compiledRules,
Ans: depAns,
GeoMatcher: geoMatcher,
Rules: compiledRules,
Ans: depAns,
Logger: config.Logger,
}, 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)},
}
}
func streamInfoToExprEnv(info StreamInfo) map[string]interface{} {
m := map[string]interface{}{
"id": info.ID,
@ -247,12 +252,126 @@ func modifiersToMap(mods []modifier.Modifier) map[string]modifier.Modifier {
return modMap
}
// 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 {
Variables map[string]bool
Identifiers map[string]bool
}
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
}
}
// idPatcher patches the AST during expr compilation, replacing certain values with
// their internal representations for better runtime performance.
type idPatcher struct {
FuncMap map[string]*Function
Err error
}
func (p *idPatcher) Visit(node *ast.Node) {
switch (*node).(type) {
case *ast.CallNode:
callNode := (*node).(*ast.CallNode)
if callNode.Callee == nil {
// Ignore invalid call nodes
return
}
if f, ok := p.FuncMap[callNode.Callee.String()]; ok {
if f.PatchFunc != nil {
if err := f.PatchFunc(&callNode.Arguments); err != nil {
p.Err = err
return
}
}
}
}
}
type Function struct {
InitFunc func() error
PatchFunc func(args *[]ast.Node) error
Func func(params ...any) (any, error)
Types []reflect.Type
}
func buildFunctionMap(config *BuiltinConfig) map[string]*Function {
return map[string]*Function{
"geoip": {
InitFunc: config.GeoMatcher.LoadGeoIP,
PatchFunc: nil,
Func: func(params ...any) (any, error) {
return config.GeoMatcher.MatchGeoIp(params[0].(string), params[1].(string)), nil
},
Types: []reflect.Type{reflect.TypeOf(config.GeoMatcher.MatchGeoIp)},
},
"geosite": {
InitFunc: config.GeoMatcher.LoadGeoSite,
PatchFunc: nil,
Func: func(params ...any) (any, error) {
return config.GeoMatcher.MatchGeoSite(params[0].(string), params[1].(string)), nil
},
Types: []reflect.Type{reflect.TypeOf(config.GeoMatcher.MatchGeoSite)},
},
"cidr": {
InitFunc: nil,
PatchFunc: func(args *[]ast.Node) error {
cidrStringNode, ok := (*args)[1].(*ast.StringNode)
if !ok {
return fmt.Errorf("cidr: invalid argument type")
}
cidr, err := builtins.CompileCIDR(cidrStringNode.Value)
if err != nil {
return err
}
(*args)[1] = &ast.ConstantNode{Value: cidr}
return nil
},
Func: func(params ...any) (any, error) {
return builtins.MatchCIDR(params[0].(string), params[1].(*net.IPNet)), nil
},
Types: []reflect.Type{reflect.TypeOf(builtins.MatchCIDR)},
},
"lookup": {
InitFunc: nil,
PatchFunc: func(args *[]ast.Node) error {
var serverStr *ast.StringNode
if len(*args) > 1 {
// Has the optional server argument
var ok bool
serverStr, ok = (*args)[1].(*ast.StringNode)
if !ok {
return fmt.Errorf("lookup: invalid argument type")
}
}
r := &net.Resolver{
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
if serverStr != nil {
address = serverStr.Value
}
return config.ProtectedDialContext(ctx, network, address)
},
}
if len(*args) > 1 {
(*args)[1] = &ast.ConstantNode{Value: r}
} else {
*args = append(*args, &ast.ConstantNode{Value: r})
}
return nil
},
Func: func(params ...any) (any, error) {
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
defer cancel()
return params[1].(*net.Resolver).LookupHost(ctx, params[0].(string))
},
Types: []reflect.Type{
reflect.TypeOf((func(string, *net.Resolver) []string)(nil)),
},
},
}
}

View File

@ -1,11 +1,13 @@
package ruleset
import (
"context"
"net"
"strconv"
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/modifier"
"github.com/apernet/OpenGFW/ruleset/builtins/geo"
)
type Action int
@ -90,10 +92,17 @@ type Ruleset interface {
Analyzers(StreamInfo) []analyzer.Analyzer
// Match matches a stream against the ruleset and returns the result.
// 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 {
GeoSiteFilename string
GeoIpFilename string
Logger Logger
GeoMatcher *geo.GeoMatcher
ProtectedDialContext func(ctx context.Context, network, address string) (net.Conn, error)
}