1 Commits
Author SHA1 Message Date
Mxmilu666 5bd0630815 first commit 2026-02-01 10:26:44 +08:00
73 changed files with 1 additions and 12409 deletions
-47
View File
@@ -1,47 +0,0 @@
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 ./...
-39
View File
@@ -1,39 +0,0 @@
name: Release
on:
release:
types: [published]
permissions:
contents: write
packages: write
jobs:
releases-matrix:
name: Release Go Binary
runs-on: ubuntu-latest
strategy:
matrix:
goos: [linux]
goarch: ["386", amd64, arm64]
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
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/*
-210
View File
@@ -1,210 +0,0 @@
# Created by https://www.toptal.com/developers/gitignore/api/windows,macos,linux,go,goland+all,visualstudiocode
# Edit at https://www.toptal.com/developers/gitignore?templates=windows,macos,linux,go,goland+all,visualstudiocode
### Go ###
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
### GoLand+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# AWS User-specific
.idea/**/aws.xml
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### GoLand+all Patch ###
# Ignore everything but code style settings and run configurations
# that are supposed to be shared within teams.
.idea/*
!.idea/codeStyles
!.idea/runConfigurations
### Linux ###
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### macOS Patch ###
# iCloud generated files
*.icloud
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
### Windows ###
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.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/
-373
View File
@@ -1,373 +0,0 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
-41
View File
@@ -1,41 +0,0 @@
# ![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 は、あなた専用の DIY 中国のグレートファイアウォール (https://en.wikipedia.org/wiki/Great_Firewall) です。Linux 上で利用可能な柔軟で使いやすいオープンソースプログラムとして提供されています。なぜ権力者だけが楽しむのでしょうか?権力を人々に与え、検閲を民主化する時が来ました。自宅のルーターにサイバー主権のスリルをもたらし、プロのようにフィルタリングを始めましょう - あなたもビッグブラザーになることができます。
**ドキュメントウェブサイト: https://gfw.dev/**
Telegram グループ: https://t.me/OpGFW
> [!CAUTION]
> 本プロジェクトはまだ初期開発段階にあります。テスト時のリスクは自己責任でお願いします。私たちは、このプロジェクトを一緒に改善するために貢献者を探しています。
## 特徴
- フル IP/TCP 再アセンブル、各種プロトコルアナライザー
- HTTP、TLS、QUIC、DNS、SSH、SOCKS4/5、WireGuard、OpenVPN、Minecraft、EasyTier、その他多数
- Shadowsocks、VMess の「完全に暗号化されたトラフィック」の検出など (https://gfw.report/publications/usenixsecurity23/en/)
- Trojan プロキシプロトコルの検出
- [WIP] 機械学習に基づくトラフィック分類
- IPv4 と IPv6 をフルサポート
- フローベースのマルチコア負荷分散
- 接続オフロード
- [expr](https://github.com/expr-lang/expr) に基づく強力なルールエンジン
- ルールのホットリロード (`SIGHUP` を送信してリロード)
- 柔軟なアナライザ&モディファイアフレームワーク
- 拡張可能な IO 実装 (今のところ NFQueue のみ)
- [WIP] ウェブ UI
## ユースケース
- 広告ブロック
- ペアレンタルコントロール
- マルウェア対策
- VPN/プロキシサービスの不正利用防止
- トラフィック分析(ログのみモード)
- 独裁的な野心を実現するのを助ける
+1 -48
View File
@@ -1,48 +1 @@
# ![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
**[中文文档](README.zh.md)**
**[日本語ドキュメント](README.ja.md)**
> [!CAUTION]
> 此版本为 LoliaFRP 二次开发版本,加入了更多的识别规则,如果您正在寻找原版请移步 original 分支
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. We are looking for contributors to help us improve and expand the project.
## Features
- Full IP/TCP reassembly, various protocol analyzers
- HTTP, TLS, QUIC, DNS, SSH, SOCKS4/5, WireGuard, OpenVPN, Minecraft, EasyTier, 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
- Connection offloading
- Powerful rule engine based on [expr](https://github.com/expr-lang/expr)
- Hot-reloadable rules (send `SIGHUP` to reload)
- Flexible analyzer & modifier framework
- Extensible IO implementation (only NFQueue for now)
- [WIP] Web UI
## Use cases
- Ad blocking
- Parental control
- Malware protection
- Abuse prevention for VPN/proxy services
- Traffic analysis (log only mode)
- Help you fulfill your dictatorial ambitions
# 你说的对,但是《原神》是由米哈游自主研发的一款全新开放世界冒险游戏。游戏发生在一个被称作「提瓦特」的幻想世界,在这里,被神选中的人将被授予「神之眼」,导引元素之力。你将扮演一位名为「旅行者」的神秘角色,在自由的旅行中邂逅性格各异、能力独特的同伴们,和他们一起击败强敌,找回失散的亲人——同时,逐步发掘「原神」的真相。
-52
View File
@@ -1,52 +0,0 @@
# ![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 上灵活、易用、开源的 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]
> 本项目仍处于早期开发阶段。测试时自行承担风险。我们正在寻求贡献者一起完善本项目。
> [!CAUTION]
> 此版本为 LoliaFRP 二次开发版本,加入了更多的识别规则,如果您正在寻找原版请移步 original 分支
## 功能
- 完整的 IP/TCP 重组,各种协议解析器
- HTTP, TLS, QUIC, DNS, SSH, SOCKS4/5, WireGuard, OpenVPN, Minecraft, EasyTier, 更多协议正在开发中
- Shadowsocks, VMess 等 "全加密流量" 检测 (https://gfw.report/publications/usenixsecurity23/zh/)
- Trojan 协议检测
- [开发中] 基于机器学习的流量分类
- 同等支持 IPv4 和 IPv6
- 基于流的多核负载均衡
- 连接 offloading
- 基于 [expr](https://github.com/expr-lang/expr) 的强大规则引擎
- 规则可以热重载 (发送 `SIGHUP` 信号)
- 灵活的协议解析和修改框架
- 可扩展的 IO 实现 (目前只有 NFQueue)
- [开发中] Web UI
## 本仓库内的文档
- [Minecraft 分析器](docs/minecraft.zh.md)
- [EasyTier 分析器](docs/easytier.zh.md)
- [TCP 窗口操纵规避与检测](docs/tcp-window-evasion.zh.md)
- [TLS record 分片规避与检测](docs/tls-record-fragmentation.zh.md)
- [拦截 TLS、放行 RDP](docs/rdp-tls.zh.md)
## 使用场景
- 广告拦截
- 家长控制
- 恶意软件防护
- VPN/代理服务滥用防护
- 流量分析 (纯日志模式)
- 助你实现你的独裁野心
-131
View File
@@ -1,131 +0,0 @@
package analyzer
import (
"net"
"strings"
)
type Analyzer interface {
// Name returns the name of the analyzer.
Name() string
// Limit returns the byte limit for this analyzer.
// For example, an analyzer can return 1000 to indicate that it only ever needs
// the first 1000 bytes of a stream to do its job. If the stream is still not
// done after 1000 bytes, the engine will stop feeding it data and close it.
// An analyzer can return 0 or a negative number to indicate that it does not
// have a hard limit.
// Note: for UDP streams, the engine always feeds entire packets, even if
// the packet is larger than the remaining quota or the limit itself.
Limit() int
}
type Logger interface {
Debugf(format string, args ...interface{})
Infof(format string, args ...interface{})
Errorf(format string, args ...interface{})
}
type TCPAnalyzer interface {
Analyzer
// NewTCP returns a new TCPStream.
NewTCP(TCPInfo, Logger) TCPStream
}
type TCPInfo struct {
// SrcIP is the source IP address.
SrcIP net.IP
// DstIP is the destination IP address.
DstIP net.IP
// SrcPort is the source port.
SrcPort uint16
// DstPort is the destination port.
DstPort uint16
}
type TCPStream interface {
// Feed feeds a chunk of reassembled data to the stream.
// It returns a prop update containing the information extracted from the stream (can be nil),
// and whether the analyzer is "done" with this stream (i.e. no more data should be fed).
Feed(rev, start, end bool, skip int, data []byte) (u *PropUpdate, done bool)
// Close indicates that the stream is closed.
// Either the connection is closed, or the stream has reached its byte limit.
// Like Feed, it optionally returns a prop update.
Close(limited bool) *PropUpdate
}
type UDPAnalyzer interface {
Analyzer
// NewUDP returns a new UDPStream.
NewUDP(UDPInfo, Logger) UDPStream
}
type UDPInfo struct {
// SrcIP is the source IP address.
SrcIP net.IP
// DstIP is the destination IP address.
DstIP net.IP
// SrcPort is the source port.
SrcPort uint16
// DstPort is the destination port.
DstPort uint16
}
type UDPStream interface {
// Feed feeds a new packet to the stream.
// It returns a prop update containing the information extracted from the stream (can be nil),
// and whether the analyzer is "done" with this stream (i.e. no more data should be fed).
Feed(rev bool, data []byte) (u *PropUpdate, done bool)
// Close indicates that the stream is closed.
// Either the connection is closed, or the stream has reached its byte limit.
// Like Feed, it optionally returns a prop update.
Close(limited bool) *PropUpdate
}
type (
PropMap map[string]interface{}
CombinedPropMap map[string]PropMap
)
// Get returns the value of the property with the given key.
// The key can be a nested key, e.g. "foo.bar.baz".
// Returns nil if the key does not exist.
func (m PropMap) Get(key string) interface{} {
keys := strings.Split(key, ".")
if len(keys) == 0 {
return nil
}
var current interface{} = m
for _, k := range keys {
currentMap, ok := current.(PropMap)
if !ok {
return nil
}
current = currentMap[k]
}
return current
}
// Get returns the value of the property with the given analyzer & key.
// The key can be a nested key, e.g. "foo.bar.baz".
// Returns nil if the key does not exist.
func (cm CombinedPropMap) Get(an string, key string) interface{} {
m, ok := cm[an]
if !ok {
return nil
}
return m.Get(key)
}
type PropUpdateType int
const (
PropUpdateNone PropUpdateType = iota
PropUpdateMerge
PropUpdateReplace
PropUpdateDelete
)
type PropUpdate struct {
Type PropUpdateType
M PropMap
}
-224
View File
@@ -1,224 +0,0 @@
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
}
-207
View File
@@ -1,207 +0,0 @@
package tcp
import (
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/analyzer/utils"
)
const (
// fetMinDataLen is how many bytes we want before classifying a stream.
// The metrics below are meaningless on a handful of bytes - a peer can
// force a tiny first segment by advertising a small TCP receive window in
// its SYN-ACK, which used to be enough to make us exempt the connection.
fetMinDataLen = 16
// fetMaxSegments bounds how long we wait for fetMinDataLen bytes. Without
// it, a peer that sends one byte and then goes quiet would keep the stream
// active - and therefore out of the connmark bypass - until the TCP timeout.
fetMaxSegments = 4
)
var _ analyzer.TCPAnalyzer = (*FETAnalyzer)(nil)
// FETAnalyzer stands for "Fully Encrypted Traffic" analyzer.
// It implements an algorithm to detect fully encrypted proxy protocols
// such as Shadowsocks, mentioned in the following paper:
// https://gfw.report/publications/usenixsecurity23/data/paper/paper.pdf
type FETAnalyzer struct{}
func (a *FETAnalyzer) Name() string {
return "fet"
}
func (a *FETAnalyzer) Limit() int {
// We only really look at the first packet
return 8192
}
func (a *FETAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) analyzer.TCPStream {
return newFETStream(logger)
}
type fetStream struct {
logger analyzer.Logger
buf utils.ByteBuffer
segs int
}
func newFETStream(logger analyzer.Logger) *fetStream {
return &fetStream{logger: logger}
}
func (s *fetStream) Feed(rev, start, end bool, skip int, data []byte) (u *analyzer.PropUpdate, done bool) {
if skip != 0 {
return nil, true
}
if len(data) == 0 {
return nil, false
}
if rev {
// We classify what the client sends first. If the server speaks before
// the client does, this isn't a handshake we can say anything about.
if s.buf.Len() == 0 {
return nil, true
}
return nil, false
}
s.segs++
if s.buf.Len() == 0 && len(data) >= fetMinDataLen {
// Enough to decide on, and nothing buffered yet - classify in place.
return fetResult(data), true
}
s.buf.Append(data)
if s.buf.Len() < fetMinDataLen && s.segs < fetMaxSegments {
return nil, false
}
u = fetResult(s.buf.Buf)
s.buf.Reset()
return u, true
}
func (s *fetStream) Close(limited bool) *analyzer.PropUpdate {
if s.buf.Len() == 0 {
return nil
}
u := fetResult(s.buf.Buf)
s.buf.Reset()
return u
}
func fetResult(data []byte) *analyzer.PropUpdate {
ex1 := averagePopCount(data)
ex2 := isFirstSixPrintable(data)
ex3 := printablePercentage(data)
ex4 := contiguousPrintable(data)
ex5 := isTLSorHTTP(data)
exempt := (ex1 <= 3.4 || ex1 >= 4.6) || ex2 || ex3 > 0.5 || ex4 > 20 || ex5
return &analyzer.PropUpdate{
Type: analyzer.PropUpdateReplace,
M: analyzer.PropMap{
"ex1": ex1,
"ex2": ex2,
"ex3": ex3,
"ex4": ex4,
"ex5": ex5,
"yes": !exempt,
},
}
}
func popCount(b byte) int {
count := 0
for b != 0 {
count += int(b & 1)
b >>= 1
}
return count
}
// averagePopCount returns the average popcount of the given bytes.
// This is the "Ex1" metric in the paper.
func averagePopCount(bytes []byte) float32 {
if len(bytes) == 0 {
return 0
}
total := 0
for _, b := range bytes {
total += popCount(b)
}
return float32(total) / float32(len(bytes))
}
// isFirstSixPrintable returns true if the first six bytes are printable ASCII.
// This is the "Ex2" metric in the paper.
func isFirstSixPrintable(bytes []byte) bool {
if len(bytes) < 6 {
return false
}
for i := range bytes[:6] {
if !isPrintable(bytes[i]) {
return false
}
}
return true
}
// printablePercentage returns the percentage of printable ASCII bytes.
// This is the "Ex3" metric in the paper.
func printablePercentage(bytes []byte) float32 {
if len(bytes) == 0 {
return 0
}
count := 0
for i := range bytes {
if isPrintable(bytes[i]) {
count++
}
}
return float32(count) / float32(len(bytes))
}
// contiguousPrintable returns the length of the longest contiguous sequence of
// printable ASCII bytes.
// This is the "Ex4" metric in the paper.
func contiguousPrintable(bytes []byte) int {
if len(bytes) == 0 {
return 0
}
maxCount := 0
current := 0
for i := range bytes {
if isPrintable(bytes[i]) {
current++
} else {
if current > maxCount {
maxCount = current
}
current = 0
}
}
if current > maxCount {
maxCount = current
}
return maxCount
}
// isTLSorHTTP returns true if the given bytes look like TLS or HTTP.
// This is the "Ex5" metric in the paper.
func isTLSorHTTP(bytes []byte) bool {
if len(bytes) < 3 {
return false
}
// "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
str := string(bytes[:3])
return str == "GET" || str == "HEA" || str == "POS" ||
str == "PUT" || str == "DEL" || str == "CON" ||
str == "OPT" || str == "TRA" || str == "PAT"
}
func isPrintable(b byte) bool {
return b >= 0x20 && b <= 0x7e
}
-115
View File
@@ -1,115 +0,0 @@
package tcp
import (
"math/rand"
"testing"
"github.com/apernet/OpenGFW/analyzer"
)
// feedFET runs a sequence of client-to-server segments through a fresh stream
// and returns the props it settled on, or nil if it never reached a verdict.
func feedFET(t *testing.T, segments ...[]byte) analyzer.PropMap {
t.Helper()
s := newFETStream(nil)
for _, seg := range segments {
u, done := s.Feed(false, false, false, 0, seg)
if done {
if u == nil {
return nil
}
return u.M
}
}
if u := s.Close(false); u != nil {
return u.M
}
return nil
}
// encryptedSample returns deterministic high-entropy bytes, standing in for the
// first segment of a fully encrypted proxy protocol such as Shadowsocks.
func encryptedSample(n int) []byte {
r := rand.New(rand.NewSource(1))
b := make([]byte, n)
for i := range b {
b[i] = byte(r.Intn(256))
}
return b
}
func tlsClientHelloSample() []byte {
return append([]byte{0x16, 0x03, 0x01, 0x02, 0x00, 0x01, 0x00, 0x01, 0xfc}, encryptedSample(64)...)
}
// A peer can force a one-byte first segment by advertising a tiny TCP receive
// window in its SYN-ACK. The verdict must not depend on where the boundary
// falls: classifying the first segment alone used to exempt these streams.
func TestFETVerdictIsSegmentationIndependent(t *testing.T) {
samples := map[string][]byte{
"encrypted": encryptedSample(64),
"tls": tlsClientHelloSample(),
"http": []byte("GET /index.html HTTP/1.1\r\nHost: example.com\r\n\r\n"),
}
for name, data := range samples {
t.Run(name, func(t *testing.T) {
whole := feedFET(t, data)
if whole == nil {
t.Fatal("no verdict on unsplit data")
}
split := feedFET(t, data[:1], data[1:])
if got, want := split.Get("yes"), whole.Get("yes"); got != want {
t.Errorf("split verdict yes = %v, unsplit = %v", got, want)
}
})
}
}
func TestFETDetectsEncryptedFirstSegment(t *testing.T) {
data := encryptedSample(64)
if got := feedFET(t, data).Get("yes"); got != true {
t.Fatalf("unsplit: yes = %v, want true", got)
}
if got := feedFET(t, data[:1], data[1:]).Get("yes"); got != true {
t.Errorf("one-byte first segment: yes = %v, want true", got)
}
}
func TestFETExemptsTLS(t *testing.T) {
data := tlsClientHelloSample()
if got := feedFET(t, data).Get("yes"); got != false {
t.Fatalf("unsplit: yes = %v, want false", got)
}
if got := feedFET(t, data[:1], data[1:]).Get("yes"); got != false {
t.Errorf("one-byte first segment: yes = %v, want false", got)
}
}
// Waiting for more data must be bounded, or a peer that sends one byte and then
// stops would hold the stream open until the TCP timeout, keeping every packet
// of the connection in userspace.
func TestFETGivesUpAfterMaxSegments(t *testing.T) {
s := newFETStream(nil)
for i := 0; i < fetMaxSegments-1; i++ {
if _, done := s.Feed(false, false, false, 0, []byte{0x00}); done {
t.Fatalf("verdict reached after %d segments, want to keep waiting", i+1)
}
}
u, done := s.Feed(false, false, false, 0, []byte{0x00})
if !done || u == nil {
t.Fatalf("no verdict after %d segments", fetMaxSegments)
}
}
// The metrics only describe a client's opening bytes, so a server that speaks
// first should end the stream rather than be classified as if it were a client.
func TestFETServerSpeaksFirst(t *testing.T) {
s := newFETStream(nil)
u, done := s.Feed(true, false, false, 0, encryptedSample(64))
if !done {
t.Error("stream not done after server-first data")
}
if u != nil {
t.Errorf("props = %#v, want none", u.M)
}
}
-193
View File
@@ -1,193 +0,0 @@
package tcp
import (
"bytes"
"strconv"
"strings"
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/analyzer/utils"
)
var _ analyzer.TCPAnalyzer = (*HTTPAnalyzer)(nil)
type HTTPAnalyzer struct{}
func (a *HTTPAnalyzer) Name() string {
return "http"
}
func (a *HTTPAnalyzer) Limit() int {
return 8192
}
func (a *HTTPAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) analyzer.TCPStream {
return newHTTPStream(logger)
}
type httpStream struct {
logger analyzer.Logger
reqBuf *utils.ByteBuffer
reqMap analyzer.PropMap
reqUpdated bool
reqLSM *utils.LinearStateMachine
reqDone bool
respBuf *utils.ByteBuffer
respMap analyzer.PropMap
respUpdated bool
respLSM *utils.LinearStateMachine
respDone bool
}
func newHTTPStream(logger analyzer.Logger) *httpStream {
s := &httpStream{logger: logger, reqBuf: &utils.ByteBuffer{}, respBuf: &utils.ByteBuffer{}}
s.reqLSM = utils.NewLinearStateMachine(
s.parseRequestLine,
s.parseRequestHeaders,
)
s.respLSM = utils.NewLinearStateMachine(
s.parseResponseLine,
s.parseResponseHeaders,
)
return s
}
func (s *httpStream) 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 {
s.respBuf.Append(data)
s.respUpdated = false
cancelled, s.respDone = s.respLSM.Run()
if s.respUpdated {
update = &analyzer.PropUpdate{
Type: analyzer.PropUpdateMerge,
M: analyzer.PropMap{"resp": s.respMap},
}
s.respUpdated = false
}
} else {
s.reqBuf.Append(data)
s.reqUpdated = false
cancelled, s.reqDone = s.reqLSM.Run()
if s.reqUpdated {
update = &analyzer.PropUpdate{
Type: analyzer.PropUpdateMerge,
M: analyzer.PropMap{"req": s.reqMap},
}
s.reqUpdated = false
}
}
return update, cancelled || (s.reqDone && s.respDone)
}
func (s *httpStream) parseRequestLine() utils.LSMAction {
// Find the end of the request line
line, ok := s.reqBuf.GetUntil([]byte("\r\n"), true, true)
if !ok {
// No end of line yet, but maybe we just need more data
return utils.LSMActionPause
}
fields := strings.Fields(string(line[:len(line)-2])) // Strip \r\n
if len(fields) != 3 {
// Invalid request line
return utils.LSMActionCancel
}
method := fields[0]
path := fields[1]
version := fields[2]
if !strings.HasPrefix(version, "HTTP/") {
// Invalid version
return utils.LSMActionCancel
}
s.reqMap = analyzer.PropMap{
"method": method,
"path": path,
"version": version,
}
s.reqUpdated = true
return utils.LSMActionNext
}
func (s *httpStream) parseResponseLine() utils.LSMAction {
// Find the end of the response line
line, ok := s.respBuf.GetUntil([]byte("\r\n"), true, true)
if !ok {
// No end of line yet, but maybe we just need more data
return utils.LSMActionPause
}
fields := strings.Fields(string(line[:len(line)-2])) // Strip \r\n
if len(fields) < 2 {
// Invalid response line
return utils.LSMActionCancel
}
version := fields[0]
status, _ := strconv.Atoi(fields[1])
if !strings.HasPrefix(version, "HTTP/") || status == 0 {
// Invalid version
return utils.LSMActionCancel
}
s.respMap = analyzer.PropMap{
"version": version,
"status": status,
}
s.respUpdated = true
return utils.LSMActionNext
}
func (s *httpStream) parseHeaders(buf *utils.ByteBuffer) (utils.LSMAction, analyzer.PropMap) {
// Find the end of headers
headers, ok := buf.GetUntil([]byte("\r\n\r\n"), true, true)
if !ok {
// No end of headers yet, but maybe we just need more data
return utils.LSMActionPause, nil
}
headers = headers[:len(headers)-4] // Strip \r\n\r\n
headerMap := make(analyzer.PropMap)
for _, line := range bytes.Split(headers, []byte("\r\n")) {
fields := bytes.SplitN(line, []byte(":"), 2)
if len(fields) != 2 {
// Invalid header
return utils.LSMActionCancel, nil
}
key := string(bytes.TrimSpace(fields[0]))
value := string(bytes.TrimSpace(fields[1]))
// Normalize header keys to lowercase
headerMap[strings.ToLower(key)] = value
}
return utils.LSMActionNext, headerMap
}
func (s *httpStream) parseRequestHeaders() utils.LSMAction {
action, headerMap := s.parseHeaders(s.reqBuf)
if action == utils.LSMActionNext {
s.reqMap["headers"] = headerMap
s.reqUpdated = true
}
return action
}
func (s *httpStream) parseResponseHeaders() utils.LSMAction {
action, headerMap := s.parseHeaders(s.respBuf)
if action == utils.LSMActionNext {
s.respMap["headers"] = headerMap
s.respUpdated = true
}
return action
}
func (s *httpStream) Close(limited bool) *analyzer.PropUpdate {
s.reqBuf.Reset()
s.respBuf.Reset()
s.reqMap = nil
s.respMap = nil
return nil
}
-64
View File
@@ -1,64 +0,0 @@
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)
}
})
}
}
-179
View File
@@ -1,179 +0,0 @@
package tcp
import (
"unicode/utf8"
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/analyzer/utils"
)
const (
minecraftMaxHandshakePacketLen = 2048
minecraftMaxServerAddrLen = 255 * 4
minecraftPacketHandshake = 0
minecraftNextStateStatus = 1
minecraftNextStateLogin = 2
minecraftNextStateTransfer = 3
)
var _ analyzer.TCPAnalyzer = (*MinecraftAnalyzer)(nil)
type MinecraftAnalyzer struct{}
func (a *MinecraftAnalyzer) Name() string {
return "minecraft"
}
func (a *MinecraftAnalyzer) Limit() int {
return minecraftMaxHandshakePacketLen + 5
}
func (a *MinecraftAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) analyzer.TCPStream {
return newMinecraftStream(logger)
}
type minecraftStream struct {
logger analyzer.Logger
reqBuf *utils.ByteBuffer
}
func newMinecraftStream(logger analyzer.Logger) *minecraftStream {
return &minecraftStream{logger: logger, reqBuf: &utils.ByteBuffer{}}
}
func (s *minecraftStream) Feed(rev, start, end bool, skip int, data []byte) (u *analyzer.PropUpdate, done bool) {
if skip != 0 {
return minecraftResult(false, false, nil), true
}
if len(data) == 0 {
return nil, false
}
if rev {
return nil, false
}
s.reqBuf.Append(data)
m, needMore, valid := parseMinecraftHandshake(s.reqBuf.Buf)
if needMore {
return minecraftResult(false, true, nil), false
}
if !valid {
return minecraftResult(false, false, nil), true
}
return minecraftResult(true, false, m), true
}
func (s *minecraftStream) Close(limited bool) *analyzer.PropUpdate {
s.reqBuf.Reset()
return minecraftResult(false, false, nil)
}
func minecraftResult(yes, candidate bool, m analyzer.PropMap) *analyzer.PropUpdate {
if m == nil {
m = analyzer.PropMap{}
}
m["yes"] = yes
m["candidate"] = candidate
return &analyzer.PropUpdate{
Type: analyzer.PropUpdateReplace,
M: m,
}
}
func parseMinecraftHandshake(buf []byte) (m analyzer.PropMap, needMore, valid bool) {
packetLen, off, ok, invalid := readMinecraftVarInt(buf, 0)
if invalid || packetLen <= 0 || packetLen > minecraftMaxHandshakePacketLen {
return nil, false, false
}
if !ok {
return nil, true, false
}
packetEnd := off + int(packetLen)
if len(buf) < packetEnd {
return nil, true, false
}
packet := buf[off:packetEnd]
packetID, pos, ok, invalid := readMinecraftVarInt(packet, 0)
if invalid || !ok || packetID != minecraftPacketHandshake {
return nil, false, false
}
protocolVersion, pos, ok, invalid := readMinecraftVarInt(packet, pos)
if invalid || !ok {
return nil, false, false
}
addrLen, pos, ok, invalid := readMinecraftVarInt(packet, pos)
if invalid || !ok || addrLen <= 0 || addrLen > minecraftMaxServerAddrLen {
return nil, false, false
}
addrEnd := pos + int(addrLen)
if addrEnd+2 > len(packet) {
return nil, false, false
}
addrBytes := packet[pos:addrEnd]
if !utf8.Valid(addrBytes) {
return nil, false, false
}
pos = addrEnd
serverPort := uint16(packet[pos])<<8 | uint16(packet[pos+1])
pos += 2
nextState, pos, ok, invalid := readMinecraftVarInt(packet, pos)
if invalid || !ok || !isMinecraftNextState(nextState) || pos != len(packet) {
return nil, false, false
}
return analyzer.PropMap{
"protocol": int(protocolVersion),
"server_addr": string(addrBytes),
"server_port": serverPort,
"next_state": int(nextState),
"next_state_name": minecraftNextStateName(nextState),
"packet_length": int(packetLen),
}, false, true
}
func readMinecraftVarInt(buf []byte, offset int) (value int32, next int, ok bool, invalid bool) {
var result int32
for i := 0; i < 5; i++ {
if offset+i >= len(buf) {
return 0, offset, false, false
}
b := buf[offset+i]
result |= int32(b&0x7f) << (7 * i)
if b&0x80 == 0 {
return result, offset + i + 1, true, false
}
}
return 0, offset, false, true
}
func isMinecraftNextState(state int32) bool {
switch state {
case minecraftNextStateStatus, minecraftNextStateLogin, minecraftNextStateTransfer:
return true
default:
return false
}
}
func minecraftNextStateName(state int32) string {
switch state {
case minecraftNextStateStatus:
return "status"
case minecraftNextStateLogin:
return "login"
case minecraftNextStateTransfer:
return "transfer"
default:
return "unknown"
}
}
-119
View File
@@ -1,119 +0,0 @@
package tcp
import (
"reflect"
"testing"
"github.com/apernet/OpenGFW/analyzer"
)
func TestMinecraftParsing_HandshakeLogin(t *testing.T) {
handshake := buildMinecraftHandshake(t, 767, "211.136.162.178", 25565, minecraftNextStateLogin)
want := analyzer.PropMap{
"yes": true,
"candidate": false,
"protocol": 767,
"server_addr": "211.136.162.178",
"server_port": uint16(25565),
"next_state": minecraftNextStateLogin,
"next_state_name": "login",
"packet_length": len(handshake) - 1,
}
u, done := newMinecraftStream(nil).Feed(false, false, false, 0, handshake)
if !done {
t.Fatal("stream not done after full handshake")
}
if !reflect.DeepEqual(u.M, want) {
t.Errorf("parsed = %#v, want %#v", u.M, want)
}
}
func TestMinecraftParsing_FragmentedHandshake(t *testing.T) {
handshake := buildMinecraftHandshake(t, 767, "mc.example.com", 25565, minecraftNextStateStatus)
s := newMinecraftStream(nil)
u, done := s.Feed(false, false, false, 0, handshake[:2])
if done {
t.Fatal("stream done on partial handshake")
}
if got := u.M.Get("candidate"); got != true {
t.Fatalf("candidate = %v, want true", got)
}
u, done = s.Feed(false, false, false, 0, handshake[2:])
if !done {
t.Fatal("stream not done after remaining handshake")
}
if got := u.M.Get("yes"); got != true {
t.Fatalf("yes = %v, want true", got)
}
if got := u.M.Get("server_addr"); got != "mc.example.com" {
t.Fatalf("server_addr = %v, want mc.example.com", got)
}
}
func TestMinecraftParsing_InvalidAfterCandidate(t *testing.T) {
s := newMinecraftStream(nil)
u, done := s.Feed(false, false, false, 0, []byte{0x04})
if done {
t.Fatal("stream done on partial packet")
}
if got := u.M.Get("candidate"); got != true {
t.Fatalf("candidate = %v, want true", got)
}
u, done = s.Feed(false, false, false, 0, []byte{0x01, 0x02, 0x03, 0x04})
if !done {
t.Fatal("stream not done on invalid packet")
}
if got := u.M.Get("candidate"); got != false {
t.Fatalf("candidate = %v, want false", got)
}
if got := u.M.Get("yes"); got != false {
t.Fatalf("yes = %v, want false", got)
}
}
func TestMinecraftParsing_RejectsInvalidUTF8Address(t *testing.T) {
packet := []byte{0x00}
packet = append(packet, encodeMinecraftVarInt(767)...)
packet = append(packet, 0x01, 0xff)
packet = append(packet, 0x63, 0xdd)
packet = append(packet, encodeMinecraftVarInt(minecraftNextStateLogin)...)
data := append(encodeMinecraftVarInt(int32(len(packet))), packet...)
u, done := newMinecraftStream(nil).Feed(false, false, false, 0, data)
if !done {
t.Fatal("stream not done on invalid UTF-8 address")
}
if got := u.M.Get("yes"); got != false {
t.Fatalf("yes = %v, want false", got)
}
}
func buildMinecraftHandshake(t *testing.T, protocol int32, addr string, port uint16, nextState int32) []byte {
t.Helper()
packet := []byte{0x00}
packet = append(packet, encodeMinecraftVarInt(protocol)...)
packet = append(packet, encodeMinecraftVarInt(int32(len(addr)))...)
packet = append(packet, []byte(addr)...)
packet = append(packet, byte(port>>8), byte(port))
packet = append(packet, encodeMinecraftVarInt(nextState)...)
return append(encodeMinecraftVarInt(int32(len(packet))), packet...)
}
func encodeMinecraftVarInt(value int32) []byte {
var out []byte
for {
b := byte(value & 0x7f)
value = int32(uint32(value) >> 7)
if value != 0 {
b |= 0x80
}
out = append(out, b)
if value == 0 {
return out
}
}
}
-508
View File
@@ -1,508 +0,0 @@
package tcp
import (
"net"
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/analyzer/utils"
)
const (
SocksInvalid = iota
Socks4
Socks4A
Socks5
Socks4Version = 0x04
Socks5Version = 0x05
Socks4ReplyVN = 0x00
Socks4CmdTCPConnect = 0x01
Socks4CmdTCPBind = 0x02
Socks4ReqGranted = 0x5A
Socks4ReqRejectOrFailed = 0x5B
Socks4ReqRejectIdentd = 0x5C
Socks4ReqRejectUser = 0x5D
Socks5CmdTCPConnect = 0x01
Socks5CmdTCPBind = 0x02
Socks5CmdUDPAssociate = 0x03
Socks5AuthNotRequired = 0x00
Socks5AuthPassword = 0x02
Socks5AuthNoMatchingMethod = 0xFF
Socks5AuthSuccess = 0x00
Socks5AuthFailure = 0x01
Socks5AddrTypeIPv4 = 0x01
Socks5AddrTypeDomain = 0x03
Socks5AddrTypeIPv6 = 0x04
)
var _ analyzer.Analyzer = (*SocksAnalyzer)(nil)
type SocksAnalyzer struct{}
func (a *SocksAnalyzer) Name() string {
return "socks"
}
func (a *SocksAnalyzer) Limit() int {
// Socks4 length limit cannot be predicted
return 0
}
func (a *SocksAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) analyzer.TCPStream {
return newSocksStream(logger)
}
type socksStream struct {
logger analyzer.Logger
reqBuf *utils.ByteBuffer
reqMap analyzer.PropMap
reqUpdated bool
reqLSM *utils.LinearStateMachine
reqDone bool
respBuf *utils.ByteBuffer
respMap analyzer.PropMap
respUpdated bool
respLSM *utils.LinearStateMachine
respDone bool
version int
authReqMethod int
authUsername string
authPassword string
authRespMethod int
}
func newSocksStream(logger analyzer.Logger) *socksStream {
s := &socksStream{logger: logger, reqBuf: &utils.ByteBuffer{}, respBuf: &utils.ByteBuffer{}}
s.reqLSM = utils.NewLinearStateMachine(
s.parseSocksReqVersion,
)
s.respLSM = utils.NewLinearStateMachine(
s.parseSocksRespVersion,
)
return s
}
func (s *socksStream) 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 {
s.respBuf.Append(data)
s.respUpdated = false
cancelled, s.respDone = s.respLSM.Run()
if s.respUpdated {
update = &analyzer.PropUpdate{
Type: analyzer.PropUpdateMerge,
M: analyzer.PropMap{"resp": s.respMap},
}
s.respUpdated = false
}
} else {
s.reqBuf.Append(data)
s.reqUpdated = false
cancelled, s.reqDone = s.reqLSM.Run()
if s.reqUpdated {
update = &analyzer.PropUpdate{
Type: analyzer.PropUpdateMerge,
M: analyzer.PropMap{
"version": s.socksVersion(),
"req": s.reqMap,
},
}
s.reqUpdated = false
}
}
return update, cancelled || (s.reqDone && s.respDone)
}
func (s *socksStream) Close(limited bool) *analyzer.PropUpdate {
s.reqBuf.Reset()
s.respBuf.Reset()
s.reqMap = nil
s.respMap = nil
return nil
}
func (s *socksStream) parseSocksReqVersion() utils.LSMAction {
socksVer, ok := s.reqBuf.GetByte(true)
if !ok {
return utils.LSMActionPause
}
if socksVer != Socks4Version && socksVer != Socks5Version {
return utils.LSMActionCancel
}
s.reqMap = make(analyzer.PropMap)
s.reqUpdated = true
if socksVer == Socks4Version {
s.version = Socks4
s.reqLSM.AppendSteps(
s.parseSocks4ReqIpAndPort,
s.parseSocks4ReqUserId,
s.parseSocks4ReqHostname,
)
} else {
s.version = Socks5
s.reqLSM.AppendSteps(
s.parseSocks5ReqMethod,
s.parseSocks5ReqAuth,
s.parseSocks5ReqConnInfo,
)
}
return utils.LSMActionNext
}
func (s *socksStream) parseSocksRespVersion() utils.LSMAction {
socksVer, ok := s.respBuf.GetByte(true)
if !ok {
return utils.LSMActionPause
}
if (s.version == Socks4 || s.version == Socks4A) && socksVer != Socks4ReplyVN ||
s.version == Socks5 && socksVer != Socks5Version || s.version == SocksInvalid {
return utils.LSMActionCancel
}
if socksVer == Socks4ReplyVN {
s.respLSM.AppendSteps(
s.parseSocks4RespPacket,
)
} else {
s.respLSM.AppendSteps(
s.parseSocks5RespMethod,
s.parseSocks5RespAuth,
s.parseSocks5RespConnInfo,
)
}
return utils.LSMActionNext
}
func (s *socksStream) parseSocks5ReqMethod() utils.LSMAction {
nMethods, ok := s.reqBuf.GetByte(false)
if !ok {
return utils.LSMActionPause
}
methods, ok := s.reqBuf.Get(int(nMethods)+1, true)
if !ok {
return utils.LSMActionPause
}
// For convenience, we only take the first method we can process
s.authReqMethod = Socks5AuthNoMatchingMethod
for _, method := range methods[1:] {
switch method {
case Socks5AuthNotRequired:
s.authReqMethod = Socks5AuthNotRequired
return utils.LSMActionNext
case Socks5AuthPassword:
s.authReqMethod = Socks5AuthPassword
return utils.LSMActionNext
default:
// TODO: more auth method to support
}
}
return utils.LSMActionNext
}
func (s *socksStream) parseSocks5ReqAuth() utils.LSMAction {
switch s.authReqMethod {
case Socks5AuthNotRequired:
s.reqMap["auth"] = analyzer.PropMap{"method": s.authReqMethod}
case Socks5AuthPassword:
meta, ok := s.reqBuf.Get(2, false)
if !ok {
return utils.LSMActionPause
}
if meta[0] != 0x01 {
return utils.LSMActionCancel
}
usernameLen := int(meta[1])
meta, ok = s.reqBuf.Get(usernameLen+3, false)
if !ok {
return utils.LSMActionPause
}
passwordLen := int(meta[usernameLen+2])
meta, ok = s.reqBuf.Get(usernameLen+passwordLen+3, true)
if !ok {
return utils.LSMActionPause
}
s.authUsername = string(meta[2 : usernameLen+2])
s.authPassword = string(meta[usernameLen+3:])
s.reqMap["auth"] = analyzer.PropMap{
"method": s.authReqMethod,
"username": s.authUsername,
"password": s.authPassword,
}
default:
return utils.LSMActionCancel
}
s.reqUpdated = true
return utils.LSMActionNext
}
func (s *socksStream) parseSocks5ReqConnInfo() utils.LSMAction {
/* preInfo struct
+----+-----+-------+------+-------------+
|VER | CMD | RSV | ATYP | DST.ADDR(1) |
+----+-----+-------+------+-------------+
*/
preInfo, ok := s.reqBuf.Get(5, false)
if !ok {
return utils.LSMActionPause
}
// verify socks version
if preInfo[0] != Socks5Version {
return utils.LSMActionCancel
}
var pktLen int
switch int(preInfo[3]) {
case Socks5AddrTypeIPv4:
pktLen = 10
case Socks5AddrTypeDomain:
domainLen := int(preInfo[4])
pktLen = 7 + domainLen
case Socks5AddrTypeIPv6:
pktLen = 22
default:
return utils.LSMActionCancel
}
pkt, ok := s.reqBuf.Get(pktLen, true)
if !ok {
return utils.LSMActionPause
}
// parse cmd
cmd := int(pkt[1])
if cmd != Socks5CmdTCPConnect && cmd != Socks5CmdTCPBind && cmd != Socks5CmdUDPAssociate {
return utils.LSMActionCancel
}
s.reqMap["cmd"] = cmd
// parse addr type
addrType := int(pkt[3])
var addr string
switch addrType {
case Socks5AddrTypeIPv4:
addr = net.IPv4(pkt[4], pkt[5], pkt[6], pkt[7]).String()
case Socks5AddrTypeDomain:
addr = string(pkt[5 : 5+pkt[4]])
case Socks5AddrTypeIPv6:
addr = net.IP(pkt[4 : 4+net.IPv6len]).String()
default:
return utils.LSMActionCancel
}
s.reqMap["addr_type"] = addrType
s.reqMap["addr"] = addr
// parse port
port := int(pkt[pktLen-2])<<8 | int(pkt[pktLen-1])
s.reqMap["port"] = port
s.reqUpdated = true
return utils.LSMActionNext
}
func (s *socksStream) parseSocks5RespMethod() utils.LSMAction {
method, ok := s.respBuf.Get(1, true)
if !ok {
return utils.LSMActionPause
}
s.authRespMethod = int(method[0])
s.respMap = make(analyzer.PropMap)
return utils.LSMActionNext
}
func (s *socksStream) parseSocks5RespAuth() utils.LSMAction {
switch s.authRespMethod {
case Socks5AuthNotRequired:
s.respMap["auth"] = analyzer.PropMap{"method": s.authRespMethod}
case Socks5AuthPassword:
authResp, ok := s.respBuf.Get(2, true)
if !ok {
return utils.LSMActionPause
}
if authResp[0] != 0x01 {
return utils.LSMActionCancel
}
authStatus := int(authResp[1])
s.respMap["auth"] = analyzer.PropMap{
"method": s.authRespMethod,
"status": authStatus,
}
default:
return utils.LSMActionCancel
}
s.respUpdated = true
return utils.LSMActionNext
}
func (s *socksStream) parseSocks5RespConnInfo() utils.LSMAction {
/* preInfo struct
+----+-----+-------+------+-------------+
|VER | REP | RSV | ATYP | BND.ADDR(1) |
+----+-----+-------+------+-------------+
*/
preInfo, ok := s.respBuf.Get(5, false)
if !ok {
return utils.LSMActionPause
}
// verify socks version
if preInfo[0] != Socks5Version {
return utils.LSMActionCancel
}
var pktLen int
switch int(preInfo[3]) {
case Socks5AddrTypeIPv4:
pktLen = 10
case Socks5AddrTypeDomain:
domainLen := int(preInfo[4])
pktLen = 7 + domainLen
case Socks5AddrTypeIPv6:
pktLen = 22
default:
return utils.LSMActionCancel
}
pkt, ok := s.respBuf.Get(pktLen, true)
if !ok {
return utils.LSMActionPause
}
// parse rep
rep := int(pkt[1])
s.respMap["rep"] = rep
// parse addr type
addrType := int(pkt[3])
var addr string
switch addrType {
case Socks5AddrTypeIPv4:
addr = net.IPv4(pkt[4], pkt[5], pkt[6], pkt[7]).String()
case Socks5AddrTypeDomain:
addr = string(pkt[5 : 5+pkt[4]])
case Socks5AddrTypeIPv6:
addr = net.IP(pkt[4 : 4+net.IPv6len]).String()
default:
return utils.LSMActionCancel
}
s.respMap["addr_type"] = addrType
s.respMap["addr"] = addr
// parse port
port := int(pkt[pktLen-2])<<8 | int(pkt[pktLen-1])
s.respMap["port"] = port
s.respUpdated = true
return utils.LSMActionNext
}
func (s *socksStream) parseSocks4ReqIpAndPort() utils.LSMAction {
/* Following field will be parsed in this state:
+-----+----------+--------+
| CMD | DST.PORT | DST.IP |
+-----+----------+--------+
*/
pkt, ok := s.reqBuf.Get(7, true)
if !ok {
return utils.LSMActionPause
}
if pkt[0] != Socks4CmdTCPConnect && pkt[0] != Socks4CmdTCPBind {
return utils.LSMActionCancel
}
dstPort := uint16(pkt[1])<<8 | uint16(pkt[2])
dstIp := net.IPv4(pkt[3], pkt[4], pkt[5], pkt[6]).String()
// Socks4a extension
if pkt[3] == 0 && pkt[4] == 0 && pkt[5] == 0 {
s.version = Socks4A
}
s.reqMap["cmd"] = pkt[0]
s.reqMap["addr"] = dstIp
s.reqMap["addr_type"] = Socks5AddrTypeIPv4
s.reqMap["port"] = dstPort
s.reqUpdated = true
return utils.LSMActionNext
}
func (s *socksStream) parseSocks4ReqUserId() utils.LSMAction {
userIdSlice, ok := s.reqBuf.GetUntil([]byte("\x00"), true, true)
if !ok {
return utils.LSMActionPause
}
userId := string(userIdSlice[:len(userIdSlice)-1])
s.reqMap["auth"] = analyzer.PropMap{
"user_id": userId,
}
s.reqUpdated = true
return utils.LSMActionNext
}
func (s *socksStream) parseSocks4ReqHostname() utils.LSMAction {
// Only Socks4a support hostname
if s.version != Socks4A {
return utils.LSMActionNext
}
hostnameSlice, ok := s.reqBuf.GetUntil([]byte("\x00"), true, true)
if !ok {
return utils.LSMActionPause
}
hostname := string(hostnameSlice[:len(hostnameSlice)-1])
s.reqMap["addr"] = hostname
s.reqMap["addr_type"] = Socks5AddrTypeDomain
s.reqUpdated = true
return utils.LSMActionNext
}
func (s *socksStream) parseSocks4RespPacket() utils.LSMAction {
pkt, ok := s.respBuf.Get(7, true)
if !ok {
return utils.LSMActionPause
}
if pkt[0] != Socks4ReqGranted &&
pkt[0] != Socks4ReqRejectOrFailed &&
pkt[0] != Socks4ReqRejectIdentd &&
pkt[0] != Socks4ReqRejectUser {
return utils.LSMActionCancel
}
dstPort := uint16(pkt[1])<<8 | uint16(pkt[2])
dstIp := net.IPv4(pkt[3], pkt[4], pkt[5], pkt[6]).String()
s.respMap = analyzer.PropMap{
"rep": pkt[0],
"addr": dstIp,
"addr_type": Socks5AddrTypeIPv4,
"port": dstPort,
}
s.respUpdated = true
return utils.LSMActionNext
}
func (s *socksStream) socksVersion() int {
switch s.version {
case Socks4, Socks4A:
return Socks4Version
case Socks5:
return Socks5Version
default:
return SocksInvalid
}
}
-147
View File
@@ -1,147 +0,0 @@
package tcp
import (
"strings"
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/analyzer/utils"
)
var _ analyzer.TCPAnalyzer = (*SSHAnalyzer)(nil)
type SSHAnalyzer struct{}
func (a *SSHAnalyzer) Name() string {
return "ssh"
}
func (a *SSHAnalyzer) Limit() int {
return 1024
}
func (a *SSHAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) analyzer.TCPStream {
return newSSHStream(logger)
}
type sshStream struct {
logger analyzer.Logger
clientBuf *utils.ByteBuffer
clientMap analyzer.PropMap
clientUpdated bool
clientLSM *utils.LinearStateMachine
clientDone bool
serverBuf *utils.ByteBuffer
serverMap analyzer.PropMap
serverUpdated bool
serverLSM *utils.LinearStateMachine
serverDone bool
}
func newSSHStream(logger analyzer.Logger) *sshStream {
s := &sshStream{logger: logger, clientBuf: &utils.ByteBuffer{}, serverBuf: &utils.ByteBuffer{}}
s.clientLSM = utils.NewLinearStateMachine(
s.parseClientExchangeLine,
)
s.serverLSM = utils.NewLinearStateMachine(
s.parseServerExchangeLine,
)
return s
}
func (s *sshStream) Feed(rev, start, end bool, skip int, data []byte) (u *analyzer.PropUpdate, done bool) {
if skip != 0 {
return nil, true
}
if len(data) == 0 {
return nil, false
}
var update *analyzer.PropUpdate
var cancelled bool
if rev {
s.serverBuf.Append(data)
s.serverUpdated = false
cancelled, s.serverDone = s.serverLSM.Run()
if s.serverUpdated {
update = &analyzer.PropUpdate{
Type: analyzer.PropUpdateMerge,
M: analyzer.PropMap{"server": s.serverMap},
}
s.serverUpdated = false
}
} else {
s.clientBuf.Append(data)
s.clientUpdated = false
cancelled, s.clientDone = s.clientLSM.Run()
if s.clientUpdated {
update = &analyzer.PropUpdate{
Type: analyzer.PropUpdateMerge,
M: analyzer.PropMap{"client": s.clientMap},
}
s.clientUpdated = false
}
}
return update, cancelled || (s.clientDone && s.serverDone)
}
// parseExchangeLine parses the SSH Protocol Version Exchange string.
// See RFC 4253, section 4.2.
// "SSH-protoversion-softwareversion SP comments CR LF"
// The "comments" part (along with the SP) is optional.
func (s *sshStream) parseExchangeLine(buf *utils.ByteBuffer) (utils.LSMAction, analyzer.PropMap) {
// Find the end of the line
line, ok := buf.GetUntil([]byte("\r\n"), true, true)
if !ok {
// No end of line yet, but maybe we just need more data
return utils.LSMActionPause, nil
}
if !strings.HasPrefix(string(line), "SSH-") {
// Not SSH
return utils.LSMActionCancel, nil
}
fields := strings.Fields(string(line[:len(line)-2])) // Strip \r\n
if len(fields) < 1 || len(fields) > 2 {
// Invalid line
return utils.LSMActionCancel, nil
}
sshFields := strings.SplitN(fields[0], "-", 3)
if len(sshFields) != 3 {
// Invalid SSH version format
return utils.LSMActionCancel, nil
}
sMap := analyzer.PropMap{
"protocol": sshFields[1],
"software": sshFields[2],
}
if len(fields) == 2 {
sMap["comments"] = fields[1]
}
return utils.LSMActionNext, sMap
}
func (s *sshStream) parseClientExchangeLine() utils.LSMAction {
action, sMap := s.parseExchangeLine(s.clientBuf)
if action == utils.LSMActionNext {
s.clientMap = sMap
s.clientUpdated = true
}
return action
}
func (s *sshStream) parseServerExchangeLine() utils.LSMAction {
action, sMap := s.parseExchangeLine(s.serverBuf)
if action == utils.LSMActionNext {
s.serverMap = sMap
s.serverUpdated = true
}
return action
}
func (s *sshStream) Close(limited bool) *analyzer.PropUpdate {
s.clientBuf.Reset()
s.serverBuf.Reset()
s.clientMap = nil
s.serverMap = nil
return nil
}
-240
View File
@@ -1,240 +0,0 @@
package tcp
import (
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/analyzer/internal"
"github.com/apernet/OpenGFW/analyzer/utils"
)
var _ analyzer.TCPAnalyzer = (*TLSAnalyzer)(nil)
type TLSAnalyzer struct{}
func (a *TLSAnalyzer) Name() string {
return "tls"
}
func (a *TLSAnalyzer) Limit() int {
return 8192
}
func (a *TLSAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) analyzer.TCPStream {
return newTLSStream(logger)
}
const (
// tlsRecordHeaderSize is content type (1 byte) + legacy protocol version
// (2 bytes) + length (2 bytes).
tlsRecordHeaderSize = 5
// tlsMaxPlaintextLen is the largest body a plaintext record may carry.
tlsMaxPlaintextLen = 1 << 14
// tlsHandshakeHeaderSize is message type (1 byte) + length (3 bytes).
tlsHandshakeHeaderSize = 4
)
type tlsStream struct {
logger analyzer.Logger
req tlsHello
resp tlsHello
}
// tlsHello reads one hello message off one direction of the stream.
//
// A handshake message may be split across any number of records (RFC 8446
// 5.1), and splitting the ClientHello into tiny records is a common way to
// hide the SNI from middleboxes that expect it in the first record. So the
// record layer is peeled off first and the message is rebuilt from the record
// bodies before it is parsed.
type tlsHello struct {
rec utils.ByteBuffer // record layer bytes not consumed yet
msg utils.ByteBuffer // handshake layer bytes collected so far
lsm *utils.LinearStateMachine
msgType byte
x224Code byte // X.224 connection PDU that may precede the hello on RDP
minLen int // smallest valid message body
parse func(*utils.ByteBuffer) analyzer.PropMap // parses the message body
recLeft int // body bytes of the current record not consumed yet
msgLen int // message body length, -1 until the handshake header is read
records int // records that have carried the message so far
reported int // value of records last published
m analyzer.PropMap
updated bool
done bool
rdp analyzer.PropMap // RDP negotiation, nil if there was none
rdpUpdated bool
}
func newTLSStream(logger analyzer.Logger) *tlsStream {
s := &tlsStream{logger: logger}
// minimal ClientHello body: protocol version (2 bytes) + random (32 bytes) +
// + session ID (1 byte) + cipher suites (4 bytes) +
// + compression methods (2 bytes) + no extensions
s.req.init(internal.TypeClientHello, x224CodeConnectionRequest, 41, internal.ParseTLSClientHelloMsgData)
// minimal ServerHello body: server version (2 bytes) + random (32 bytes) +
// + session ID (>=1 byte) + cipher suite (2 bytes) +
// + compression method (1 byte) + no extensions
s.resp.init(internal.TypeServerHello, x224CodeConnectionConfirm, 38, internal.ParseTLSServerHelloMsgData)
return s
}
func (h *tlsHello) init(msgType, x224Code byte, minLen int, parse func(*utils.ByteBuffer) analyzer.PropMap) {
h.msgType, h.x224Code, h.minLen, h.parse = msgType, x224Code, minLen, parse
h.msgLen = -1
h.lsm = utils.NewLinearStateMachine(h.readPrelude, h.readMessage, h.parseMessage)
}
func (s *tlsStream) Feed(rev, start, end bool, skip int, data []byte) (u *analyzer.PropUpdate, done bool) {
if skip != 0 {
return nil, true
}
if len(data) == 0 {
return nil, false
}
h, key := &s.req, "req"
if rev {
h, key = &s.resp, "resp"
}
if h.done {
return nil, false
}
h.rec.Append(data)
h.updated = false
var cancelled bool
cancelled, h.done = h.lsm.Run()
// A fragmented hello is published as soon as it shows up, not only once
// the message is complete: a peer can make it too large to ever complete
// within Limit(), and a rule should not need the SNI to act on it.
if m := h.update(key, h.updated || (h.records > 1 && h.reported <= 1)); m != nil {
u = &analyzer.PropUpdate{Type: analyzer.PropUpdateMerge, M: m}
}
return u, cancelled || (s.req.done && s.resp.done)
}
// update returns the properties to publish for this direction, or nil if
// there is nothing to publish. The parsed message goes under key, the record
// count under key + "_records". The record count is kept out of the message
// map so that a non-nil message map still means a fully parsed hello. An RDP
// negotiation goes under "rdp_" + key as soon as it is parsed, whatever
// publish says.
func (h *tlsHello) update(key string, publish bool) analyzer.PropMap {
m := analyzer.PropMap{}
if h.rdpUpdated {
m["rdp_"+key] = h.rdp
h.rdpUpdated = false
}
if publish && h.records > 0 {
m[key+"_records"] = h.records
if h.updated {
m[key] = h.m
h.updated = false
}
h.reported = h.records
}
if len(m) == 0 {
return nil
}
return m
}
// readMessage consumes records until a whole hello message is collected in
// h.msg.
func (h *tlsHello) readMessage() utils.LSMAction {
for {
if h.msgLen < 0 && h.msg.Len() >= tlsHandshakeHeaderSize {
header := h.msg.Buf
if header[0] != h.msgType {
return utils.LSMActionCancel
}
h.msgLen = int(header[1])<<16 | int(header[2])<<8 | int(header[3])
if h.msgLen < h.minLen {
return utils.LSMActionCancel
}
}
if h.msgLen >= 0 && h.msg.Len() >= tlsHandshakeHeaderSize+h.msgLen {
return utils.LSMActionNext
}
if h.recLeft == 0 {
header, ok := h.rec.Get(tlsRecordHeaderSize, true)
if !ok {
return utils.LSMActionPause
}
if header[0] != internal.RecordTypeHandshake {
// Nothing else may come between the records of a handshake
// message.
return utils.LSMActionCancel
}
h.recLeft = int(header[3])<<8 | int(header[4])
if h.recLeft > tlsMaxPlaintextLen {
return utils.LSMActionCancel
}
// Empty records are forbidden for handshake messages, but still
// counted rather than rejected: a peer that sends them anyway is
// fragmenting, and rejecting them would stop the analysis.
h.records++
continue
}
n := min(h.recLeft, h.rec.Len())
if n == 0 {
return utils.LSMActionPause
}
body, _ := h.rec.Get(n, true)
h.recLeft -= n
if h.msg.Len() == 0 {
// Usually the whole message sits in this one chunk, so reference
// it instead of copying. The capacity is capped so that a later
// Append reallocates rather than writing into h.rec.
h.msg.Buf = body[:n:n]
} else {
h.msg.Append(body)
}
}
}
// parseMessage converts the collected hello message into h.m.
//
// Anything after the message, such as the rest of the server's flight, is
// ignored.
func (h *tlsHello) parseMessage() utils.LSMAction {
body := &utils.ByteBuffer{Buf: h.msg.Buf[tlsHandshakeHeaderSize : tlsHandshakeHeaderSize+h.msgLen]}
m := h.parse(body)
h.rec.Reset()
h.msg.Reset()
if m == nil {
return utils.LSMActionCancel
}
h.m = m
h.updated = true
return utils.LSMActionNext
}
func (s *tlsStream) Close(limited bool) *analyzer.PropUpdate {
// Report how far a fragmented hello got if it never completed, e.g.
// because it was stretched past Limit().
m := s.req.update("req", s.req.records > 1 && s.req.records > s.req.reported)
if rm := s.resp.update("resp", s.resp.records > 1 && s.resp.records > s.resp.reported); rm != nil {
if m == nil {
m = rm
} else {
for k, v := range rm {
m[k] = v
}
}
}
s.req.close()
s.resp.close()
if m == nil {
return nil
}
return &analyzer.PropUpdate{Type: analyzer.PropUpdateMerge, M: m}
}
func (h *tlsHello) close() {
h.rec.Reset()
h.msg.Reset()
h.m = nil
}
-123
View File
@@ -1,123 +0,0 @@
package tcp
import (
"bytes"
"encoding/binary"
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/analyzer/internal"
"github.com/apernet/OpenGFW/analyzer/utils"
)
// RDP does not start TLS right away: the client first sends an X.224
// Connection Request and the server answers with a Connection Confirm, each in
// a TPKT packet, to agree on a security protocol ([MS-RDPBCGR] 1.3.1.1). Only
// then does the TLS handshake begin, on the same connection.
//
// The tls analyzer used to give up on the first byte of such a stream, which
// left RDP invisible to TLS rules, and with it anything that puts a fake RDP
// negotiation in front of its TLS. So the negotiation is parsed as an optional
// prelude of the hello, and published as rdp_req / rdp_resp.
const (
tpktVersion = 0x03
tpktHeaderSize = 4 // version (1 byte) + reserved (1 byte) + length (2 bytes)
// x224FixedSize is length indicator (1 byte) + code (1 byte) +
// DST-REF (2 bytes) + SRC-REF (2 bytes) + class option (1 byte).
x224FixedSize = 7
x224CodeConnectionRequest = 0xe0
x224CodeConnectionConfirm = 0xd0
rdpNegReq = 0x01
rdpNegRsp = 0x02
rdpNegFailure = 0x03
rdpNegSize = 8
)
var rdpCookiePrefix = []byte("Cookie: ")
// readPrelude consumes the X.224 connection PDU that precedes the hello on an
// RDP connection, if there is one.
func (h *tlsHello) readPrelude() utils.LSMAction {
b, ok := h.rec.GetByte(false)
if !ok {
return utils.LSMActionPause
}
if b == internal.RecordTypeHandshake {
return utils.LSMActionNext
}
if b != tpktVersion {
return utils.LSMActionCancel
}
header, ok := h.rec.Get(tpktHeaderSize, false)
if !ok {
return utils.LSMActionPause
}
n := int(binary.BigEndian.Uint16(header[2:4]))
if header[1] != 0 || n < tpktHeaderSize+x224FixedSize {
return utils.LSMActionCancel
}
pdu, ok := h.rec.Get(n, true)
if !ok {
return utils.LSMActionPause
}
m := parseX224Connection(pdu[tpktHeaderSize:], h.x224Code)
if m == nil {
return utils.LSMActionCancel
}
h.rdp = m
h.rdpUpdated = true
return utils.LSMActionNext
}
// parseX224Connection parses an X.224 Connection Request or Confirm TPDU
// carrying the RDP negotiation, or returns nil if tpdu is not one.
func parseX224Connection(tpdu []byte, code byte) analyzer.PropMap {
// The length indicator does not count itself. The low nibble of the code
// is the credit, and the high nibble of the class option the class, which
// is always 0 for RDP.
if int(tpdu[0]) != len(tpdu)-1 || tpdu[1]&0xf0 != code || tpdu[6]&0xf0 != 0 {
return nil
}
rest := tpdu[x224FixedSize:]
m := analyzer.PropMap{}
if code == x224CodeConnectionRequest {
// Both the cookie ("Cookie: mstshash=<user>") and the load balancer
// routing token ("Cookie: msts=<token>") are terminated by CRLF.
if bytes.HasPrefix(rest, rdpCookiePrefix) {
i := bytes.Index(rest, []byte("\r\n"))
if i < 0 {
return nil
}
m["cookie"] = string(rest[len(rdpCookiePrefix):i])
rest = rest[i+2:]
}
// A client that sends no RDP_NEG_REQ can only do standard RDP
// security (PROTOCOL_RDP).
m["protocols"] = 0
} else {
m["protocol"] = 0
}
if len(rest) == 0 {
return m
}
// Anything after the negotiation structure, such as the correlation info
// a client may append, is ignored.
if len(rest) < rdpNegSize || binary.LittleEndian.Uint16(rest[2:4]) != rdpNegSize {
return nil
}
v := int(binary.LittleEndian.Uint32(rest[4:8]))
switch {
case code == x224CodeConnectionRequest && rest[0] == rdpNegReq:
m["protocols"] = v
case code == x224CodeConnectionConfirm && rest[0] == rdpNegRsp:
m["protocol"] = v
case code == x224CodeConnectionConfirm && rest[0] == rdpNegFailure:
delete(m, "protocol")
m["failure"] = v
default:
return nil
}
return m
}
-131
View File
@@ -1,131 +0,0 @@
package tcp
import (
"reflect"
"testing"
"github.com/apernet/OpenGFW/analyzer"
)
// rdpConnectionRequest is what mstsc sends first: a cookie with the user name
// and an RDP_NEG_REQ asking for PROTOCOL_SSL | PROTOCOL_HYBRID |
// PROTOCOL_HYBRID_EX.
var rdpConnectionRequest = []byte{
0x03, 0x00, 0x00, 0x2c, // TPKT, length 44
0x27, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, // X.224 CR
'C', 'o', 'o', 'k', 'i', 'e', ':', ' ', 'm', 's', 't', 's', 'h', 'a', 's', 'h', '=', 'e', 'l', 't', 'o', 'n', 's', '\r', '\n',
0x01, 0x00, 0x08, 0x00, 0x0b, 0x00, 0x00, 0x00, // RDP_NEG_REQ
}
func rdpConnectionConfirm(negType byte, v byte) []byte {
return []byte{
0x03, 0x00, 0x00, 0x13, // TPKT, length 19
0x0e, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00, // X.224 CC
negType, 0x00, 0x08, 0x00, v, 0x00, 0x00, 0x00,
}
}
var rdpTLSClientHelloProps = analyzer.PropMap{"cookie": "mstshash=eltons", "protocols": 11}
func TestTlsRDPClientHello(t *testing.T) {
data := append(append([]byte{}, rdpConnectionRequest...), tlsExampleClientHello...)
for name, segs := range map[string][][]byte{
"one segment": {data},
"byte by byte": splitEvery(data, 1),
"pdu, hello": {rdpConnectionRequest, tlsExampleClientHello},
} {
props, _ := feedTLS(newTLSStream(nil), false, segs...)
if got := props.Get("rdp_req"); !reflect.DeepEqual(got, rdpTLSClientHelloProps) {
t.Errorf("%s: rdp_req = %v, want %v", name, got, rdpTLSClientHelloProps)
}
if got := props.Get("req"); !reflect.DeepEqual(got, tlsExampleClientHelloProps) {
t.Errorf("%s: req = %v, want %v", name, got, tlsExampleClientHelloProps)
}
}
}
// The negotiation is published before any TLS shows up, so that rules can
// already see the stream is RDP.
func TestTlsRDPNegotiationReportedEarly(t *testing.T) {
s := newTLSStream(nil)
props, done := feedTLS(s, false, rdpConnectionRequest)
if done {
t.Fatal("stream done after connection request")
}
if got := props.Get("rdp_req"); !reflect.DeepEqual(got, rdpTLSClientHelloProps) {
t.Errorf("rdp_req = %v, want %v", got, rdpTLSClientHelloProps)
}
props, done = feedTLS(s, true, rdpConnectionConfirm(0x02, 0x02))
if done {
t.Fatal("stream done after connection confirm")
}
if got, want := props.Get("rdp_resp"), (analyzer.PropMap{"protocol": 2}); !reflect.DeepEqual(got, want) {
t.Errorf("rdp_resp = %v, want %v", got, want)
}
}
func TestTlsRDPConnectionConfirm(t *testing.T) {
for name, tc := range map[string]struct {
pdu []byte
want analyzer.PropMap
}{
"ssl": {rdpConnectionConfirm(0x02, 0x01), analyzer.PropMap{"protocol": 1}},
"hybrid_ex": {rdpConnectionConfirm(0x02, 0x08), analyzer.PropMap{"protocol": 8}},
"failure": {rdpConnectionConfirm(0x03, 0x05), analyzer.PropMap{"failure": 5}},
"no neg rsp": {[]byte{0x03, 0x00, 0x00, 0x0b, 0x06, 0xd0, 0x00, 0x00, 0x12, 0x34, 0x00}, analyzer.PropMap{"protocol": 0}},
} {
props, _ := feedTLS(newTLSStream(nil), true, tc.pdu)
if got := props.Get("rdp_resp"); !reflect.DeepEqual(got, tc.want) {
t.Errorf("%s: rdp_resp = %v, want %v", name, got, tc.want)
}
}
}
// Standard RDP security has no TLS: the client goes on with MCS, which is not
// a TLS record, so the analyzer stops with only the negotiation published.
func TestTlsRDPWithoutTLS(t *testing.T) {
mcs := []byte{0x03, 0x00, 0x01, 0xa0, 0x02, 0xf0, 0x80, 0x7f, 0x65}
props, done := feedTLS(newTLSStream(nil), false, rdpConnectionRequest, mcs)
if !done {
t.Error("stream not done")
}
if props.Get("rdp_req") == nil || props.Get("req") != nil {
t.Errorf("props = %v, want rdp_req only", props)
}
}
// truncatedNegReq is rdpConnectionRequest with the last two bytes of the
// RDP_NEG_REQ cut off and the lengths adjusted to match.
func truncatedNegReq() []byte {
pdu := append([]byte{}, rdpConnectionRequest[:len(rdpConnectionRequest)-2]...)
pdu[3] -= 2
pdu[4] -= 2
return pdu
}
func TestTlsRDPRejectsMalformedPrelude(t *testing.T) {
mutate := func(i int, b byte) []byte {
pdu := append([]byte{}, rdpConnectionRequest...)
pdu[i] = b
return pdu
}
for name, pdu := range map[string][]byte{
"reserved set": mutate(1, 0x01),
"wrong code": mutate(5, 0xd0),
"length mismatch": mutate(4, 0x26),
"class 2": mutate(10, 0x20),
"cookie w/o crlf": mutate(len(rdpConnectionRequest)-10, 'x'),
"bad neg type": mutate(len(rdpConnectionRequest)-8, 0x02),
"tpkt too short": {0x03, 0x00, 0x00, 0x05, 0x00},
"not tpkt nor tls": {0x05, 0x01, 0x00},
"truncated neg req": truncatedNegReq(),
} {
props, done := feedTLS(newTLSStream(nil), false, pdu)
if !done {
t.Errorf("%s: stream not done", name)
}
if len(props) != 0 {
t.Errorf("%s: props = %v, want none", name, props)
}
}
}
-188
View File
@@ -1,188 +0,0 @@
package tcp
import (
"reflect"
"testing"
"github.com/apernet/OpenGFW/analyzer"
)
// tlsExampleClientHello is taken from <https://tls12.xargs.org/#client-hello/annotated>.
var tlsExampleClientHello = []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,
}
var tlsExampleClientHelloProps = 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),
}
func TestTlsStreamParsing_ClientHello(t *testing.T) {
s := newTLSStream(nil)
u, _ := s.Feed(false, false, false, 0, tlsExampleClientHello)
if got := u.M.Get("req"); !reflect.DeepEqual(got, tlsExampleClientHelloProps) {
t.Errorf("parsed = %v, want %v", got, tlsExampleClientHelloProps)
}
if got := u.M.Get("req_records"); got != 1 {
t.Errorf("req_records = %v, want 1", got)
}
}
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)
}
}
// tlsFragment re-encodes the handshake message in a single-record hello as
// records carrying at most size bytes of it each.
func tlsFragment(hello []byte, size int) []byte {
var out []byte
for msg := hello[5:]; len(msg) > 0; {
n := min(size, len(msg))
out = append(out, hello[0], hello[1], hello[2], byte(n>>8), byte(n))
out = append(out, msg[:n]...)
msg = msg[n:]
}
return out
}
// feedTLS feeds each segment in turn and returns the properties as the engine
// would have merged them, along with whether the stream said it was done.
func feedTLS(s *tlsStream, rev bool, segs ...[]byte) (analyzer.PropMap, bool) {
props := analyzer.PropMap{}
done := false
for _, seg := range segs {
var u *analyzer.PropUpdate
u, done = s.Feed(rev, false, false, 0, seg)
if u != nil {
for k, v := range u.M {
props[k] = v
}
}
}
return props, done
}
// Splitting the ClientHello across records must not hide it: the parse result
// has to be the same wherever the record boundaries fall.
func TestTlsClientHelloRecordFragmentation(t *testing.T) {
msgLen := len(tlsExampleClientHello) - 5
for _, size := range []int{1, 2, 3, 4, 5, 9, 50, msgLen - 1} {
data := tlsFragment(tlsExampleClientHello, size)
wantRecords := (msgLen + size - 1) / size
for name, segs := range map[string][][]byte{
"one segment": {data},
"byte by byte": splitEvery(data, 1),
"7 byte segs": splitEvery(data, 7),
} {
props, _ := feedTLS(newTLSStream(nil), false, segs...)
if got := props.Get("req"); !reflect.DeepEqual(got, tlsExampleClientHelloProps) {
t.Errorf("size %d, %s: parsed = %v, want %v", size, name, got, tlsExampleClientHelloProps)
}
if got := props.Get("req_records"); got != wantRecords {
t.Errorf("size %d, %s: req_records = %v, want %d", size, name, got, wantRecords)
}
}
}
}
// Fragmentation is published before the hello is complete, so that a rule can
// act on it even if the hello never completes.
func TestTlsClientHelloFragmentationReportedEarly(t *testing.T) {
data := tlsFragment(tlsExampleClientHello, 1)
s := newTLSStream(nil)
props, done := feedTLS(s, false, data[:12])
if done {
t.Fatal("stream done after two records")
}
if props.Get("req") != nil {
t.Errorf("req published early: %v", props.Get("req"))
}
if got := props.Get("req_records"); got != 2 {
t.Errorf("req_records = %v, want 2", got)
}
// Further records are not published one by one...
if u, _ := s.Feed(false, false, false, 0, data[12:60]); u != nil {
t.Errorf("unexpected update %v", u.M)
}
// ...but the count reached is, if the stream gets cut off.
u := s.Close(true)
if u == nil {
t.Fatal("no update on close")
}
if got := u.M.Get("req_records"); got != 10 {
t.Errorf("req_records on close = %v, want 10", got)
}
}
func TestTlsClientHelloRejectsInterleavedRecords(t *testing.T) {
data := tlsFragment(tlsExampleClientHello, 50)
// Turn the second record into an alert.
data[5+50] = 0x15
props, done := feedTLS(newTLSStream(nil), false, data)
if !done {
t.Error("stream not done")
}
if props.Get("req") != nil {
t.Errorf("req = %v, want nil", props.Get("req"))
}
}
func TestTlsStreamIgnoresUnfragmentedPartialHelloOnClose(t *testing.T) {
s := newTLSStream(nil)
if props, _ := feedTLS(s, false, tlsExampleClientHello[:100]); len(props) != 0 {
t.Errorf("props = %v, want none", props)
}
if u := s.Close(true); u != nil {
t.Errorf("close update = %v, want nil", u.M)
}
}
func splitEvery(data []byte, n int) [][]byte {
var segs [][]byte
for len(data) > 0 {
k := min(n, len(data))
segs = append(segs, data[:k])
data = data[k:]
}
return segs
}
-517
View File
@@ -1,517 +0,0 @@
package tcp
import (
"bytes"
"github.com/apernet/OpenGFW/analyzer"
)
var _ analyzer.TCPAnalyzer = (*TrojanAnalyzer)(nil)
// CCS stands for "Change Cipher Spec"
var ccsPattern = []byte{20, 3, 3, 0, 1, 1}
// 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 {
return "trojan"
}
func (a *TrojanAnalyzer) Limit() int {
return 512000
}
func (a *TrojanAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) analyzer.TCPStream {
return newTrojanStream(logger)
}
type trojanStream struct {
logger analyzer.Logger
first bool
count bool
rev bool
seq [4]int
seqIndex int
}
func newTrojanStream(logger analyzer.Logger) *trojanStream {
return &trojanStream{logger: logger}
}
func (s *trojanStream) Feed(rev, start, end bool, skip int, data []byte) (u *analyzer.PropUpdate, done bool) {
if skip != 0 {
return nil, true
}
if len(data) == 0 {
return nil, false
}
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 !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 {
// Different direction, bump the index
s.seqIndex += 1
if s.seqIndex == 4 {
return &analyzer.PropUpdate{
Type: analyzer.PropUpdateReplace,
M: analyzer.PropMap{
"seq": s.seq,
"yes": isTrojanSeq(s.seq),
},
}, true
}
s.seq[s.seqIndex] += len(data)
s.rev = rev
}
}
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
}
}
}
}
}
}
}
}
}
-265
View File
@@ -1,265 +0,0 @@
package udp
import (
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/analyzer/utils"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
)
const (
dnsUDPInvalidCountThreshold = 4
)
// DNSAnalyzer is for both DNS over UDP and TCP.
var (
_ analyzer.UDPAnalyzer = (*DNSAnalyzer)(nil)
_ analyzer.TCPAnalyzer = (*DNSAnalyzer)(nil)
)
type DNSAnalyzer struct{}
func (a *DNSAnalyzer) Name() string {
return "dns"
}
func (a *DNSAnalyzer) Limit() int {
// DNS is a stateless protocol, with unlimited amount
// of back-and-forth exchanges. Don't limit it here.
return 0
}
func (a *DNSAnalyzer) NewUDP(info analyzer.UDPInfo, logger analyzer.Logger) analyzer.UDPStream {
return &dnsUDPStream{logger: logger}
}
func (a *DNSAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) analyzer.TCPStream {
s := &dnsTCPStream{logger: logger, reqBuf: &utils.ByteBuffer{}, respBuf: &utils.ByteBuffer{}}
s.reqLSM = utils.NewLinearStateMachine(
s.getReqMessageLength,
s.getReqMessage,
)
s.respLSM = utils.NewLinearStateMachine(
s.getRespMessageLength,
s.getRespMessage,
)
return s
}
type dnsUDPStream struct {
logger analyzer.Logger
invalidCount int
}
func (s *dnsUDPStream) Feed(rev bool, data []byte) (u *analyzer.PropUpdate, done bool) {
m := parseDNSMessage(data)
// To allow non-DNS UDP traffic to get offloaded,
// we consider a UDP stream invalid and "done" if
// it has more than a certain number of consecutive
// packets that are not valid DNS messages.
if m == nil {
s.invalidCount++
return nil, s.invalidCount >= dnsUDPInvalidCountThreshold
}
s.invalidCount = 0 // Reset invalid count on valid DNS message
return &analyzer.PropUpdate{
Type: analyzer.PropUpdateReplace,
M: m,
}, false
}
func (s *dnsUDPStream) Close(limited bool) *analyzer.PropUpdate {
return nil
}
type dnsTCPStream struct {
logger analyzer.Logger
reqBuf *utils.ByteBuffer
reqMap analyzer.PropMap
reqUpdated bool
reqLSM *utils.LinearStateMachine
reqDone bool
respBuf *utils.ByteBuffer
respMap analyzer.PropMap
respUpdated bool
respLSM *utils.LinearStateMachine
respDone bool
reqMsgLen int
respMsgLen int
}
func (s *dnsTCPStream) Feed(rev, start, end bool, skip int, data []byte) (u *analyzer.PropUpdate, done bool) {
if skip != 0 {
return nil, true
}
if len(data) == 0 {
return nil, false
}
var update *analyzer.PropUpdate
var cancelled bool
if rev {
s.respBuf.Append(data)
s.respUpdated = false
cancelled, s.respDone = s.respLSM.Run()
if s.respUpdated {
update = &analyzer.PropUpdate{
Type: analyzer.PropUpdateReplace,
M: s.respMap,
}
s.respUpdated = false
}
} else {
s.reqBuf.Append(data)
s.reqUpdated = false
cancelled, s.reqDone = s.reqLSM.Run()
if s.reqUpdated {
update = &analyzer.PropUpdate{
Type: analyzer.PropUpdateReplace,
M: s.reqMap,
}
s.reqUpdated = false
}
}
return update, cancelled || (s.reqDone && s.respDone)
}
func (s *dnsTCPStream) Close(limited bool) *analyzer.PropUpdate {
s.reqBuf.Reset()
s.respBuf.Reset()
s.reqMap = nil
s.respMap = nil
return nil
}
func (s *dnsTCPStream) getReqMessageLength() utils.LSMAction {
bs, ok := s.reqBuf.Get(2, true)
if !ok {
return utils.LSMActionPause
}
s.reqMsgLen = int(bs[0])<<8 | int(bs[1])
return utils.LSMActionNext
}
func (s *dnsTCPStream) getRespMessageLength() utils.LSMAction {
bs, ok := s.respBuf.Get(2, true)
if !ok {
return utils.LSMActionPause
}
s.respMsgLen = int(bs[0])<<8 | int(bs[1])
return utils.LSMActionNext
}
func (s *dnsTCPStream) getReqMessage() utils.LSMAction {
bs, ok := s.reqBuf.Get(s.reqMsgLen, true)
if !ok {
return utils.LSMActionPause
}
m := parseDNSMessage(bs)
if m == nil {
// Invalid DNS message
return utils.LSMActionCancel
}
s.reqMap = m
s.reqUpdated = true
return utils.LSMActionReset
}
func (s *dnsTCPStream) getRespMessage() utils.LSMAction {
bs, ok := s.respBuf.Get(s.respMsgLen, true)
if !ok {
return utils.LSMActionPause
}
m := parseDNSMessage(bs)
if m == nil {
// Invalid DNS message
return utils.LSMActionCancel
}
s.respMap = m
s.respUpdated = true
return utils.LSMActionReset
}
func parseDNSMessage(msg []byte) analyzer.PropMap {
dns := &layers.DNS{}
err := dns.DecodeFromBytes(msg, gopacket.NilDecodeFeedback)
if err != nil {
// Not a DNS packet
return nil
}
m := analyzer.PropMap{
"id": dns.ID,
"qr": dns.QR,
"opcode": dns.OpCode,
"aa": dns.AA,
"tc": dns.TC,
"rd": dns.RD,
"ra": dns.RA,
"z": dns.Z,
"rcode": dns.ResponseCode,
}
if len(dns.Questions) > 0 {
mQuestions := make([]analyzer.PropMap, len(dns.Questions))
for i, q := range dns.Questions {
mQuestions[i] = analyzer.PropMap{
"name": string(q.Name),
"type": q.Type,
"class": q.Class,
}
}
m["questions"] = mQuestions
}
if len(dns.Answers) > 0 {
mAnswers := make([]analyzer.PropMap, len(dns.Answers))
for i, rr := range dns.Answers {
mAnswers[i] = dnsRRToPropMap(rr)
}
m["answers"] = mAnswers
}
if len(dns.Authorities) > 0 {
mAuthorities := make([]analyzer.PropMap, len(dns.Authorities))
for i, rr := range dns.Authorities {
mAuthorities[i] = dnsRRToPropMap(rr)
}
m["authorities"] = mAuthorities
}
if len(dns.Additionals) > 0 {
mAdditionals := make([]analyzer.PropMap, len(dns.Additionals))
for i, rr := range dns.Additionals {
mAdditionals[i] = dnsRRToPropMap(rr)
}
m["additionals"] = mAdditionals
}
return m
}
func dnsRRToPropMap(rr layers.DNSResourceRecord) analyzer.PropMap {
m := analyzer.PropMap{
"name": string(rr.Name),
"type": rr.Type,
"class": rr.Class,
"ttl": rr.TTL,
}
switch rr.Type {
// These are not everything, but is
// all we decided to support for now.
case layers.DNSTypeA:
m["a"] = rr.IP.String()
case layers.DNSTypeAAAA:
m["aaaa"] = rr.IP.String()
case layers.DNSTypeNS:
m["ns"] = string(rr.NS)
case layers.DNSTypeCNAME:
m["cname"] = string(rr.CNAME)
case layers.DNSTypePTR:
m["ptr"] = string(rr.PTR)
case layers.DNSTypeTXT:
m["txt"] = utils.ByteSlicesToStrings(rr.TXTs)
case layers.DNSTypeMX:
m["mx"] = string(rr.MX.Name)
}
return m
}
-766
View File
@@ -1,766 +0,0 @@
package udp
import (
"encoding/binary"
"maps"
"unicode"
"unicode/utf8"
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/analyzer/utils"
)
// EasyTierAnalyzer 识别 EasyTier(https://github.com/EasyTier/EasyTier)
type EasyTierAnalyzer struct{}
var (
_ analyzer.UDPAnalyzer = (*EasyTierAnalyzer)(nil)
_ analyzer.TCPAnalyzer = (*EasyTierAnalyzer)(nil)
_ analyzer.UDPStream = (*easyTierUDPStream)(nil)
_ analyzer.TCPStream = (*easyTierTCPStream)(nil)
)
const (
// easyTierUDPTunnelHeaderSize UDPTunnelHeader:conn_id u32、msg_type u8、padding u8、len u16,全部小端
easyTierUDPTunnelHeaderSize = 8
// easyTierTCPTunnelHeaderSize TCPTunnelHeader:一个小端 u32 帧体长度
easyTierTCPTunnelHeaderSize = 4
// easyTierPeerManagerHeaderSize PeerManagerHeader:from u32、to u32、type u8、flags u8、fwd u8、reserved u8、len u32
easyTierPeerManagerHeaderSize = 16
// easyTierAEADTailSize AEAD 加密追加的 16 字节 tag 加 12 字节 nonce,头里声明的长度不包含它
easyTierAEADTailSize = 28
// easyTierMaxFrameBody 帧体长度上限。上游自己的限制更低(TCP 隧道 2000,字节流隧道 4096,
// UDP 会话 8192),这里取最大的那个,精确的约束交给长度不变量
easyTierMaxFrameBody = 8192
// easyTierMaxForwardCounter 路由在 >7 时丢弃后再自增,所以线上最大能见到 8
easyTierMaxForwardCounter = 8
// easyTierHandshakeMagic HandshakeRequest.magic,见上游 peers/conn/peer_conn.rs
easyTierHandshakeMagic uint64 = 0xd1e1a5e1
// easyTierSecretDigestLen NetworkSecretDigest = [u8; 32]
easyTierSecretDigestLen = 32
// easyTierNoiseEphemeralLen Noise XX 首条消息以 32 字节临时公钥开头
easyTierNoiseEphemeralLen = 32
// easyTierFramesForYes 没有握手可指的情况下,置 yes 需要攒够的自洽帧数
easyTierFramesForYes = 2
// easyTierUDPInvalidCountThreshold UDP 流上连续无法解析的数据报容忍数,与 wireguard 分析器一致
easyTierUDPInvalidCountThreshold = 4
// easyTierPacketLimit 超过这个包数仍未定性就结束该流,与 openvpn 分析器一致
easyTierPacketLimit = 256
)
// easyTierHolePunchBodyLens 上游打洞包的两种体长:连通性路径 16 字节,UDP 会话层 32 字节。
// 体本身是随机数据,长度是唯一能看的东西。
var easyTierHolePunchBodyLens = [...]int{16, 32}
// UDP 隧道消息类型(UdpPacketType)
const (
easyTierMsgSyn = 1
easyTierMsgSack = 2
easyTierMsgData = 3
easyTierMsgFin = 4
easyTierMsgHolePunch = 5
easyTierMsgV4HolePunch = 6
easyTierMsgV6HolePunch = 7
)
// 需要特殊处理的 PeerManagerHeader packet_type(PacketType)
const (
easyTierPktHandshake = 2
easyTierPktNoiseMsg1 = 13
easyTierPktNoiseMsg3 = 15
)
// PeerManagerHeaderFlags
const (
easyTierFlagEncrypted = 0b0000_0001
easyTierFlagLatencyFirst = 0b0000_0010
easyTierFlagExitNode = 0b0000_0100
easyTierFlagNoProxy = 0b0000_1000
easyTierFlagCompressed = 0b0001_0000
// easyTierFlagLiveness 两个存活探测标志,是仅有的让 reserved 字节有意义的标志
easyTierFlagLiveness = 0b1010_0000
)
func (a *EasyTierAnalyzer) Name() string {
return "easytier"
}
func (a *EasyTierAnalyzer) Limit() int {
// 定长帧协议,按字节截断会让帧读取失步;用 easyTierPacketLimit 约束开销
return 0
}
func (a *EasyTierAnalyzer) NewUDP(info analyzer.UDPInfo, logger analyzer.Logger) analyzer.UDPStream {
return newEasyTierUDPStream(logger)
}
func (a *EasyTierAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) analyzer.TCPStream {
return newEasyTierTCPStream(logger)
}
// easyTierEvidence 一帧能证明多少
type easyTierEvidence int
const (
// easyTierEvidenceWeak 格式正确但单独不足为据:体是随机数据的短定长数据报。字段照发,不推进 yes
easyTierEvidenceWeak easyTierEvidence = iota
// easyTierEvidenceFrame 带 PeerManagerHeader 且通过全部校验的帧
easyTierEvidenceFrame
// easyTierEvidenceConclusive 匹配上的握手,别的东西不会碰巧产生它
easyTierEvidenceConclusive
)
// easyTierCommon 对外发布的属性,加上 yes 背后的计数。
//
// 描述最近一帧的字段每次都会覆盖;从握手里读出来的字段是粘性的,
// 因为网络名只发一次,而按 mesh 名写的规则必须在握手滚过去之后还能用。
type easyTierCommon struct {
props analyzer.PropMap
frames int
conclusive bool
packetCount int
rxPacketCount int
txPacketCount int
}
func newEasyTierCommon(transport string) easyTierCommon {
return easyTierCommon{
props: analyzer.PropMap{
"yes": false,
"transport": transport,
"tx_pkt_cnt": 0,
"rx_pkt_cnt": 0,
},
}
}
func (c *easyTierCommon) countPacket(rev bool) {
c.packetCount++
if rev {
c.rxPacketCount++
c.props["rx_pkt_cnt"] = c.rxPacketCount
} else {
c.txPacketCount++
c.props["tx_pkt_cnt"] = c.txPacketCount
}
}
func (c *easyTierCommon) record(evidence easyTierEvidence) {
switch evidence {
case easyTierEvidenceConclusive:
c.conclusive = true
case easyTierEvidenceFrame:
c.frames++
}
c.props["yes"] = c.conclusive || c.frames >= easyTierFramesForYes
}
func (c *easyTierCommon) set(key string, value interface{}) {
c.props[key] = value
}
// setSticky 只写一次、之后不再覆盖,用于从握手里读出的字段
func (c *easyTierCommon) setSticky(key string, value interface{}) {
if _, ok := c.props[key]; !ok {
c.props[key] = value
}
}
// update 发布一份属性快照。引擎会持有这份 map,所以必须复制,不能把内部的交出去
func (c *easyTierCommon) update() *analyzer.PropUpdate {
return &analyzer.PropUpdate{
Type: analyzer.PropUpdateReplace,
M: maps.Clone(c.props),
}
}
func (c *easyTierCommon) exhausted() bool {
return c.packetCount > easyTierPacketLimit
}
// applyHeader 记录 PeerManagerHeader 的字段,并返回这一帧能证明多少
func (c *easyTierCommon) applyHeader(h easyTierPeerManagerHeader, body []byte) easyTierEvidence {
c.set("packet_type", h.packetType)
c.set("packet_type_name", easyTierPacketTypeName(h.packetType))
c.set("from_peer_id", h.fromPeerID)
c.set("to_peer_id", h.toPeerID)
c.set("forward_counter", h.forwardCounter)
c.set("payload_len", h.length)
c.set("encrypted", h.flags&easyTierFlagEncrypted != 0)
c.set("compressed", h.flags&easyTierFlagCompressed != 0)
c.set("latency_first", h.flags&easyTierFlagLatencyFirst != 0)
c.set("exit_node", h.flags&easyTierFlagExitNode != 0)
c.set("no_proxy", h.flags&easyTierFlagNoProxy != 0)
payload := body[easyTierPeerManagerHeaderSize:]
switch h.packetType {
case easyTierPktHandshake:
return c.applyPlainHandshake(payload)
case easyTierPktNoiseMsg1:
c.setSticky("secure", true)
c.applyNoiseMsg1(payload)
// 只有临时公钥和一段 protobuf,随机流也能凑出来,不算确证
return easyTierEvidenceFrame
case easyTierPktNoiseMsg3:
c.setSticky("secure", true)
return easyTierEvidenceFrame
default:
return easyTierEvidenceFrame
}
}
// applyPlainHandshake 明文 HandshakeRequest,magic 对上就定性
func (c *easyTierCommon) applyPlainHandshake(payload []byte) easyTierEvidence {
hs, ok := easyTierParseHandshakeRequest(payload)
if !ok {
return easyTierEvidenceFrame
}
c.setSticky("magic_ok", true)
c.setSticky("secure", false)
if hs.hasVersion {
c.setSticky("version", hs.version)
}
if hs.networkName != "" {
c.setSticky("network_name", hs.networkName)
}
return easyTierEvidenceConclusive
}
// applyNoiseMsg1 Noise XX 的首条消息此时还没有密钥,protobuf 载荷是明文。
// 机会性地读:解析不干净就什么都不说,不猜。
func (c *easyTierCommon) applyNoiseMsg1(payload []byte) {
if len(payload) <= easyTierNoiseEphemeralLen {
return
}
msg, ok := easyTierParseNoiseMsg1(payload[easyTierNoiseEphemeralLen:])
if !ok {
return
}
if msg.hasVersion {
c.setSticky("version", msg.version)
}
if msg.networkName != "" {
c.setSticky("network_name", msg.networkName)
}
}
type easyTierPeerManagerHeader struct {
fromPeerID uint32
toPeerID uint32
packetType uint8
flags uint8
forwardCounter uint8
length uint32
}
// easyTierReadPeerManagerHeader 读取并校验帧体开头的 PeerManagerHeader。
//
// 声明的载荷长度要和实际收到的帧体对照。AEAD 加密会追加 28 字节尾部而不改声明长度,
// 压缩会缩短载荷也不改声明长度,所以只有两个标志都没置位时关系才是精确相等。
func easyTierReadPeerManagerHeader(body []byte) (easyTierPeerManagerHeader, bool) {
var h easyTierPeerManagerHeader
if len(body) < easyTierPeerManagerHeaderSize {
return h, false
}
packetType := body[8]
if !easyTierIsKnownPacketType(packetType) {
return h, false
}
flags := body[9]
forwardCounter := body[10]
if forwardCounter > easyTierMaxForwardCounter {
return h, false
}
// reserved 只在存活探测时装 token,其余时候恒为 0
if body[11] != 0 && flags&easyTierFlagLiveness == 0 {
return h, false
}
length := binary.LittleEndian.Uint32(body[12:16])
if length > easyTierMaxFrameBody {
return h, false
}
wire := uint32(len(body) - easyTierPeerManagerHeaderSize)
var ok bool
switch {
case flags&easyTierFlagCompressed != 0:
// 压缩器只在能省下自己那 1 字节尾时才压
ok = wire >= 1 && wire <= length+easyTierAEADTailSize
case flags&easyTierFlagEncrypted != 0:
// XOR 不改长度;AEAD 追加尾部
ok = wire == length || wire == length+easyTierAEADTailSize
default:
ok = wire == length
}
if !ok {
return h, false
}
return easyTierPeerManagerHeader{
fromPeerID: binary.LittleEndian.Uint32(body[0:4]),
toPeerID: binary.LittleEndian.Uint32(body[4:8]),
packetType: packetType,
flags: flags,
forwardCounter: forwardCounter,
length: length,
}, true
}
func easyTierIsKnownPacketType(packetType uint8) bool {
return packetType >= 1 && packetType <= 21
}
func easyTierPacketTypeName(packetType uint8) string {
switch packetType {
case 1:
return "data"
case 2:
return "handshake"
case 3:
return "route_packet"
case 4:
return "ping"
case 5:
return "pong"
case 6:
return "ta_rpc"
case 7:
return "route"
case 8:
return "rpc_req"
case 9:
return "rpc_resp"
case 10:
return "foreign_network_packet"
case 11:
return "kcp_src"
case 12:
return "kcp_dst"
case 13:
return "noise_handshake_msg1"
case 14:
return "noise_handshake_msg2"
case 15:
return "noise_handshake_msg3"
case 16:
return "quic_src"
case 17:
return "quic_dst"
case 18:
return "data_with_kcp_src_modified"
case 19:
return "data_with_quic_src_modified"
case 20:
return "relay_handshake"
case 21:
return "relay_handshake_ack"
default:
return "unknown"
}
}
func easyTierMsgTypeName(msgType uint8) string {
switch msgType {
case easyTierMsgSyn:
return "syn"
case easyTierMsgSack:
return "sack"
case easyTierMsgData:
return "data"
case easyTierMsgFin:
return "fin"
case easyTierMsgHolePunch:
return "hole_punch"
case easyTierMsgV4HolePunch:
return "v4_hole_punch"
case easyTierMsgV6HolePunch:
return "v6_hole_punch"
default:
return "unknown"
}
}
// easyTierPendingSyn 一个等着 Sack 的 Syn
type easyTierPendingSyn struct {
connID uint32
magic uint64
rev bool
}
type easyTierUDPStream struct {
logger analyzer.Logger
common easyTierCommon
invalidCount int
pendingSyn *easyTierPendingSyn
}
func newEasyTierUDPStream(logger analyzer.Logger) *easyTierUDPStream {
return &easyTierUDPStream{
logger: logger,
common: newEasyTierCommon("udp"),
}
}
func (s *easyTierUDPStream) Feed(rev bool, data []byte) (u *analyzer.PropUpdate, done bool) {
// EasyTier 把 STUN 复用在同一个 socket 上,并且在自己的帧解析之前先分流 STUN;
// 照做:既不算证据,也不算错误
if easyTierIsSTUNDatagram(data) {
s.common.packetCount++
return nil, s.common.exhausted()
}
evidence, ok := s.parseDatagram(rev, data)
if !ok {
s.invalidCount++
return nil, s.invalidCount >= easyTierUDPInvalidCountThreshold
}
s.invalidCount = 0
s.common.countPacket(rev)
s.common.record(evidence)
return s.common.update(), s.common.exhausted()
}
func (s *easyTierUDPStream) Close(limited bool) *analyzer.PropUpdate {
return nil
}
func (s *easyTierUDPStream) parseDatagram(rev bool, data []byte) (easyTierEvidence, bool) {
if len(data) < easyTierUDPTunnelHeaderSize {
return 0, false
}
connID := binary.LittleEndian.Uint32(data[0:4])
msgType := data[4]
// padding 只为对齐长度字段而存在
if data[5] != 0 {
return 0, false
}
declared := int(binary.LittleEndian.Uint16(data[6:8]))
body := data[easyTierUDPTunnelHeaderSize:]
// 上游要求这两者相等,否则整个数据报直接丢弃
if declared != len(body) {
return 0, false
}
var evidence easyTierEvidence
switch msgType {
case easyTierMsgSyn:
if len(body) != 8 {
return 0, false
}
s.pendingSyn = &easyTierPendingSyn{connID: connID, magic: binary.LittleEndian.Uint64(body), rev: rev}
evidence = easyTierEvidenceWeak
case easyTierMsgSack:
// 反向回来、同一 conn_id、同一随机 64 位 magic 的 Sack,别的东西做不到
if len(body) != 8 {
return 0, false
}
magic := binary.LittleEndian.Uint64(body)
syn := s.pendingSyn
if syn != nil && syn.rev != rev && syn.connID == connID && syn.magic == magic {
evidence = easyTierEvidenceConclusive
} else {
evidence = easyTierEvidenceWeak
}
case easyTierMsgData:
h, ok := easyTierReadPeerManagerHeader(body)
if !ok {
return 0, false
}
evidence = s.common.applyHeader(h, body)
case easyTierMsgFin:
if len(body) != 0 {
return 0, false
}
evidence = easyTierEvidenceWeak
case easyTierMsgHolePunch:
// 打洞探测是定长随机体,往可能根本没跑 EasyTier 的地址上喷,
// 不足以下结论,但 msg_type_name 照样说明它是什么
if !easyTierIsHolePunchBodyLen(len(body)) {
return 0, false
}
evidence = easyTierEvidenceWeak
case easyTierMsgV4HolePunch:
// 两种定向变体在 conn_id 里回显目标端口。上游只从回环地址接受它们,主机间线上少见
if len(body) != 6 || connID != uint32(binary.LittleEndian.Uint16(body[4:6])) {
return 0, false
}
evidence = easyTierEvidenceWeak
case easyTierMsgV6HolePunch:
if len(body) != 38 || connID != uint32(binary.LittleEndian.Uint16(body[16:18])) {
return 0, false
}
evidence = easyTierEvidenceWeak
default:
return 0, false
}
s.common.set("conn_id", connID)
s.common.set("msg_type", msgType)
s.common.set("msg_type_name", easyTierMsgTypeName(msgType))
return evidence, true
}
func easyTierIsHolePunchBodyLen(n int) bool {
for _, l := range easyTierHolePunchBodyLens {
if n == l {
return true
}
}
return false
}
// easyTierIsSTUNDatagram 和 EasyTier 自己先做的判断一样:
// 它的长度字段位置上是 STUN magic cookie,且前两位为零
func easyTierIsSTUNDatagram(data []byte) bool {
return len(data) >= easyTierUDPTunnelHeaderSize &&
data[4] == 0x21 && data[5] == 0x12 && data[6] == 0xa4 && data[7] == 0x42 &&
data[0]&0xc0 == 0
}
type easyTierTCPStream struct {
logger analyzer.Logger
common easyTierCommon
reqBuf utils.ByteBuffer
respBuf utils.ByteBuffer
}
func newEasyTierTCPStream(logger analyzer.Logger) *easyTierTCPStream {
return &easyTierTCPStream{
logger: logger,
common: newEasyTierCommon("tcp"),
}
}
// easyTierFrameStatus 从一个方向的缓冲里读一帧的结果
type easyTierFrameStatus int
const (
// easyTierFrameRead 头已校验通过,帧体可以发布
easyTierFrameRead easyTierFrameStatus = iota
// easyTierFrameNeedMore 声明的帧体还没到齐
easyTierFrameNeedMore
// easyTierFrameNo 不可能是 EasyTier 的帧
easyTierFrameNo
)
func (s *easyTierTCPStream) Feed(rev, start, end bool, skip int, data []byte) (u *analyzer.PropUpdate, done bool) {
// 重组有空洞时帧边界无从推断
if skip != 0 {
return nil, true
}
if len(data) == 0 {
return nil, false
}
buf := &s.reqBuf
if rev {
buf = &s.respBuf
}
buf.Append(data)
updated := false
for {
h, body, status := easyTierReadFrame(buf)
switch status {
case easyTierFrameRead:
evidence := s.common.applyHeader(h, body)
s.common.countPacket(rev)
s.common.record(evidence)
updated = true
if s.common.exhausted() {
return s.common.update(), true
}
case easyTierFrameNeedMore:
if updated {
return s.common.update(), false
}
return nil, false
case easyTierFrameNo:
if updated {
return s.common.update(), true
}
return nil, true
}
}
}
func (s *easyTierTCPStream) Close(limited bool) *analyzer.PropUpdate {
s.reqBuf.Reset()
s.respBuf.Reset()
return nil
}
// easyTierReadFrame 帧结构是 [u32 LE 帧体长度][帧体],帧体总是以 PeerManagerHeader 开头,
// 上游会把更短的帧体按 "body too short" 拒绝。
func easyTierReadFrame(buf *utils.ByteBuffer) (easyTierPeerManagerHeader, []byte, easyTierFrameStatus) {
var h easyTierPeerManagerHeader
bodyLen, ok := buf.GetUint32(true, false)
if !ok {
return h, nil, easyTierFrameNeedMore
}
if bodyLen < easyTierPeerManagerHeaderSize || bodyLen > easyTierMaxFrameBody {
return h, nil, easyTierFrameNo
}
total := easyTierTCPTunnelHeaderSize + int(bodyLen)
frame, ok := buf.Get(total, false)
if !ok {
return h, nil, easyTierFrameNeedMore
}
body := frame[easyTierTCPTunnelHeaderSize:]
h, ok = easyTierReadPeerManagerHeader(body)
if !ok {
return h, nil, easyTierFrameNo
}
// 只在帧确认合法后才消费;被拒绝的流反正要结束,留着字节让缓冲状态更好推理
buf.Skip(total)
return h, body, easyTierFrameRead
}
// easyTierHandshakeInfo 从握手里读出来的东西。networkName 为空表示没有可用的名字
type easyTierHandshakeInfo struct {
version uint64
hasVersion bool
networkName string
}
// easyTierParseHandshakeRequest 解析 HandshakeRequest { magic = 1, my_peer_id = 2,
// version = 3, features = 4, network_name = 5, network_secret_digest = 6 }。
//
// 只有 magic 存在且正确才返回 true,调用方可以把 true 当作证据而不是提示。
func easyTierParseHandshakeRequest(payload []byte) (easyTierHandshakeInfo, bool) {
var info easyTierHandshakeInfo
var magic uint64
hasMagic := false
digestLen := -1
ok := easyTierForEachProtoField(payload, func(f easyTierProtoField) {
switch {
case f.isVarint && f.number == 1:
magic, hasMagic = f.varint, true
case f.isVarint && f.number == 3:
info.version, info.hasVersion = f.varint, true
case f.isBytes && f.number == 5:
info.networkName, _ = easyTierPrintableUTF8(f.bytes)
case f.isBytes && f.number == 6:
digestLen = len(f.bytes)
}
})
if !ok || !hasMagic || magic != easyTierHandshakeMagic {
return easyTierHandshakeInfo{}, false
}
// 上游会拒绝 digest 宽度不对的握手
if digestLen >= 0 && digestLen != easyTierSecretDigestLen {
return easyTierHandshakeInfo{}, false
}
return info, true
}
// easyTierParseNoiseMsg1 解析 PeerConnNoiseMsg1Pb { version = 1, a_network_name = 2, ... }。
//
// 这里没有 magic 可查,所以消息必须完整解析并声明一个合理的版本,否则返回 false。
func easyTierParseNoiseMsg1(payload []byte) (easyTierHandshakeInfo, bool) {
var info easyTierHandshakeInfo
ok := easyTierForEachProtoField(payload, func(f easyTierProtoField) {
switch {
case f.isVarint && f.number == 1:
info.version, info.hasVersion = f.varint, true
case f.isBytes && f.number == 2:
info.networkName, _ = easyTierPrintableUTF8(f.bytes)
}
})
// 协议版本从有这个字段起就一直是 1,很大的值说明这些字节不是这条消息
if !ok || !info.hasVersion || info.version > 16 {
return easyTierHandshakeInfo{}, false
}
return info, true
}
type easyTierProtoField struct {
number uint32
isVarint bool
varint uint64
isBytes bool
bytes []byte
}
// easyTierForEachProtoField 遍历 protobuf 字段,跳过调用方不认识的字段,
// 遇到编码站不住脚(截断、字段号为 0、这两条消息从不使用的 wire type)时返回 false。
func easyTierForEachProtoField(data []byte, fn func(easyTierProtoField)) bool {
pos := 0
for pos < len(data) {
tag, ok := easyTierReadVarint(data, &pos)
if !ok {
return false
}
number := uint32(tag >> 3)
if number == 0 {
return false
}
switch tag & 0x7 {
case 0:
value, ok := easyTierReadVarint(data, &pos)
if !ok {
return false
}
fn(easyTierProtoField{number: number, isVarint: true, varint: value})
case 2:
length, ok := easyTierReadVarint(data, &pos)
if !ok || length > uint64(len(data)-pos) {
return false
}
end := pos + int(length)
fn(easyTierProtoField{number: number, isBytes: true, bytes: data[pos:end]})
pos = end
case 5:
if len(data)-pos < 4 {
return false
}
pos += 4
case 1:
if len(data)-pos < 8 {
return false
}
pos += 8
default:
// group 类型,两条消息都不用
return false
}
}
return true
}
func easyTierReadVarint(data []byte, pos *int) (uint64, bool) {
var value uint64
for i := 0; i < 10; i++ {
if *pos >= len(data) {
return 0, false
}
b := data[*pos]
*pos++
value |= uint64(b&0x7f) << (7 * i)
if b&0x80 == 0 {
return value, true
}
}
return 0, false
}
// easyTierPrintableUTF8 网络名是运营者自己起的文本。拒绝控制字符,
// 免得一段碰巧的字节最后被引用进日志或审计记录里。
func easyTierPrintableUTF8(b []byte) (string, bool) {
if len(b) == 0 || !utf8.Valid(b) {
return "", false
}
s := string(b)
for _, r := range s {
if unicode.IsControl(r) {
return "", false
}
}
return s, true
}
-562
View File
@@ -1,562 +0,0 @@
package udp
import (
"bytes"
"encoding/binary"
"fmt"
"testing"
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/analyzer/tcp"
)
func etVarint(v uint64) []byte {
var out []byte
for {
b := byte(v & 0x7f)
v >>= 7
if v != 0 {
b |= 0x80
}
out = append(out, b)
if v == 0 {
return out
}
}
}
func etProtoVarint(field uint32, v uint64) []byte {
return append(etVarint(uint64(field)<<3), etVarint(v)...)
}
func etProtoBytes(field uint32, v []byte) []byte {
out := etVarint(uint64(field)<<3 | 2)
out = append(out, etVarint(uint64(len(v)))...)
return append(out, v...)
}
// etHandshakeRequest HandshakeRequest,按 send_handshake 的构造方式
func etHandshakeRequest(peerID uint32, networkName string) []byte {
var out []byte
out = append(out, etProtoVarint(1, easyTierHandshakeMagic)...)
out = append(out, etProtoVarint(2, uint64(peerID))...)
out = append(out, etProtoVarint(3, 1)...)
out = append(out, etProtoBytes(4, []byte("liveness-echo-v1"))...)
out = append(out, etProtoBytes(5, []byte(networkName))...)
out = append(out, etProtoBytes(6, make([]byte, easyTierSecretDigestLen))...)
return out
}
// etNoiseMsg1 Noise XX 先写的临时公钥,后面跟 PeerConnNoiseMsg1Pb
func etNoiseMsg1(networkName string) []byte {
out := bytes.Repeat([]byte{0xab}, easyTierNoiseEphemeralLen)
out = append(out, etProtoVarint(1, 1)...)
return append(out, etProtoBytes(2, []byte(networkName))...)
}
// etBody 帧体:16 字节 PeerManagerHeader 加载荷
func etBody(packetType uint8, from, to uint32, payload []byte) []byte {
return etBodyRaw(packetType, from, to, 0, 1, 0, uint32(len(payload)), payload)
}
func etBodyRaw(packetType uint8, from, to uint32, flags, forwardCounter, reserved uint8, declaredLen uint32, payload []byte) []byte {
out := make([]byte, 0, easyTierPeerManagerHeaderSize+len(payload))
out = binary.LittleEndian.AppendUint32(out, from)
out = binary.LittleEndian.AppendUint32(out, to)
out = append(out, packetType, flags, forwardCounter, reserved)
out = binary.LittleEndian.AppendUint32(out, declaredLen)
return append(out, payload...)
}
// etDatagram 8 字节 UDPTunnelHeader 加帧体
func etDatagram(connID uint32, msgType uint8, body []byte) []byte {
out := make([]byte, 0, easyTierUDPTunnelHeaderSize+len(body))
out = binary.LittleEndian.AppendUint32(out, connID)
out = append(out, msgType, 0)
out = binary.LittleEndian.AppendUint16(out, uint16(len(body)))
return append(out, body...)
}
func etSyn(connID uint32, magic uint64) []byte {
return etDatagram(connID, easyTierMsgSyn, binary.LittleEndian.AppendUint64(nil, magic))
}
func etSack(connID uint32, magic uint64) []byte {
return etDatagram(connID, easyTierMsgSack, binary.LittleEndian.AppendUint64(nil, magic))
}
// etSegment 4 字节 TCPTunnelHeader 加帧体
func etSegment(body []byte) []byte {
out := binary.LittleEndian.AppendUint32(nil, uint32(len(body)))
return append(out, body...)
}
func etUDP() analyzer.UDPStream {
return (&EasyTierAnalyzer{}).NewUDP(analyzer.UDPInfo{}, nil)
}
func etTCP() analyzer.TCPStream {
return (&EasyTierAnalyzer{}).NewTCP(analyzer.TCPInfo{}, nil)
}
func etFeedUDP(s analyzer.UDPStream, rev bool, data []byte) (analyzer.PropMap, bool) {
u, done := s.Feed(rev, data)
if u == nil {
return analyzer.PropMap{}, done
}
return u.M, done
}
func etFeedTCP(s analyzer.TCPStream, rev bool, data []byte) (analyzer.PropMap, bool) {
u, done := s.Feed(rev, false, false, 0, data)
if u == nil {
return analyzer.PropMap{}, done
}
return u.M, done
}
func etText(m analyzer.PropMap, key string) string {
v, ok := m[key]
if !ok {
return "<missing>"
}
return fmt.Sprint(v)
}
func etFlag(m analyzer.PropMap, key string) bool {
v, _ := m[key].(bool)
return v
}
func etExpect(t *testing.T, m analyzer.PropMap, key, want string) {
t.Helper()
if got := etText(m, key); got != want {
t.Fatalf("%s = %q, want %q (props: %v)", key, got, want, m)
}
}
func TestEasyTier_HandshakeMagicEncodesToDocumentedBytes(t *testing.T) {
want := []byte{0x08, 0xe1, 0xcb, 0x86, 0x8f, 0x0d}
if got := etProtoVarint(1, easyTierHandshakeMagic); !bytes.Equal(got, want) {
t.Fatalf("magic encodes to % x, want % x", got, want)
}
}
func TestEasyTier_UDPHandshakeNamesTheMeshOnFirstDatagram(t *testing.T) {
payload := etHandshakeRequest(0x1a2b3c4d, "office-mesh")
m, done := etFeedUDP(etUDP(), false, etDatagram(7, easyTierMsgData, etBody(easyTierPktHandshake, 0x1a2b3c4d, 0, payload)))
if done {
t.Fatal("stream done after a single handshake")
}
if !etFlag(m, "yes") {
t.Fatal("the magic alone should settle it")
}
if !etFlag(m, "magic_ok") {
t.Fatal("magic_ok not set")
}
etExpect(t, m, "network_name", "office-mesh")
etExpect(t, m, "version", "1")
etExpect(t, m, "transport", "udp")
etExpect(t, m, "msg_type_name", "data")
etExpect(t, m, "packet_type_name", "handshake")
etExpect(t, m, "from_peer_id", "439041101")
etExpect(t, m, "to_peer_id", "0")
etExpect(t, m, "secure", "false")
}
func TestEasyTier_TCPHandshakeNamesTheMeshOnFirstFrame(t *testing.T) {
payload := etHandshakeRequest(9, "office-mesh")
m, done := etFeedTCP(etTCP(), false, etSegment(etBody(easyTierPktHandshake, 9, 0, payload)))
if done {
t.Fatal("stream done after a single handshake")
}
if !etFlag(m, "yes") {
t.Fatal("yes not set")
}
etExpect(t, m, "network_name", "office-mesh")
etExpect(t, m, "transport", "tcp")
etExpect(t, m, "tx_pkt_cnt", "1")
}
func TestEasyTier_HandshakeWithWrongMagicIsNotConclusive(t *testing.T) {
payload := append(etProtoVarint(1, 0xd1e1a5e0), etProtoBytes(5, []byte("office-mesh"))...)
m, _ := etFeedUDP(etUDP(), false, etDatagram(7, easyTierMsgData, etBody(easyTierPktHandshake, 1, 0, payload)))
// 帧结构仍然自洽,字段照发;但一帧不够,而且对网络不做任何声明
if etFlag(m, "yes") {
t.Fatal("yes set on a wrong magic")
}
etExpect(t, m, "network_name", "<missing>")
etExpect(t, m, "magic_ok", "<missing>")
}
func TestEasyTier_HandshakeWithWrongDigestWidthIsRejected(t *testing.T) {
// 上游这种情况会拒连,报匹配等于报一个根本连不起来的 mesh 名
payload := etProtoVarint(1, easyTierHandshakeMagic)
payload = append(payload, etProtoBytes(5, []byte("office-mesh"))...)
payload = append(payload, etProtoBytes(6, make([]byte, 16))...)
m, _ := etFeedUDP(etUDP(), false, etDatagram(7, easyTierMsgData, etBody(easyTierPktHandshake, 1, 0, payload)))
if etFlag(m, "yes") {
t.Fatal("yes set on a wrong digest width")
}
etExpect(t, m, "network_name", "<missing>")
}
func TestEasyTier_NoiseFirstMessageStillGivesUpTheNetworkName(t *testing.T) {
// Noise XX 写 msg1 时还没有密钥,临时公钥后面的载荷是明文
s := etUDP()
m, _ := etFeedUDP(s, false, etDatagram(7, easyTierMsgData, etBody(easyTierPktNoiseMsg1, 5, 0, etNoiseMsg1("secure-mesh"))))
if !etFlag(m, "secure") {
t.Fatal("secure not set")
}
etExpect(t, m, "network_name", "secure-mesh")
if etFlag(m, "yes") {
t.Fatal("one frame with no magic to check should not settle it")
}
// 反方向再来一帧就到阈值了
m, _ = etFeedUDP(s, true, etDatagram(7, easyTierMsgData, etBody(14, 6, 5, bytes.Repeat([]byte{0xcd}, 48))))
if !etFlag(m, "yes") {
t.Fatal("two consistent frames should settle it")
}
etExpect(t, m, "packet_type_name", "noise_handshake_msg2")
}
func TestEasyTier_OpaqueNoiseMessageSaysNothingAboutTheNetwork(t *testing.T) {
m, _ := etFeedUDP(etUDP(), false, etDatagram(7, easyTierMsgData, etBody(easyTierPktNoiseMsg1, 5, 0, bytes.Repeat([]byte{0x5a}, 96))))
if !etFlag(m, "secure") {
t.Fatal("secure not set")
}
etExpect(t, m, "network_name", "<missing>")
}
func TestEasyTier_SynAnsweredByItsSackSettlesTheFlow(t *testing.T) {
s := etUDP()
m, _ := etFeedUDP(s, false, etSyn(0x12345678, 0x0102030405060708))
if etFlag(m, "yes") {
t.Fatal("a lone syn is 16 bytes of nearly nothing")
}
etExpect(t, m, "msg_type_name", "syn")
m, _ = etFeedUDP(s, true, etSack(0x12345678, 0x0102030405060708))
if !etFlag(m, "yes") {
t.Fatal("a matching sack should settle it")
}
etExpect(t, m, "msg_type_name", "sack")
etExpect(t, m, "conn_id", "305419896")
etExpect(t, m, "tx_pkt_cnt", "1")
etExpect(t, m, "rx_pkt_cnt", "1")
}
func TestEasyTier_SackEchoingTheWrongMagicSettlesNothing(t *testing.T) {
s := etUDP()
etFeedUDP(s, false, etSyn(0x12345678, 1))
m, _ := etFeedUDP(s, true, etSack(0x12345678, 2))
if etFlag(m, "yes") {
t.Fatal("yes set on a mismatched magic")
}
}
func TestEasyTier_SackDownTheSameDirectionSettlesNothing(t *testing.T) {
s := etUDP()
etFeedUDP(s, false, etSyn(9, 42))
m, _ := etFeedUDP(s, false, etSack(9, 42))
if etFlag(m, "yes") {
t.Fatal("yes set on a same-direction sack")
}
}
func TestEasyTier_HolePunchProbesAreReportedButDoNotSettleTheFlow(t *testing.T) {
// 往可能根本没跑 EasyTier 的地址上喷的,规则得点名才拦
s := etUDP()
var m analyzer.PropMap
for i := 0; i < 3; i++ {
m, _ = etFeedUDP(s, false, etDatagram(1, easyTierMsgHolePunch, bytes.Repeat([]byte{0x7f}, 16)))
}
if etFlag(m, "yes") {
t.Fatal("hole punches settled the flow")
}
etExpect(t, m, "msg_type_name", "hole_punch")
}
func TestEasyTier_BothHolePunchBodyLengthsAreRecognised(t *testing.T) {
// 连通性路径 16 字节,UDP 会话层 32 字节;只有长度能把它们和噪声分开,两个都得列
for _, n := range []int{16, 32} {
m, _ := etFeedUDP(etUDP(), false, etDatagram(1, easyTierMsgHolePunch, bytes.Repeat([]byte{0x7f}, n)))
if got := etText(m, "msg_type_name"); got != "hole_punch" {
t.Fatalf("body length %d: msg_type_name = %q", n, got)
}
}
m, _ := etFeedUDP(etUDP(), false, etDatagram(1, easyTierMsgHolePunch, bytes.Repeat([]byte{0x7f}, 24)))
etExpect(t, m, "msg_type_name", "<missing>")
}
func TestEasyTier_DirectedHolePunchMustEchoTheDestinationPortInConnID(t *testing.T) {
punch := []byte{203, 0, 113, 9}
punch = binary.LittleEndian.AppendUint16(punch, 11010)
m, _ := etFeedUDP(etUDP(), false, etDatagram(11010, easyTierMsgV4HolePunch, punch))
etExpect(t, m, "msg_type_name", "v4_hole_punch")
m, done := etFeedUDP(etUDP(), false, etDatagram(11011, easyTierMsgV4HolePunch, punch))
etExpect(t, m, "msg_type_name", "<missing>")
if done {
t.Fatal("one bad datagram is not enough to give up")
}
}
func TestEasyTier_UDPLengthFieldMustMatchTheDatagram(t *testing.T) {
wrong := etSyn(1, 2)
wrong[6] = 9 // 声明 9,体是 8
m, _ := etFeedUDP(etUDP(), false, wrong)
etExpect(t, m, "msg_type_name", "<missing>")
}
func TestEasyTier_UDPPaddingByteMustBeZero(t *testing.T) {
wrong := etSyn(1, 2)
wrong[5] = 1
m, _ := etFeedUDP(etUDP(), false, wrong)
etExpect(t, m, "msg_type_name", "<missing>")
}
func TestEasyTier_FourUnparseableDatagramsEndAUDPFlow(t *testing.T) {
s := etUDP()
for i := 0; i < 3; i++ {
if _, done := etFeedUDP(s, false, []byte("not easytier at all")); done {
t.Fatalf("gave up after %d datagrams", i+1)
}
}
if _, done := etFeedUDP(s, false, []byte("not easytier at all")); !done {
t.Fatal("did not give up after four unparseable datagrams")
}
}
func TestEasyTier_TwoLengthFieldsOfATCPFrameMustAgree(t *testing.T) {
// 16 字节头后面声明载荷 4,实际到了 8
frame := etBodyRaw(1, 1, 2, 0, 1, 0, 4, bytes.Repeat([]byte{0xaa}, 8))
m, done := etFeedTCP(etTCP(), false, etSegment(frame))
if !done {
t.Fatal("stream not done on a length mismatch")
}
etExpect(t, m, "packet_type_name", "<missing>")
}
func TestEasyTier_AEADTailIsAllowedToOutrunTheDeclaredLength(t *testing.T) {
// 加密器追加 16 字节 tag 和 12 字节 nonce,不改头里声明的长度
payload := bytes.Repeat([]byte{0x11}, 40+easyTierAEADTailSize)
frame := etBodyRaw(1, 1, 2, easyTierFlagEncrypted, 1, 0, 40, payload)
m, _ := etFeedTCP(etTCP(), false, etSegment(frame))
if !etFlag(m, "encrypted") {
t.Fatal("encrypted not set")
}
etExpect(t, m, "payload_len", "40")
}
func TestEasyTier_CompressedPayloadMayComeInUnderTheDeclaredLength(t *testing.T) {
frame := etBodyRaw(1, 1, 2, easyTierFlagCompressed, 1, 0, 900, bytes.Repeat([]byte{0x22}, 61))
m, _ := etFeedTCP(etTCP(), false, etSegment(frame))
if !etFlag(m, "compressed") {
t.Fatal("compressed not set")
}
etExpect(t, m, "payload_len", "900")
}
func TestEasyTier_UnencryptedPayloadMustMatchTheDeclaredLengthExactly(t *testing.T) {
frame := etBodyRaw(1, 1, 2, 0, 1, 0, 40, bytes.Repeat([]byte{0x11}, 40+easyTierAEADTailSize))
if _, done := etFeedTCP(etTCP(), false, etSegment(frame)); !done {
t.Fatal("stream not done on an unencrypted length mismatch")
}
}
func TestEasyTier_ReservedByteIsZeroUnlessALivenessFlagIsSet(t *testing.T) {
withFlag := etBodyRaw(1, 1, 2, 0b0010_0000, 1, 0x5c, 4, make([]byte, 4))
m, _ := etFeedTCP(etTCP(), false, etSegment(withFlag))
etExpect(t, m, "packet_type_name", "data")
withoutFlag := etBodyRaw(1, 1, 2, 0, 1, 0x5c, 4, make([]byte, 4))
m, done := etFeedTCP(etTCP(), false, etSegment(withoutFlag))
if !done {
t.Fatal("stream not done on a non-zero reserved byte")
}
etExpect(t, m, "packet_type_name", "<missing>")
}
func TestEasyTier_ForwardCounterPastTheRoutersOwnCapIsRejected(t *testing.T) {
frame := etBodyRaw(1, 1, 2, 0, 9, 0, 4, make([]byte, 4))
m, done := etFeedTCP(etTCP(), false, etSegment(frame))
if !done {
t.Fatal("stream not done on forward counter 9")
}
etExpect(t, m, "packet_type_name", "<missing>")
}
func TestEasyTier_UnnamedPacketTypeIsRejected(t *testing.T) {
frame := etBodyRaw(22, 1, 2, 0, 1, 0, 4, make([]byte, 4))
if _, done := etFeedTCP(etTCP(), false, etSegment(frame)); !done {
t.Fatal("stream not done on packet type 22")
}
}
func TestEasyTier_FrameSplitAcrossSegmentsIsReassembled(t *testing.T) {
wire := etSegment(etBody(easyTierPktHandshake, 9, 0, etHandshakeRequest(9, "office-mesh")))
s := etTCP()
m, done := etFeedTCP(s, false, wire[:3])
if done {
t.Fatal("gave up on a length field that had not fully arrived")
}
etExpect(t, m, "packet_type_name", "<missing>")
m, done = etFeedTCP(s, false, wire[3:])
if done {
t.Fatal("stream done after a valid handshake")
}
if !etFlag(m, "yes") {
t.Fatal("yes not set after reassembly")
}
etExpect(t, m, "network_name", "office-mesh")
}
func TestEasyTier_ByteAtATimeDeliveryStillReadsTheFrame(t *testing.T) {
wire := etSegment(etBody(easyTierPktHandshake, 9, 0, etHandshakeRequest(9, "mesh")))
s := etTCP()
last := analyzer.PropMap{}
for _, b := range wire {
m, done := etFeedTCP(s, false, []byte{b})
if done {
t.Fatal("stream done mid-frame")
}
if len(m) != 0 {
last = m
}
}
if !etFlag(last, "yes") {
t.Fatal("yes not set after byte-at-a-time delivery")
}
etExpect(t, last, "network_name", "mesh")
}
func TestEasyTier_SeveralFramesPipelinedIntoOneSegmentAreAllRead(t *testing.T) {
wire := etSegment(etBody(1, 9, 8, bytes.Repeat([]byte{0x01}, 32)))
wire = append(wire, etSegment(etBody(4, 9, 8, nil))...)
wire = append(wire, etSegment(etBody(1, 9, 8, bytes.Repeat([]byte{0x03}, 64)))...)
m, done := etFeedTCP(etTCP(), false, wire)
if done {
t.Fatal("stream done after valid frames")
}
if !etFlag(m, "yes") {
t.Fatal("yes not set after three consistent frames")
}
etExpect(t, m, "tx_pkt_cnt", "3")
// map 描述的是最新一帧
etExpect(t, m, "payload_len", "64")
}
func TestEasyTier_NetworkNameSurvivesLaterFrames(t *testing.T) {
// 只发一次。按 mesh 名写的规则得在握手滚过去之后还能用
s := etTCP()
etFeedTCP(s, false, etSegment(etBody(easyTierPktHandshake, 9, 0, etHandshakeRequest(9, "office-mesh"))))
m, _ := etFeedTCP(s, false, etSegment(etBody(1, 9, 8, bytes.Repeat([]byte{0xee}, 128))))
etExpect(t, m, "network_name", "office-mesh")
etExpect(t, m, "packet_type_name", "data")
}
func TestEasyTier_GapInTheTCPStreamEndsIt(t *testing.T) {
u, done := etTCP().Feed(false, false, false, 7, []byte("whatever"))
if u != nil {
t.Fatal("update published on a gap")
}
if !done {
t.Fatal("stream not done on a gap")
}
}
func TestEasyTier_OrdinaryTrafficIsNotMistakenForATunnel(t *testing.T) {
m, done := etFeedTCP(etTCP(), false, []byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"))
if !done || etFlag(m, "yes") {
t.Fatalf("HTTP: done=%v yes=%v", done, etFlag(m, "yes"))
}
// TLS ClientHello 记录:16 03 01 <len> ... 读出来是 21 MB 的帧体长度,直接拒绝
m, done = etFeedTCP(etTCP(), false, []byte{0x16, 0x03, 0x01, 0x01, 0x4a, 0x01, 0x00, 0x01, 0x46})
if !done || etFlag(m, "yes") {
t.Fatalf("TLS: done=%v yes=%v", done, etFlag(m, "yes"))
}
}
func TestEasyTier_FETDoesNotCatchTheTCPHandshake(t *testing.T) {
// fet 只看客户端第一个分段,判完就 done。EasyTier 连接的第一个分段永远是明文握手,
// 整条连接上唯一低熵的东西,所以 fet 读作"非全加密",引擎放行,后面真正加密的帧再也不看。
// 而且能不能中取决于握手里碰巧有多少随机内容(ex1 落在 3.4 豁免线附近,随网络名长短摇摆),
// 一个随运营者起的名字长短而变的检测器不是检测器。
digest := make([]byte, 32)
for i := range digest {
digest[i] = byte(uint32(i) * 2654435761)
}
payload := etProtoVarint(1, easyTierHandshakeMagic)
payload = append(payload, etProtoVarint(2, 0x1a2b3c4d)...)
payload = append(payload, etProtoVarint(3, 1)...)
payload = append(payload, etProtoBytes(4, []byte("liveness-echo-v1"))...)
payload = append(payload, etProtoBytes(5, []byte("mesh"))...)
payload = append(payload, etProtoBytes(6, digest)...)
wire := etSegment(etBody(easyTierPktHandshake, 0x1a2b3c4d, 0, payload))
fet := (&tcp.FETAnalyzer{}).NewTCP(analyzer.TCPInfo{}, nil)
u, done := fet.Feed(false, true, false, 0, wire)
if !done {
t.Fatal("fet should decide on the first segment and not look again")
}
if u != nil && etFlag(u.M, "yes") {
t.Fatal("fet claimed this handshake, so this analyzer would be redundant")
}
// 同样的字节,给本分析器
m, _ := etFeedTCP(etTCP(), false, wire)
if !etFlag(m, "yes") {
t.Fatal("easytier did not claim its own handshake")
}
etExpect(t, m, "network_name", "mesh")
}
func TestEasyTier_WireGuardAnalyzerDoesNotClaimUDPTransport(t *testing.T) {
// wireguard 覆盖的是 EasyTier 的 wg:// 监听,六分之一。udp:// 上没有任何东西读起来像 WireGuard 报文
datagrams := [][]byte{
etSyn(0x12345678, 0x0102030405060708),
etDatagram(7, easyTierMsgData, etBody(easyTierPktHandshake, 0x1a2b3c4d, 0, etHandshakeRequest(9, "mesh"))),
etDatagram(1, easyTierMsgHolePunch, bytes.Repeat([]byte{0x7f}, 16)),
}
for i, data := range datagrams {
wg := (&WireGuardAnalyzer{}).NewUDP(analyzer.UDPInfo{}, nil)
if u, _ := wg.Feed(false, data); u != nil {
t.Fatalf("wireguard claimed easytier datagram %d: %v", i, u.M)
}
}
}
func TestEasyTier_STUNOnTheSameSocketIsNeitherEvidenceNorHeldAgainstTheFlow(t *testing.T) {
// EasyTier 把 STUN 复用在自己的 UDP socket 上,并在自己的帧解析之前分流;四个 STUN 不能结束流
stun := append([]byte{0x00, 0x01, 0x00, 0x00, 0x21, 0x12, 0xa4, 0x42}, bytes.Repeat([]byte{0x11}, 12)...)
s := etUDP()
for i := 0; i < 8; i++ {
m, done := etFeedUDP(s, false, stun)
if done {
t.Fatalf("STUN datagram %d ended the flow", i+1)
}
if len(m) != 0 {
t.Fatalf("STUN datagram %d published props: %v", i+1, m)
}
}
m, _ := etFeedUDP(s, false, etSyn(1, 2))
etExpect(t, m, "msg_type_name", "syn")
}
-31
View File
@@ -1,31 +0,0 @@
Author:: Cuong Manh Le <cuong.manhle.vn@gmail.com>
Copyright:: Copyright (c) 2023, Cuong Manh Le
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of the @organization@ nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL LE MANH CUONG
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-1
View File
@@ -1 +0,0 @@
The code here is from https://github.com/cuonglm/quicsni with various modifications.
-105
View File
@@ -1,105 +0,0 @@
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
}
@@ -1,193 +0,0 @@
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
}
@@ -1,94 +0,0 @@
package quic
import (
"bytes"
"crypto"
"crypto/tls"
"encoding/hex"
"strings"
"testing"
"unicode"
"golang.org/x/crypto/hkdf"
)
func TestInitialPacketProtector_UnProtect(t *testing.T) {
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-server-initial
protect := mustHexDecodeString(`
c7ff0000200008f067a5502a4262b500 4075fb12ff07823a5d24534d906ce4c7
6782a2167e3479c0f7f6395dc2c91676 302fe6d70bb7cbeb117b4ddb7d173498
44fd61dae200b8338e1b932976b61d91 e64a02e9e0ee72e3a6f63aba4ceeeec5
be2f24f2d86027572943533846caa13e 6f163fb257473d0eda5047360fd4a47e
fd8142fafc0f76
`)
unProtect := mustHexDecodeString(`
02000000000600405a020000560303ee fce7f7b37ba1d1632e96677825ddf739
88cfc79825df566dc5430b9a045a1200 130100002e00330024001d00209d3c94
0d89690b84d08a60993c144eca684d10 81287c834d5311bcf32bb9da1a002b00
020304
`)
connID := mustHexDecodeString(`8394c8f03e515708`)
packet := append([]byte{}, protect...)
hdr, offset, err := ParseInitialHeader(packet)
if err != nil {
t.Fatal(err)
}
initialSecret := hkdf.Extract(crypto.SHA256.New, connID, getSalt(hdr.Version))
serverSecret := hkdfExpandLabel(crypto.SHA256.New, initialSecret, "server in", []byte{}, crypto.SHA256.Size())
key, err := NewInitialProtectionKey(serverSecret, hdr.Version)
if err != nil {
t.Fatal(err)
}
pp := NewPacketProtector(key)
got, err := pp.UnProtect(protect, offset, 1)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, unProtect) {
t.Error("UnProtect returns wrong result")
}
}
func TestPacketProtectorShortHeader_UnProtect(t *testing.T) {
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-chacha20-poly1305-short-hea
protect := mustHexDecodeString(`4cfe4189655e5cd55c41f69080575d7999c25a5bfb`)
unProtect := mustHexDecodeString(`01`)
hdr := mustHexDecodeString(`4200bff4`)
secret := mustHexDecodeString(`9ac312a7f877468ebe69422748ad00a1 5443f18203a07d6060f688f30f21632b`)
k, err := NewProtectionKey(tls.TLS_CHACHA20_POLY1305_SHA256, secret, V1)
if err != nil {
t.Fatal(err)
}
pnLen := int(hdr[0]&0x03) + 1
offset := len(hdr) - pnLen
pp := NewPacketProtector(k)
got, err := pp.UnProtect(protect, int64(offset), 654360564)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(got, unProtect) {
t.Error("UnProtect returns wrong result")
}
}
func mustHexDecodeString(s string) []byte {
b, err := hex.DecodeString(normalizeHex(s))
if err != nil {
panic(err)
}
return b
}
func normalizeHex(s string) string {
return strings.Map(func(c rune) rune {
if unicode.IsSpace(c) {
return -1
}
return c
}, s)
}
-122
View File
@@ -1,122 +0,0 @@
package quic
import (
"bytes"
"crypto"
"errors"
"fmt"
"io"
"sort"
"github.com/quic-go/quic-go/quicvarint"
"golang.org/x/crypto/hkdf"
)
func ReadCryptoPayload(packet []byte) ([]byte, error) {
hdr, offset, err := ParseInitialHeader(packet)
if err != nil {
return nil, err
}
// Some sanity checks
if hdr.Version != V1 && hdr.Version != V2 {
return nil, fmt.Errorf("unsupported version: %x", hdr.Version)
}
if offset == 0 || hdr.Length == 0 {
return nil, errors.New("invalid packet")
}
initialSecret := hkdf.Extract(crypto.SHA256.New, hdr.DestConnectionID, getSalt(hdr.Version))
clientSecret := hkdfExpandLabel(crypto.SHA256.New, initialSecret, "client in", []byte{}, crypto.SHA256.Size())
key, err := NewInitialProtectionKey(clientSecret, hdr.Version)
if err != nil {
return nil, fmt.Errorf("NewInitialProtectionKey: %w", err)
}
pp := NewPacketProtector(key)
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-client-initial
//
// "The unprotected header includes the connection ID and a 4-byte packet number encoding for a packet number of 2"
if int64(len(packet)) < offset+hdr.Length {
return nil, fmt.Errorf("packet is too short: %d < %d", len(packet), offset+hdr.Length)
}
unProtectedPayload, err := pp.UnProtect(packet[:offset+hdr.Length], offset, 2)
if err != nil {
return nil, err
}
frs, err := extractCryptoFrames(bytes.NewReader(unProtectedPayload))
if err != nil {
return nil, err
}
data := assembleCryptoFrames(frs)
if data == nil {
return nil, errors.New("unable to assemble crypto frames")
}
return data, nil
}
const (
paddingFrameType = 0x00
pingFrameType = 0x01
cryptoFrameType = 0x06
)
type cryptoFrame struct {
Offset int64
Data []byte
}
func extractCryptoFrames(r *bytes.Reader) ([]cryptoFrame, error) {
var frames []cryptoFrame
for r.Len() > 0 {
typ, err := quicvarint.Read(r)
if err != nil {
return nil, err
}
if typ == paddingFrameType || typ == pingFrameType {
continue
}
if typ != cryptoFrameType {
return nil, fmt.Errorf("encountered unexpected frame type: %d", typ)
}
var frame cryptoFrame
offset, err := quicvarint.Read(r)
if err != nil {
return nil, err
}
frame.Offset = int64(offset)
dataLen, err := quicvarint.Read(r)
if err != nil {
return nil, err
}
frame.Data = make([]byte, dataLen)
if _, err := io.ReadFull(r, frame.Data); err != nil {
return nil, err
}
frames = append(frames, frame)
}
return frames, nil
}
// assembleCryptoFrames assembles multiple crypto frames into a single slice (if possible).
// It returns an error if the frames cannot be assembled. This can happen if the frames are not contiguous.
func assembleCryptoFrames(frames []cryptoFrame) []byte {
if len(frames) == 0 {
return nil
}
if len(frames) == 1 {
return frames[0].Data
}
// sort the frames by offset
sort.Slice(frames, func(i, j int) bool { return frames[i].Offset < frames[j].Offset })
// check if the frames are contiguous
for i := 1; i < len(frames); i++ {
if frames[i].Offset != frames[i-1].Offset+int64(len(frames[i-1].Data)) {
return nil
}
}
// concatenate the frames
data := make([]byte, frames[len(frames)-1].Offset+int64(len(frames[len(frames)-1].Data)))
for _, frame := range frames {
copy(data[frame.Offset:], frame.Data)
}
return data
}
-59
View File
@@ -1,59 +0,0 @@
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
View File
@@ -1,384 +0,0 @@
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
View File
@@ -1,81 +0,0 @@
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
View File
@@ -1,58 +0,0 @@
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)
}
}
-217
View File
@@ -1,217 +0,0 @@
package udp
import (
"container/ring"
"encoding/binary"
"slices"
"sync"
"github.com/apernet/OpenGFW/analyzer"
)
var (
_ analyzer.UDPAnalyzer = (*WireGuardAnalyzer)(nil)
_ analyzer.UDPStream = (*wireGuardUDPStream)(nil)
)
const (
wireguardUDPInvalidCountThreshold = 4
wireguardRememberedIndexCount = 6
wireguardPropKeyMessageType = "message_type"
)
const (
wireguardTypeHandshakeInitiation = 1
wireguardTypeHandshakeResponse = 2
wireguardTypeData = 4
wireguardTypeCookieReply = 3
)
const (
wireguardSizeHandshakeInitiation = 148
wireguardSizeHandshakeResponse = 92
wireguardMinSizePacketData = 32 // 16 bytes header + 16 bytes AEAD overhead
wireguardSizePacketCookieReply = 64
)
type WireGuardAnalyzer struct{}
func (a *WireGuardAnalyzer) Name() string {
return "wireguard"
}
func (a *WireGuardAnalyzer) Limit() int {
return 0
}
func (a *WireGuardAnalyzer) NewUDP(info analyzer.UDPInfo, logger analyzer.Logger) analyzer.UDPStream {
return newWireGuardUDPStream(logger)
}
type wireGuardUDPStream struct {
logger analyzer.Logger
invalidCount int
rememberedIndexes *ring.Ring
rememberedIndexesLock sync.RWMutex
}
func newWireGuardUDPStream(logger analyzer.Logger) *wireGuardUDPStream {
return &wireGuardUDPStream{
logger: logger,
rememberedIndexes: ring.New(wireguardRememberedIndexCount),
}
}
func (s *wireGuardUDPStream) Feed(rev bool, data []byte) (u *analyzer.PropUpdate, done bool) {
m := s.parseWireGuardPacket(rev, data)
if m == nil {
s.invalidCount++
return nil, s.invalidCount >= wireguardUDPInvalidCountThreshold
}
s.invalidCount = 0 // Reset invalid count on valid WireGuard packet
messageType := m[wireguardPropKeyMessageType].(byte)
propUpdateType := analyzer.PropUpdateMerge
if messageType == wireguardTypeHandshakeInitiation {
propUpdateType = analyzer.PropUpdateReplace
}
return &analyzer.PropUpdate{
Type: propUpdateType,
M: m,
}, false
}
func (s *wireGuardUDPStream) Close(limited bool) *analyzer.PropUpdate {
return nil
}
func (s *wireGuardUDPStream) parseWireGuardPacket(rev bool, data []byte) analyzer.PropMap {
if len(data) < 4 {
return nil
}
if slices.Max(data[1:4]) != 0 {
return nil
}
messageType := data[0]
var propKey string
var propValue analyzer.PropMap
switch messageType {
case wireguardTypeHandshakeInitiation:
propKey = "handshake_initiation"
propValue = s.parseWireGuardHandshakeInitiation(rev, data)
case wireguardTypeHandshakeResponse:
propKey = "handshake_response"
propValue = s.parseWireGuardHandshakeResponse(rev, data)
case wireguardTypeData:
propKey = "packet_data"
propValue = s.parseWireGuardPacketData(rev, data)
case wireguardTypeCookieReply:
propKey = "packet_cookie_reply"
propValue = s.parseWireGuardPacketCookieReply(rev, data)
}
if propValue == nil {
return nil
}
m := make(analyzer.PropMap)
m[wireguardPropKeyMessageType] = messageType
m[propKey] = propValue
return m
}
func (s *wireGuardUDPStream) parseWireGuardHandshakeInitiation(rev bool, data []byte) analyzer.PropMap {
if len(data) != wireguardSizeHandshakeInitiation {
return nil
}
m := make(analyzer.PropMap)
senderIndex := binary.LittleEndian.Uint32(data[4:8])
m["sender_index"] = senderIndex
s.putSenderIndex(rev, senderIndex)
return m
}
func (s *wireGuardUDPStream) parseWireGuardHandshakeResponse(rev bool, data []byte) analyzer.PropMap {
if len(data) != wireguardSizeHandshakeResponse {
return nil
}
m := make(analyzer.PropMap)
senderIndex := binary.LittleEndian.Uint32(data[4:8])
m["sender_index"] = senderIndex
s.putSenderIndex(rev, senderIndex)
receiverIndex := binary.LittleEndian.Uint32(data[8:12])
m["receiver_index"] = receiverIndex
m["receiver_index_matched"] = s.matchReceiverIndex(rev, receiverIndex)
return m
}
func (s *wireGuardUDPStream) parseWireGuardPacketData(rev bool, data []byte) analyzer.PropMap {
if len(data) < wireguardMinSizePacketData {
return nil
}
if len(data)%16 != 0 {
// WireGuard zero padding the packet to make the length a multiple of 16
return nil
}
m := make(analyzer.PropMap)
receiverIndex := binary.LittleEndian.Uint32(data[4:8])
m["receiver_index"] = receiverIndex
m["receiver_index_matched"] = s.matchReceiverIndex(rev, receiverIndex)
m["counter"] = binary.LittleEndian.Uint64(data[8:16])
return m
}
func (s *wireGuardUDPStream) parseWireGuardPacketCookieReply(rev bool, data []byte) analyzer.PropMap {
if len(data) != wireguardSizePacketCookieReply {
return nil
}
m := make(analyzer.PropMap)
receiverIndex := binary.LittleEndian.Uint32(data[4:8])
m["receiver_index"] = receiverIndex
m["receiver_index_matched"] = s.matchReceiverIndex(rev, receiverIndex)
return m
}
type wireGuardIndex struct {
SenderIndex uint32
Reverse bool
}
func (s *wireGuardUDPStream) putSenderIndex(rev bool, senderIndex uint32) {
s.rememberedIndexesLock.Lock()
defer s.rememberedIndexesLock.Unlock()
s.rememberedIndexes.Value = &wireGuardIndex{
SenderIndex: senderIndex,
Reverse: rev,
}
s.rememberedIndexes = s.rememberedIndexes.Prev()
}
func (s *wireGuardUDPStream) matchReceiverIndex(rev bool, receiverIndex uint32) bool {
s.rememberedIndexesLock.RLock()
defer s.rememberedIndexesLock.RUnlock()
var found bool
ris := s.rememberedIndexes
for it := ris.Next(); it != ris; it = it.Next() {
if it.Value == nil {
break
}
wgidx := it.Value.(*wireGuardIndex)
if wgidx.Reverse == !rev && wgidx.SenderIndex == receiverIndex {
found = true
break
}
}
return found
}
-99
View File
@@ -1,99 +0,0 @@
package utils
import "bytes"
type ByteBuffer struct {
Buf []byte
}
func (b *ByteBuffer) Append(data []byte) {
b.Buf = append(b.Buf, data...)
}
func (b *ByteBuffer) Len() int {
return len(b.Buf)
}
func (b *ByteBuffer) Index(sep []byte) int {
return bytes.Index(b.Buf, sep)
}
func (b *ByteBuffer) Get(length int, consume bool) (data []byte, ok bool) {
if len(b.Buf) < length {
return nil, false
}
data = b.Buf[:length]
if consume {
b.Buf = b.Buf[length:]
}
return data, true
}
func (b *ByteBuffer) GetString(length int, consume bool) (string, bool) {
data, ok := b.Get(length, consume)
if !ok {
return "", false
}
return string(data), true
}
func (b *ByteBuffer) GetByte(consume bool) (byte, bool) {
data, ok := b.Get(1, consume)
if !ok {
return 0, false
}
return data[0], true
}
func (b *ByteBuffer) GetUint16(littleEndian, consume bool) (uint16, bool) {
data, ok := b.Get(2, consume)
if !ok {
return 0, false
}
if littleEndian {
return uint16(data[0]) | uint16(data[1])<<8, true
}
return uint16(data[1]) | uint16(data[0])<<8, true
}
func (b *ByteBuffer) GetUint32(littleEndian, consume bool) (uint32, bool) {
data, ok := b.Get(4, consume)
if !ok {
return 0, false
}
if littleEndian {
return uint32(data[0]) | uint32(data[1])<<8 | uint32(data[2])<<16 | uint32(data[3])<<24, true
}
return uint32(data[3]) | uint32(data[2])<<8 | uint32(data[1])<<16 | uint32(data[0])<<24, true
}
func (b *ByteBuffer) GetUntil(sep []byte, includeSep, consume bool) (data []byte, ok bool) {
index := b.Index(sep)
if index == -1 {
return nil, false
}
if includeSep {
index += len(sep)
}
return b.Get(index, consume)
}
func (b *ByteBuffer) GetSubBuffer(length int, consume bool) (sub *ByteBuffer, ok bool) {
data, ok := b.Get(length, consume)
if !ok {
return nil, false
}
return &ByteBuffer{Buf: data}, true
}
func (b *ByteBuffer) Skip(length int) bool {
if len(b.Buf) < length {
return false
}
b.Buf = b.Buf[length:]
return true
}
func (b *ByteBuffer) Reset() {
b.Buf = nil
}
-54
View File
@@ -1,54 +0,0 @@
package utils
type LSMAction int
const (
LSMActionPause LSMAction = iota
LSMActionNext
LSMActionReset
LSMActionCancel
)
type LinearStateMachine struct {
Steps []func() LSMAction
index int
cancelled bool
}
func NewLinearStateMachine(steps ...func() LSMAction) *LinearStateMachine {
return &LinearStateMachine{
Steps: steps,
}
}
// Run runs the state machine until it pauses, finishes or is cancelled.
func (lsm *LinearStateMachine) Run() (cancelled bool, done bool) {
if lsm.index >= len(lsm.Steps) {
return lsm.cancelled, true
}
for lsm.index < len(lsm.Steps) {
action := lsm.Steps[lsm.index]()
switch action {
case LSMActionPause:
return false, false
case LSMActionNext:
lsm.index++
case LSMActionReset:
lsm.index = 0
case LSMActionCancel:
lsm.cancelled = true
return true, true
}
}
return false, true
}
func (lsm *LinearStateMachine) AppendSteps(steps ...func() LSMAction) {
lsm.Steps = append(lsm.Steps, steps...)
}
func (lsm *LinearStateMachine) Reset() {
lsm.index = 0
lsm.cancelled = false
}
-9
View File
@@ -1,9 +0,0 @@
package utils
func ByteSlicesToStrings(bss [][]byte) []string {
ss := make([]string, len(bss))
for i, bs := range bss {
ss[i] = string(bs)
}
return ss
}
-18
View File
@@ -1,18 +0,0 @@
package cmd
import (
"fmt"
)
type configError struct {
Field string
Err error
}
func (e configError) Error() string {
return fmt.Sprintf("invalid config: %s: %s", e.Field, e.Err)
}
func (e configError) Unwrap() error {
return e.Err
}
-487
View File
@@ -1,487 +0,0 @@
package cmd
import (
"context"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/analyzer/tcp"
"github.com/apernet/OpenGFW/analyzer/udp"
"github.com/apernet/OpenGFW/engine"
"github.com/apernet/OpenGFW/io"
"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"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
const (
appLogo = `
░█▀█░█▀█░█▀▀░█▀█░█▀▀░█▀▀░█░█
░█░█░█▀▀░█▀▀░█░█░█░█░█▀▀░█▄█
░▀▀▀░▀░░░▀▀▀░▀░▀░▀▀▀░▀░░░▀░▀
`
appDesc = "Open source network filtering and analysis software"
appAuthors = "Aperture Internet Laboratory <https://github.com/apernet>"
appLogLevelEnv = "OPENGFW_LOG_LEVEL"
appLogFormatEnv = "OPENGFW_LOG_FORMAT"
)
var logger *zap.Logger
// Flags
var (
cfgFile string
pcapFile string
logLevel string
logFormat string
)
var rootCmd = &cobra.Command{
Use: "OpenGFW [flags] rule_file",
Short: appDesc,
Args: cobra.ExactArgs(1),
Run: runMain,
}
var logLevelMap = map[string]zapcore.Level{
"debug": zapcore.DebugLevel,
"info": zapcore.InfoLevel,
"warn": zapcore.WarnLevel,
"error": zapcore.ErrorLevel,
}
var logFormatMap = map[string]zapcore.EncoderConfig{
"console": {
TimeKey: "time",
LevelKey: "level",
NameKey: "logger",
MessageKey: "msg",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.CapitalColorLevelEncoder,
EncodeTime: zapcore.RFC3339TimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
},
"json": {
TimeKey: "time",
LevelKey: "level",
NameKey: "logger",
MessageKey: "msg",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeTime: zapcore.EpochMillisTimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
},
}
// Analyzers & modifiers
var analyzers = []analyzer.Analyzer{
&tcp.FETAnalyzer{},
&tcp.HTTPAnalyzer{},
&tcp.MinecraftAnalyzer{},
&tcp.SocksAnalyzer{},
&tcp.SSHAnalyzer{},
&tcp.TLSAnalyzer{},
&tcp.TrojanAnalyzer{},
&udp.DNSAnalyzer{},
&udp.EasyTierAnalyzer{},
&udp.OpenVPNAnalyzer{},
&udp.QUICAnalyzer{},
&udp.WireGuardAnalyzer{},
}
var modifiers = []modifier.Modifier{
&modUDP.DNSModifier{},
}
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
initFlags()
cobra.OnInitialize(initConfig)
cobra.OnInitialize(initLogger) // initLogger must come after initConfig as it depends on config
}
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")
}
func initConfig() {
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.SupportedExts = append([]string{"yaml", "yml"}, viper.SupportedExts...)
viper.AddConfigPath(".")
viper.AddConfigPath("$HOME/.opengfw")
viper.AddConfigPath("/etc/opengfw")
}
}
func initLogger() {
level, ok := logLevelMap[strings.ToLower(logLevel)]
if !ok {
fmt.Printf("unsupported log level: %s\n", logLevel)
os.Exit(1)
}
enc, ok := logFormatMap[strings.ToLower(logFormat)]
if !ok {
fmt.Printf("unsupported log format: %s\n", logFormat)
os.Exit(1)
}
c := zap.Config{
Level: zap.NewAtomicLevelAt(level),
DisableCaller: true,
DisableStacktrace: true,
Encoding: strings.ToLower(logFormat),
EncoderConfig: enc,
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
}
var err error
logger, err = c.Build()
if err != nil {
fmt.Printf("failed to initialize logger: %s\n", err)
os.Exit(1)
}
}
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"`
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"`
TCPTimeout time.Duration `mapstructure:"tcpTimeout"`
UDPMaxStreams int `mapstructure:"udpMaxStreams"`
}
type cliConfigRuleset struct {
GeoIp string `mapstructure:"geoip"`
GeoSite string `mapstructure:"geosite"`
}
func (c *cliConfig) fillLogger(config *engine.Config) error {
config.Logger = &engineLogger{}
return nil
}
func (c *cliConfig) fillIO(config *engine.Config) error {
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.IO = ioImpl
return nil
}
func (c *cliConfig) fillWorkers(config *engine.Config) error {
config.Workers = c.Workers.Count
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
}
// Config validates the fields and returns a ready-to-use engine config.
// This does not include the ruleset.
func (c *cliConfig) Config() (*engine.Config, error) {
engineConfig := &engine.Config{}
fillers := []func(*engine.Config) error{
c.fillLogger,
c.fillIO,
c.fillWorkers,
}
for _, f := range fillers {
if err := f(engineConfig); err != nil {
return nil, err
}
}
return engineConfig, nil
}
func runMain(cmd *cobra.Command, args []string) {
// Config
if err := viper.ReadInConfig(); err != nil {
logger.Fatal("failed to read config", zap.Error(err))
}
var config cliConfig
if err := viper.Unmarshal(&config); err != nil {
logger.Fatal("failed to parse config", zap.Error(err))
}
engineConfig, err := config.Config()
if err != nil {
logger.Fatal("failed to parse config", zap.Error(err))
}
defer engineConfig.IO.Close() // Make sure to close IO on exit
// Ruleset
rawRs, err := ruleset.ExprRulesFromYAML(args[0])
if err != nil {
logger.Fatal("failed to load rules", zap.Error(err))
}
rsConfig := &ruleset.BuiltinConfig{
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 {
logger.Fatal("failed to compile rules", zap.Error(err))
}
engineConfig.Ruleset = rs
// Engine
en, err := engine.NewEngine(*engineConfig)
if err != nil {
logger.Fatal("failed to initialize engine", zap.Error(err))
}
// Signal handling
ctx, cancelFunc := context.WithCancel(context.Background())
go func() {
// Graceful shutdown
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, 1)
signal.Notify(reloadChan, syscall.SIGHUP)
for {
<-reloadChan
logger.Info("reloading rules")
rawRs, err := ruleset.ExprRulesFromYAML(args[0])
if err != nil {
logger.Error("failed to load rules, using old rules", zap.Error(err))
continue
}
rs, err := ruleset.CompileExprRules(rawRs, analyzers, modifiers, rsConfig)
if err != nil {
logger.Error("failed to compile rules, using old rules", zap.Error(err))
continue
}
err = en.UpdateRuleset(rs)
if err != nil {
logger.Error("failed to update ruleset", zap.Error(err))
} else {
logger.Info("rules reloaded")
}
}
}()
logger.Info("engine started")
logger.Info("engine exited", zap.Error(en.Run(ctx)))
}
type engineLogger struct{}
func (l *engineLogger) WorkerStart(id int) {
logger.Debug("worker started", zap.Int("id", id))
}
func (l *engineLogger) WorkerStop(id int) {
logger.Debug("worker stopped", zap.Int("id", id))
}
func (l *engineLogger) TCPStreamNew(workerID int, info ruleset.StreamInfo) {
logger.Debug("new TCP stream",
zap.Int("workerID", workerID),
zap.Int64("id", info.ID),
zap.String("src", info.SrcString()),
zap.String("dst", info.DstString()))
}
func (l *engineLogger) TCPStreamPropUpdate(info ruleset.StreamInfo, close bool) {
logger.Debug("TCP stream property update",
zap.Int64("id", info.ID),
zap.String("src", info.SrcString()),
zap.String("dst", info.DstString()),
zap.Any("props", info.Props),
zap.Bool("close", close))
}
func (l *engineLogger) TCPStreamAction(info ruleset.StreamInfo, action ruleset.Action, noMatch bool) {
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) {
logger.Debug("new UDP stream",
zap.Int("workerID", workerID),
zap.Int64("id", info.ID),
zap.String("src", info.SrcString()),
zap.String("dst", info.DstString()))
}
func (l *engineLogger) UDPStreamPropUpdate(info ruleset.StreamInfo, close bool) {
logger.Debug("UDP stream property update",
zap.Int64("id", info.ID),
zap.String("src", info.SrcString()),
zap.String("dst", info.DstString()),
zap.Any("props", info.Props),
zap.Bool("close", close))
}
func (l *engineLogger) UDPStreamAction(info ruleset.StreamInfo, action ruleset.Action, noMatch bool) {
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) {
logger.Error("modify error",
zap.Int64("id", info.ID),
zap.String("src", info.SrcString()),
zap.String("dst", info.DstString()),
zap.Error(err))
}
func (l *engineLogger) AnalyzerDebugf(streamID int64, name string, format string, args ...interface{}) {
logger.Debug("analyzer debug message",
zap.Int64("id", streamID),
zap.String("name", name),
zap.String("msg", fmt.Sprintf(format, args...)))
}
func (l *engineLogger) AnalyzerInfof(streamID int64, name string, format string, args ...interface{}) {
logger.Info("analyzer info message",
zap.Int64("id", streamID),
zap.String("name", name),
zap.String("msg", fmt.Sprintf(format, args...)))
}
func (l *engineLogger) AnalyzerErrorf(streamID int64, name string, format string, args ...interface{}) {
logger.Error("analyzer error message",
zap.Int64("id", streamID),
zap.String("name", name),
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
}
-241
View File
@@ -1,241 +0,0 @@
# EasyTier 分析器
识别 [EasyTier](https://github.com/EasyTier/EasyTier) 去中心化 Mesh VPN 的对等连接。
## 传输方式与可见性
EasyTier 的对等连接可以走六种传输,节点默认全部监听:
| scheme | 默认端口 | 线上是什么样 | 谁负责 |
| --- | --- | --- | --- |
| `tcp://` | 11010 | EasyTier 自己的定长帧,明文头 | **本分析器** |
| `udp://` | 11010 | EasyTier 自己的数据报头,明文头 | **本分析器** |
| `ws://` | 11011 | HTTP Upgrade,之后是 WebSocket 二进制帧 | `http`(握手可见),帧内容不解 |
| `wss://` | 11012 | TLS(自签证书,CN=`localhost`) | `tls`(SNI/证书层面) |
| `wg://` | 11013 | 标准 WireGuard 报文 | `wireguard` |
| `quic://` | 11010 | QUIC,未设置 ALPN | `quic` |
只有前两种把 EasyTier 的帧结构直接放在网络层上,也是唯二能拿到网络名、对端 ID 这些信息的。下面说的都是这两种。
## 为什么 `wireguard` 和 `fet` 不够
这两个现有分析器看起来应该能覆盖,实测都不行。
### `wireguard`:只挡得住六分之一
它匹配的是真正的 WireGuard 报文,也就是 EasyTier 的 `wg://` 传输(默认 11013)。问题在于**节点默认把六种传输全部监听起来**,连接方按拿到的 URI 列表逐个尝试,哪个通用哪个。只挡 `wg://`,mesh 换 `udp://` 照样建起来,而 `udp://` 恰恰是 NAT 穿透场景下的首选。
另外 WireGuard 报文里没有网络名,拦了也不知道拦的是哪个 mesh,而且会一并拦掉合规的 WireGuard VPN。粒度太粗。
`TestEasyTier_WireGuardAnalyzerDoesNotClaimUDPTransport` 这个用例把 `udp://` 上的 Syn、握手、打洞包依次喂给 `wireguard` 分析器,确认它一个都不认。
### `fet`:判在了整条连接熵最低的那一段上
`fet` 检测「全加密流量」,只看客户端**第一个**分段,判完就 `done`。而 EasyTier 连接的第一个分段永远是明文握手帧,整条连接上唯一不加密的东西。
那一帧长什么样:`to_peer_id` 四个零、`flags`/`reserved` 是零、两个长度字段的高位字节是零、`liveness-echo-v1` 和网络名是 ASCII。实测 `ex1`(平均 popcount)落在 3.4 这条豁免线附近,而且**结果取决于网络名有多长**:
| 网络名 | `fet.yes` 命中率(200 组随机 digest) | `ex1` 区间 |
| --- | --- | --- |
| `a` | 0/200 | 3.045..3.258 |
| `mesh` | 0/200 | 3.076..3.283 |
| `office-mesh` | 3/200 | 3.242..3.434 |
| `my-home-network` | 52/200 | 3.320..3.505 |
握手不带真实 digest 时(跨网络/共享节点场景,字段填 32 个零)更低,四种名字全是 0/200,`ex1` 只有 1.6..2.2。
也就是说 `fet` 在这里是**大概率漏、小概率中**,而且能不能中取决于对方给自己的网络起了个多长的名字。而后续真正加密的数据帧 `ex1` 有 3.92,`fet` 本来能认,但它早就判完退出了。
更麻烦的是漏判的后果:`fet` 报 `yes=false` 且没有别的规则拦截时,引擎会把这条流放进快速通道(connmark bypass),之后的包**根本不再进用户态**。一次漏判就是永久放行,和 `docs/tcp-window-evasion.zh.md` 里描述的是同一个后果。
`TestEasyTier_FETDoesNotCatchTheTCPHandshake` 这个用例把这一点钉住了。
## 报文结构
三个头,全部小端,全部 `#[repr(C, packed)]`,没有任何对齐填充。
**`UDPTunnelHeader`(8 字节,仅 UDP)**
| 偏移 | 字段 | 说明 |
| --- | --- | --- |
| 0..4 | `conn_id` u32 | 会话 ID,发起方随机选取 |
| 4 | `msg_type` u8 | 1=Syn 2=Sack 3=Data 4=Fin\* 5=HolePunch 6=V4HolePunch 7=V6HolePunch(\* 枚举里有,当前实现从不发送) |
| 5 | `padding` u8 | 恒为 0,只为对齐长度字段而存在 |
| 6..8 | `len` u16 | 后面的字节数,**必须**等于 `数据报长度 - 8` |
**`TCPTunnelHeader`(4 字节,仅 TCP)**
一个 u32 长度前缀,值等于后面帧体的字节数。上游要求 `16 ≤ len ≤ max_packet_size`(TCP 隧道 2000,字节流隧道 4096)。
**`PeerManagerHeader`(16 字节,两种传输都有)**
| 偏移 | 字段 | 说明 |
| --- | --- | --- |
| 0..4 | `from_peer_id` u32 | 源节点 ID |
| 4..8 | `to_peer_id` u32 | 目的节点 ID,首包为 0 |
| 8 | `packet_type` u8 | 见下表,取值 1..=21 |
| 9 | `flags` u8 | bit0 加密 bit1 延迟优先 bit2 出口节点 bit3 不代理 bit4 压缩 bit5/7 存活探测 bit6 不送 TUN |
| 10 | `forward_counter` u8 | 中继跳数,路由在 >7 时丢弃后再自增,所以线上最大 8 |
| 11 | `reserved` u8 | 存活标志位(bit5/bit7)置位时是探测 token,否则恒为 0 |
| 12..16 | `len` u32 | 载荷长度 |
`packet_type` 取值:1 Data、2 HandShake、3 RoutePacket*、4 Ping、5 Pong、6 TaRpc*、7 Route*、8 RpcReq、9 RpcResp、10 ForeignNetworkPacket、11 KcpSrc、12 KcpDst、13/14/15 NoiseHandshakeMsg1/2/3、16 QuicSrc、17 QuicDst、18/19 DataWith{Kcp,Quic}SrcModified、20 RelayHandshake、21 RelayHandshakeAck(\* = 已废弃但仍占位)。
### 关键点:头不加密
EasyTier 的三种加密(AES-GCM / ChaCha20 / XOR)**只覆盖载荷**,`PeerManagerHeader` 自始至终是明文,加密只是把 `flags` 的 bit0 置位(`tunnel/encrypt/aes_gcm.rs` 等)。压缩同理,置 bit4。
所以哪怕整个 mesh 开了加密,对端 ID、报文类型、跳数、声明的载荷长度仍然一直挂在明面上。这是本分析器的立足点。
## 判定信号
分三档,从弱到强。
### 一、帧结构自洽(`easyTierEvidenceFrame`)
每一帧里有两个互相冗余的长度字段,必须和实际收到的字节数对得上:
```
UDP: len(6..8) == 数据报长度 - 8
TCP: len(0..4) == 帧体长度
两者: PeerManagerHeader.len 与实际载荷长度的关系
```
最后一条要分情况,因为**加密和压缩都不更新 `PeerManagerHeader.len`**:
| flags | 实际载荷长度 |
| --- | --- |
| 都没置位 | `== len`,精确相等 |
| 仅加密 | `== len`(XOR)或 `== len + 28`(AEAD 的 16 字节 tag + 12 字节 nonce) |
| 压缩 | `≤ len + 28`(压缩器只在能省下自己那 1 字节尾时才压) |
再加上 `padding == 0`、`packet_type ∈ 1..=21`、`forward_counter ≤ 8`、`reserved` 的取值约束,单帧大约值三十几个比特。分析器要求**攒够 2 帧**才把 `yes` 置真。
### 二、UDP 会话握手(`easyTierEvidenceConclusive`)
发起方发一个 16 字节的 `Syn`(`len == 8`,体是随机 u64 magic),对端回一个 16 字节的 `Sack`,**同一个 `conn_id`、同一个 magic**,方向相反。96 比特的巧合概率,一次就够。
单独一个 `Syn` 或 magic 对不上的 `Sack` 只算「见过」,不推进 `yes`。
### 三、对等握手(`easyTierEvidenceConclusive`)
默认(非 secure)模式下,连接建立后第一帧就是 `packet_type = 2` 的 protobuf `HandshakeRequest`,第一个字段是常量 `magic = 0xd1e1a5e1`。protobuf 编码后是固定的 6 字节:
```
08 e1 cb 86 8f 0d
```
在 TCP 上,它落在整条连接载荷的固定偏移 20..26:
```
0..4 TCPTunnelHeader.len
4..20 PeerManagerHeader(type=2, to_peer_id=0)
20..26 08 e1 cb 86 8f 0d
```
同一条消息还带着 `network_name`(字段 5,明文)和 32 字节的 `network_secret_digest`(字段 6)。**digest 宽度不对的握手上游会直接拒连**,所以分析器也把它当作否定条件,避免报一个根本连不起来的 mesh 名。
### 关于 secure mode
`--enable-secure-mode` 会把首帧换成 Noise XX 的 msg1(`packet_type = 13`)。Noise XX 的第一条消息此时还没有密钥,载荷是**明文**,所以 32 字节临时公钥后面的 protobuf 仍然能读出 `a_network_name`。
分析器对这段是「能读就读、读不出就不说」:protobuf 必须完整解析、version 合理、网络名是不含控制字符的 UTF-8,否则只置 `secure`,不填 `network_name`。secure mode 下没有 magic 可查,所以要靠第一档攒够 2 帧才判 `yes`。
## 属性
| 属性 | 类型 | 说明 |
| --- | --- | --- |
| `yes` | bool | 综合判定。一定存在 |
| `transport` | string | `udp` / `tcp`。一定存在 |
| `tx_pkt_cnt` / `rx_pkt_cnt` | int | 两个方向已解析的帧数。一定存在 |
| `msg_type` | uint8 | UDP 隧道消息类型,仅 UDP |
| `msg_type_name` | string | `syn` / `sack` / `data` / `fin` / `hole_punch` / `v4_hole_punch` / `v6_hole_punch` |
| `conn_id` | uint32 | UDP 会话 ID,仅 UDP |
| `packet_type` | uint8 | `PeerManagerHeader.packet_type` |
| `packet_type_name` | string | 上表的可读形式,如 `handshake`、`ping`、`noise_handshake_msg1` |
| `from_peer_id` / `to_peer_id` | uint32 | 对端 ID |
| `forward_counter` | uint8 | 中继跳数 |
| `payload_len` | uint32 | 头里声明的载荷长度 |
| `encrypted` / `compressed` / `latency_first` / `exit_node` / `no_proxy` | bool | `flags` 各位 |
| `magic_ok` | bool | 明文握手 magic 匹配。**粘性** |
| `secure` | bool | true = 见到 Noise 握手,false = 见到明文握手。**粘性** |
| `version` | uint64 | 握手里的协议版本。**粘性** |
| `network_name` | string | mesh 名。**粘性** |
标「粘性」的字段一旦写入就不再被后续帧覆盖:网络名只在握手里出现一次,一条按 mesh 名写的规则必须在握手滚过去之后还能用。其余字段描述的是**最近一帧**。整数字段的具体宽度不影响规则,expr 比较时会自动对齐。
`packet_type` 及其后的字段只在该帧带 `PeerManagerHeader` 时存在(UDP 的 `Data`、全部 TCP 帧)。`Syn`/`Sack`/打洞包没有。
> [!NOTE]
> 规则里要写 `easytier != nil && ...`。规则引擎只把已经发布过属性的分析器放进表达式环境(见 `ruleset/expr.go` 的 `streamInfoToExprEnv`),流上一帧都没解析出来时 `easytier` 是 `nil`,直接取 `easytier.yes` 会报错。map 里不存在的键读出来是 `nil`,比较结果为假,不报错:所以 `easytier.exit_node == true` 在 `Syn`/`Sack`/打洞包上只是**不匹配**,`exit_node` 要等一个带 `PeerManagerHeader` 的帧到达才有值;同理 `easytier.network_name == "..."` 在握手到达之前一直不匹配。
## 规则示例
拦截所有 EasyTier 对等连接:
```yaml
- name: block-easytier
action: block
log: true
expr: easytier != nil && easytier.yes
```
只拦某一个 mesh,其余放行(网络名明文可见,这是最精确的一档):
```yaml
- name: block-one-easytier-mesh
action: block
log: true
expr: easytier != nil && easytier.network_name == "office-mesh"
```
只拦出口节点,即把本网流量往公网转发的那些,通常是策略上真正在意的:
```yaml
- name: block-easytier-exit-node
action: block
log: true
expr: easytier != nil && easytier.yes && easytier.exit_node == true
```
打洞包不参与 `yes`,要单独点名(见下节的取舍说明):
```yaml
- name: log-easytier-hole-punch
log: true
expr: easytier != nil && easytier.msg_type_name == "hole_punch"
```
只观察不拦截,先看清网络里有什么:
```yaml
- name: log-easytier
log: true
expr: easytier != nil && easytier.yes
```
规则按顺序匹配,第一条带 action 的命中即生效。想同时用「只拦一个 mesh」和「拦全部」,具体的要写在前面,否则被兜底的那条挡住永远到不了。
配套的 WireGuard 传输要另外拦,走 `wireguard` 分析器。
## 检不到什么
- **`wg://` / `quic://` / `wss://` 传输**。线上分别就是 WireGuard、QUIC、TLS,交给对应分析器。`wss://` 的服务端用的是 `rcgen` 现签的自签证书,CN 为 `localhost`,这本身是个可用的特征,但它属于 `tls` 的范畴。
- **`ws://` 帧内容**。HTTP Upgrade 握手 `http` 能看到,之后的 WebSocket 二进制帧里才是 `PeerManagerHeader`(无长度前缀,`ZCPacketType::DummyTunnel`),本分析器不做 WebSocket 拆帧。
- **中途接入的流**。TCP 侧遇到 `skip != 0`(重组有空洞)会直接放弃,因为帧边界无从推断。
- **只发打洞包的流**。打洞探测是往可能根本没跑 EasyTier 的地址上喷的随机数据,除了长度没有任何结构(连通性路径 16 字节,UDP 会话层 32 字节,两个值都认),把它算作确证会在正常网络里产生误报,所以它只填 `msg_type_name`、不推进 `yes`。要抓就按上面的例子点名。
## 误报边界
- UDP 侧连续 4 个无法解析的数据报就放弃该流(和 `wireguard` 分析器一致);TCP 侧一旦帧结构对不上立刻结束。
- EasyTier 把 STUN 复用在同一个 UDP socket 上,并且在自己的帧解析**之前**先分流 STUN。分析器照做:识别出 STUN(`data[4..8] == 21 12 A4 42` 且 `data[0] & 0xC0 == 0`)的数据报既不算证据也不算错误,只计入总包数,免得一串 STUN 把流提前判死。
- 任一传输超过 256 个包仍未定性就结束该流(和 `openvpn` 分析器一致),避免长流一直占着分析器状态。
- `Limit()` 返回 0。这是个定长帧协议,按字节数截断会让帧读取失步;上面的包数上限替代它约束开销。
## 复现
字节级的边界条件都在 `analyzer/udp/easytier_test.go` 里,包括 magic 的编码、两个长度字段的关系、AEAD 尾、压缩、`reserved` 的取值规则、帧切在长度前缀中间的重组、逐字节投递、STUN 复用,以及上面提到的 `fet` 和 `wireguard` 对照用例:
```bash
go test ./analyzer/udp -run EasyTier -v
```
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

-67
View File
@@ -1,67 +0,0 @@
# Minecraft 分析器
识别 Minecraft Java 版客户端在连接建立后发出的第一个数据包(Handshake,packet ID `0x00`),从中取出客户端请求的服务器地址、协议版本和后续状态。基岩版走 UDP/RakNet,不在本分析器范围内。
分析器名是 `minecraft`。和其他分析器一样,**只有当某条规则的表达式里出现了 `minecraft` 这个标识符时,它才会被启用**(见 `ruleset/expr.go` 的依赖收集逻辑),不写规则就不会有任何开销。
## 属性
| 属性 | 类型 | 说明 |
| --- | --- | --- |
| `yes` | bool | 确认是 Minecraft 握手 |
| `candidate` | bool | 数据还不完整,暂时无法判定,仍在等待后续分段 |
| `protocol` | int | 协议版本号,**不是游戏版本号**,对照表见 [minecraft.wiki](https://minecraft.wiki/w/Protocol_version)(例如 767 对应 1.21) |
| `server_addr` | string | 客户端在握手包里填写的服务器地址 |
| `server_port` | uint16 | 客户端填写的端口 |
| `next_state` | int | 1 = status,2 = login,3 = transfer |
| `next_state_name` | string | 上面的可读形式:`status` / `login` / `transfer` |
| `packet_length` | int | 握手包声明的长度(不含长度字段自身) |
`yes` 和 `candidate` 一定存在,其余字段只在 `yes` 为 true 时出现。
## 规则示例
只拦截真正进服的连接,放过服务器列表里的 ping:
```yaml
- name: block-minecraft-login
action: block
log: true
expr: minecraft != nil && minecraft.yes && minecraft.next_state_name == "login"
```
只允许连接指定的服务器,其余一律拦截:
```yaml
- name: minecraft-allowlist
action: block
expr: >
minecraft != nil && minecraft.yes &&
!hasPrefix(minecraft.server_addr, "mc.example.com")
```
按协议版本卡老客户端:
```yaml
- name: block-old-minecraft-clients
action: block
expr: minecraft != nil && minecraft.yes && minecraft.protocol < 763
```
纯记录不拦截,先摸清楚流量情况:
```yaml
- name: log-minecraft
log: true
expr: minecraft != nil && minecraft.yes
```
## 写规则时要注意的几点
**`server_addr` 不一定是干净的域名。** 握手包里的地址字段被生态里几个东西复用了,客户端可能在真实域名后面追加以 `\0` 分隔的内容:Forge/FML 客户端会加 `\0FML\0`、`\0FML2\0`、`\0FML3\0` 之类的标记,BungeeCord 和 Velocity 的 IP 转发会把玩家真实 IP 和 UUID 拼在后面。分析器保存的是原始字节,不做任何清洗。所以不要用 `==` 精确比较,用 `hasPrefix(minecraft.server_addr, ...)`,或者先 `split(minecraft.server_addr, "\x00")[0]` 再比。
**这个字段是客户端自己填的,不可信。** 它反映客户端想连哪个域名(对 SRV 记录的情况是解析前的原始主机名),不是它实际连到的 IP。要认真限制目标,得配合 `ip.dst` 一起写。
**判定发生在第一个数据包。** 握手之后的登录、加密、游戏数据都不会再看。`Limit()` 是 2053 字节,正常握手包只有几十字节,绰绰有余;超过这个量还没解析成功的连接会被判为非 Minecraft。
**握手包被拆到多个 TCP 分段时也能正确解析。** 分析器内部有缓冲区,数据不够时返回 `candidate = true` 并继续等待,不会像只看单个分段的分析器那样被切包绕过。相关背景见 [TCP 窗口操纵规避](tcp-window-evasion.zh.md)。
-93
View File
@@ -1,93 +0,0 @@
# 拦截 TLS、放行 RDP
## 问题
RDP 不是一连上就开始 TLS:客户端先在 TPKT 里发一个 X.224 Connection Request,服务端回一个 Connection Confirm,双方借此协商安全协议([MS-RDPBCGR] 1.3.1.1),协商完才在同一条连接上开始 TLS 握手。
`tls` 分析器以前要求流的第一个字节就是 TLS record(`0x16`),碰到 RDP 开头的 `03 00` 直接放弃。结果是:
- RDP 里的 TLS 对 `tls` 规则完全不可见,没法按 RDP 单独写规则;
- 反过来,任何流量只要在 TLS 前面垫一段假的 X.224 协商,就能绕过"拦截所有 TLS"的规则。
## 做法
`tls` 分析器现在把 RDP 协商当作 hello 之前可选的一段前导来解析:
- 第一个字节是 `0x16`:和以前一样,直接当 TLS 解析。
- 第一个字节是 `0x03`:按 TPKT + X.224 解析。客户端方向要求是 Connection Request,服务端方向要求是 Connection Confirm;长度、class、协商结构任何一处不对都视为非法,分析器取消。
- 前导解析完后照常解析 ClientHello / ServerHello,record 分片重组等逻辑完全一样。
协商一解析出来就上报,不用等 TLS:
| 属性 | 类型 | 说明 |
| --- | --- | --- |
| `tls.rdp_req.cookie` | string | Connection Request 里的 cookie,去掉 `Cookie: ` 前缀和 CRLF,如 `mstshash=alice`;没有 cookie 时不存在 |
| `tls.rdp_req.protocols` | int | RDP_NEG_REQ 里客户端请求的协议位图;没有 RDP_NEG_REQ 时为 0 |
| `tls.rdp_resp.protocol` | int | RDP_NEG_RSP 里服务端选定的协议;没有 RDP_NEG_RSP 时为 0 |
| `tls.rdp_resp.failure` | int | 服务端回的是 RDP_NEG_FAILURE 时的失败码(此时没有 `protocol`) |
协议取值:
| 值 | 名称 | 是否走 TLS |
| --- | --- | --- |
| 0 | PROTOCOL_RDP(标准 RDP 安全,RC4) | 否 |
| 1 | PROTOCOL_SSL | 是 |
| 2 | PROTOCOL_HYBRID(CredSSP / NLA) | 是 |
| 4 | PROTOCOL_RDSTLS | 是 |
| 8 | PROTOCOL_HYBRID_EX | 是 |
| 16 | PROTOCOL_RDSAAD | 是 |
## 规则
规则按顺序匹配、第一条命中的生效,所以放行 RDP 的规则要写在拦截 TLS 的前面:
```yaml
- name: allow-rdp-tls
action: allow
log: true
expr: >
tls != nil && tls.req != nil &&
tls.rdp_req != nil && tls.rdp_resp != nil &&
tls.rdp_resp.protocol in [1, 2, 4, 8, 16]
- name: block-tls
action: block
log: true
expr: tls != nil && tls.req != nil
```
放行条件同时要求:
- **`tls.req != nil`**:ClientHello 已经完整解析。只看到协商就放行的话,协商之后跟什么都行。
- **`tls.rdp_resp != nil` 且 `protocol` 是走 TLS 的协议**:真实的 RDP 只有在服务端选了 TLS 类协议之后才会发 ClientHello。服务端选了 PROTOCOL_RDP 或者回了协商失败,后面却出现 TLS,说明是伪造的,交给 `block-tls` 拦掉。
正常 RDP 的时序是 CR → CC → ClientHello,ClientHello 到的时候 `rdp_resp` 早已上报,不存在规则先看到 `req` 再看到 `rdp_resp` 的竞争。
标准 RDP 安全(`protocol == 0`)的连接里根本没有 TLS,两条规则都不会命中,按默认放行。
### 收紧放行条件
上面的规则只验证"协商格式正确、协商结果和后续 TLS 一致"。客户端和服务端串通的话,完全可以按规范发一套协商再跑任意 TLS,所以按需叠加:
```yaml
# 只放行标准端口
expr: ... && port.dst == 3389
# 只放行指定服务器
expr: ... && ip.dst in ["10.0.0.10", "10.0.0.11"]
# 要求客户端带 mstshash cookie(mstsc 通常会带,但不保证,先观察日志再启用)
expr: ... && tls.rdp_req.cookie != nil && tls.rdp_req.cookie startsWith "mstshash="
```
## 行为变化
**以前按 `tls != nil && tls.req != nil` 写的规则现在会命中 RDP。** 以前 RDP 里的 TLS 看不到,所以不受影响;现在看得到了。不想拦 RDP 的话,必须像上面那样在前面加一条放行规则。
## 局限
**只认 RDP 这一种前导。** 其他协议里的 STARTTLS(SMTP、IMAP、XMPP 等)和任何非 TLS、非 TPKT 开头的前缀仍然会让 `tls` 分析器直接放弃。要做到"拦截所有 TLS",还需要配合 `fet` 等不依赖协议开头的检测。
**RD Gateway 不在此列。** 经由 RD 网关的 RDP 跑在 443 端口的 HTTPS 上,开头就是普通 TLS,没有 X.224 协商,只能按 SNI / IP 放行。
**RDP 的 UDP 传输不在此列。** RDP-UDP 用的是 DTLS,走 UDP,与 `tls` 分析器无关。
-112
View File
@@ -1,112 +0,0 @@
# TCP 窗口操纵规避与检测
## 规避手法
服务端在本机加一条这样的规则,就能让不少 DPI 失效:
```
table inet filter {
chain output {
type filter hook output priority filter;
oifname eth0 tcp sport <本地端口> tcp flags & (syn | ack) == (syn | ack) tcp window set 1 accept
}
}
```
它只匹配服务端发出的 SYN-ACK,把里面通告的接收窗口改成 1。按 RFC 7323,window scale 选项不作用于 SYN/SYN-ACK 自身的窗口字段,所以这里的 `1` 就是字面意义的 1 字节,不会被 wscale 放大。
握手完成后,客户端认为对端只能收 1 个字节,第一个数据包就只带 1 字节负载。服务端内核回 ACK 时通告的是真实窗口(那条规则不匹配纯 ACK),客户端随即把剩下的数据正常发出。净效果是首包被切成「1 字节 + 剩余部分」两段,代价只有一个 RTT,之后传输速度不受影响。
对于**只看第一个 TCP 分段就下结论**的分析器,这一刀就够了:拿到 1 个字节,任何需要 3 字节、6 字节的特征都匹配不上,于是误判放行。更糟的是 OpenGFW 在所有分析器都判完且无人拦截时会下发 `ACCEPT_STREAM`,通过 connmark 让这条连接的后续包在内核里直接 accept,根本不再进入用户态——一次误判就是永久放行。
## 第一层防御:分析器攒够数据再判决
`fet`(全加密流量检测)原先在第一次 `Feed` 就返回 `done = true`,是这个手法的主要受害者。现在它会缓冲到至少 `fetMinDataLen`(16)字节再分类。
为了不增加正常流量的开销,实现上做了两件事:
- **快路径**:首个分段本身就够长时(正常流量的首包都是几百字节),直接在原始切片上分类,不拷贝、不分配,开销与改动前完全一致。只有小于 16 字节的首段才会走缓冲。
- **段数上限**:最多等 `fetMaxSegments`(4)个分段。否则一个只发 1 字节然后装死的对端,就能让流一直留在活跃状态、拿不到 connmark 快速通道,直到 TCP 超时(默认 10 分钟)为止——那会变成一个可被主动利用的资源消耗面。
`http`、`tls`、`minecraft` 等分析器本来就用 `utils.ByteBuffer` 做增量解析,不受这个手法影响。注意这只针对 TCP 分段;TLS 自己记录层上的分片是另一回事,见 [TLS record 分片规避与检测](tls-record-fragmentation.zh.md)。
## 第二层防御:把窗口改写本身当指纹
分析器只能看到重组后的载荷,拿不到任何 TCP 头字段。所以这一层做在引擎里:`engine/tcp.go` 的 `Accept` 每个包都会被调用,其中检查 SYN-ACK 的窗口值,异常时把握手元数据挂到 `tcpmeta` 属性下,供规则引擎使用。
| 属性 | 类型 | 说明 |
| --- | --- | --- |
| `tcpmeta.synack_window` | int | SYN-ACK 通告的接收窗口(未经 wscale 放大的字面值) |
| `tcpmeta.synack_wscale` | int | SYN-ACK 协商的 window scale 因子,没有该选项时不存在 |
| `tcpmeta.first_seg_len` | int | 客户端第一个数据分段的字节数 |
`tcpmeta` 本身只在有东西值得报告时才会创建:SYN-ACK 窗口 ≤ `tcpSynAckWindowMax`(4096),或者客户端首段 ≤ `tcpFirstSegLenMax`(64)字节。正常连接一次分配都不会发生,代价是**规则里看不到超过门槛的值**——门槛只是为了省开销,真正的判定阈值请在规则里写。
> [!IMPORTANT]
> **每个字段都要单独判空**,不能只判 `tcpmeta != nil`。这两个信号是独立触发的:一条 SYN-ACK 窗口完全正常、只是首包偏短的连接(SSH 的客户端版本标识只有二十几字节,就是典型)会创建 `tcpmeta`,但里面没有 `synack_window`。此时 `tcpmeta != nil && tcpmeta.synack_window <= 16` 会在运行期抛 `invalid operation: <nil> <= int`。这类错误不会让进程崩溃,`ruleset/expr.go` 里的 `Match` 记录一条 `MatchError` 后就跳到下一条规则——也就是说**这条检测会静默失效**,日志里不翻出来根本发现不了。
>
> 只要 OpenGFW 看得到 SYN-ACK,`tcpmeta` 里就一定带 `synack_window`(`tcpMeta()` 在创建时会把握手字段一并填进去)。但单向可见的部署看不到返回方向,所以判空仍然是必须的。`synack_wscale` 则是本来就可能不存在——对端没协商窗口缩放时就没有这个字段。
`synack_window` 看的是**手法**,`first_seg_len` 看的是**结果**。前者要求 OpenGFW 能看到返回方向的流量,后者只需要看到客户端方向;前者能被改用窗口 40 之类的取值绕开,后者不管对方怎么调窗口都会留下痕迹。两个一起用最稳。
### 规则示例
窗口小到不可能是真实协议栈:
```yaml
- name: tcp-tiny-synack-window
action: block
log: true
expr: >
tcpmeta != nil && tcpmeta.synack_window != nil &&
tcpmeta.synack_window <= 16
```
更锋利的判据是**自相矛盾的握手**。接收缓冲区真的只有几个字节的协议栈,不会同时去协商 128 倍的窗口缩放;这两者同时出现,基本只可能是字段在传输途中被改写:
```yaml
- name: tcp-window-rewritten
action: block
log: true
expr: >
tcpmeta != nil && tcpmeta.synack_wscale != nil &&
tcpmeta.synack_window <= 16
```
直接拦被切开的首包,不管对方是怎么切的:
```yaml
- name: tcp-split-first-segment
action: block
log: true
expr: >
tcpmeta != nil && tcpmeta.first_seg_len != nil &&
tcpmeta.first_seg_len <= 8
```
配合 `fet` 一起用,把「全加密流量」和「还试图掩盖自己」两个信号叠加。因为有第二个条件兜底,首包长度的阈值可以放宽到 32:
```yaml
- name: evasive-encrypted-traffic
action: block
log: true
expr: >
tcpmeta != nil && tcpmeta.first_seg_len != nil &&
tcpmeta.first_seg_len <= 32 && fet != nil && fet.yes
```
建议先用 `log: true` 不带 `action` 跑一段时间,看清楚基线再决定要不要拦。
## 局限
**判定要等到第一个数据包。** `ruleset.Match` 只在 `ReassembledSG` 里调用,也就是必须有载荷。握手异常但从不发数据的连接不会被匹配到——不过那种连接本来也没什么可拦的。
**阈值可以被绕。** 对方把窗口改成 40 或 100,既躲开 `<= 16`,又照样能把 ClientHello 切开。所以第二层是辅助,真正根治的是第一层的缓冲——它消灭的是整类切包规避,而不是某一种参数取值。
**小窗口不等于恶意。** 部分嵌入式协议栈的初始窗口确实只有几百到一两千字节,某些 CDN 和负载均衡设备也会改写窗口。阈值别设太高,`<= 16` 是安全的,上到 1024 就要先看基线。`synack_wscale` 那条组合判据的误报率要低得多。
**`first_seg_len` 的阈值尤其要保守。** 有不少协议的首包本来就很短:Minecraft 握手包只有二十几字节,SSH 客户端版本标识约二十到四十字节,还有各种短命令的二进制 RPC。把阈值设到 16 以上会误伤这些正常流量——特别是如果你同时在用 Minecraft 分析器,`<= 32` 会命中每一条 Minecraft 连接。单独拿它当拦截依据时用 `<= 8`;想放宽到 32,就必须像上面那样再叠加一个条件(`fet.yes`、目标端口、或者 `synack_window`)。
**首包短不代表后面还有数据。** 目前只记录第一个分段的长度,不区分「被切开的大请求」和「本来就只有几字节的完整请求」。前者才是规避,后者是正常的。这也是建议把阈值压到 8 或者叠加条件的原因之一。
**只改 SYN-ACK 会留下窗口跃变。** 服务端内核对改写并不知情,后续纯 ACK 通告的还是真实窗口,线上必然出现 `1 → 64240` 这种一跳到顶的序列,而真实协议栈的接收窗口是随缓冲区占用平滑变化的。这个指纹更难规避,但要观察到它就必须持续跟踪 server→client 方向的窗口序列,也就意味着不能再提前下发 `ACCEPT_STREAM`——那会让全局最大的性能优化失效。目前没有实现,需要时建议做成只对已标记可疑的连接开启的定向跟踪。
-70
View File
@@ -1,70 +0,0 @@
# TLS record 分片规避与检测
## 规避手法
TLS 允许一个握手消息跨任意多个 record 传输(RFC 8446 §5.1)。客户端把 ClientHello 拆成很多个小 record,服务端照常把它们拼回去完成握手;但只看第一个 record 的 DPI 就拿不到完整的 ClientHello,SNI 也就看不到了。Xray 的 `fragment`(`packets: "tlshello"`)以及各类"TLS 分片"工具用的都是这个手法。
它和 [TCP 窗口操纵](tcp-window-evasion.zh.md) 是两层不同的东西:TCP 分段切的是字节流,OpenGFW 的 TCP 重组会把它抹平;record 分片切的是 TLS 自己的记录层,重组后的字节流里每一片前面都还带着 5 字节的 record 头。
`tls` 分析器以前默认 ClientHello 只在一个 record 里,读完第一个 record 头就直接按握手长度切数据,碰到分片会:
- 第一个 record 少于 4 字节握手数据时,把下一个 record 的头误当成握手长度(例如 `16 03 01` 被读成 0x160301),一直等到 8192 字节的上限;
- 其他情况下,后续 record 的头混进消息体导致解析失败,分析器取消。
两种情况都不产出任何属性,所有基于 `tls.req.sni` 的规则都会被绕过。
## 第一层防御:在 record 层重组
现在分析器先剥掉 record 层,把各个 record 的内容拼回完整的握手消息再解析,所以无论切在哪里、切成多少片,`tls.req` 的解析结果都和不分片时完全一样。ServerHello 方向也做了同样的处理。
- 握手消息中间夹了其他类型的 record(alert、change_cipher_spec 等)会被视为非法,分析器取消。
- record 长度超过 2^14 视为非法。
- 空 record 按规范不允许出现在握手中,但这里不拒绝,只是计数——拒绝的话反而给了对方一个让分析中止的开关。
- 常见情况下整个 ClientHello 在一个 record 里,此时直接引用原始缓冲区,不做额外拷贝。
## 第二层防御:把分片本身当指纹
| 属性 | 类型 | 说明 |
| --- | --- | --- |
| `tls.req_records` | int | ClientHello 跨了几个 record |
| `tls.resp_records` | int | ServerHello 跨了几个 record |
主流浏览器和 TLS 库都把 ClientHello 放在一个 record 里,即使是带后量子密钥交换、将近 2KB 的 ClientHello 也远小于 16KB 的 record 上限。所以 `req_records > 1` 本身就是很强的规避信号,误报率很低。
这个计数放在 `req` 外面,是为了保证 `tls.req != nil` 仍然表示"ClientHello 已完整解析",现有规则的语义不变。
### 提前上报
分片一出现(第二个 record 开始)就会上报一次 `req_records`,不用等消息收完。原因是分析器有 8192 字节的 `Limit()`:切成每片 1 字节时,每个字节要额外带 5 字节的 record 头,一个 1.8KB 的 ClientHello 会膨胀到 11KB 左右,永远收不完。所以规则不能依赖 SNI 才动手。
之后的 record 不会逐个上报(否则每片都要跑一次规则匹配);消息收完时,或者分析器因为超限被关闭时,再上报最终的计数。所以规则里看到的值可能是 2,也可能是最终值——写阈值时按 `> 1` 来写。
### 规则示例
直接拦截分片的 ClientHello:
```yaml
- name: tls-record-fragmentation
action: block
log: true
expr: tls != nil && tls.req_records != nil && tls.req_records > 1
```
> [!IMPORTANT]
> 要判断 `tls.req_records != nil`,不能只判 `tls != nil`:只看到服务端方向时,`tls` 里可能只有 `resp` 相关的字段。
按 SNI 拦截的规则不需要任何改动,分片后照样能匹配:
```yaml
- name: block-example
action: block
expr: tls != nil && tls.req != nil && tls.req.sni endsWith "example.com"
```
## 局限
**只统计 hello 消息本身。** ClientHello 收完就停止计数,后面的 record 不影响 `req_records`。
**超长的分片拿不到 SNI。** 分片把 ClientHello 撑过 8192 字节时,`tls.req` 不会出现,只能靠 `req_records` 拦截。需要同时拿到 SNI 的话可以调大 `Limit()`,代价是每条 TLS 连接的缓冲上限相应变大。
**QUIC 不在此列。** QUIC 的 ClientHello 在 CRYPTO 帧里,走的是 `quic` 分析器,与这里无关。
-125
View File
@@ -1,125 +0,0 @@
package engine
import (
"context"
"runtime"
"github.com/apernet/OpenGFW/io"
"github.com/apernet/OpenGFW/ruleset"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
)
var _ Engine = (*engine)(nil)
type engine struct {
logger Logger
io io.PacketIO
workers []*worker
}
func NewEngine(config Config) (Engine, error) {
workerCount := config.Workers
if workerCount <= 0 {
workerCount = runtime.NumCPU()
}
var err error
workers := make([]*worker, workerCount)
for i := range workers {
workers[i], err = newWorker(workerConfig{
ID: i,
ChanSize: config.WorkerQueueSize,
Logger: config.Logger,
Ruleset: config.Ruleset,
TCPMaxBufferedPagesTotal: config.WorkerTCPMaxBufferedPagesTotal,
TCPMaxBufferedPagesPerConn: config.WorkerTCPMaxBufferedPagesPerConn,
TCPTimeout: config.WorkerTCPTimeout,
UDPMaxStreams: config.WorkerUDPMaxStreams,
})
if err != nil {
return nil, err
}
}
return &engine{
logger: config.Logger,
io: config.IO,
workers: workers,
}, nil
}
func (e *engine) UpdateRuleset(r ruleset.Ruleset) error {
for _, w := range e.workers {
if err := w.UpdateRuleset(r); err != nil {
return err
}
}
return nil
}
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)
e.io.SetCancelFunc(ioCancel)
defer ioCancel() // Stop IO
// Start workers
for _, w := range e.workers {
go w.Run(workerCtx)
}
// Register IO callback
errChan := make(chan error, 1)
err := e.io.Register(ioCtx, func(p io.Packet, err error) bool {
if err != nil {
errChan <- err
return false
}
return e.dispatch(p)
})
if err != nil {
return err
}
// Block until IO errors or context is cancelled
select {
case err := <-errChan:
return err
case <-ctx.Done():
return nil
case <-ioCtx.Done():
return nil
}
}
// dispatch dispatches a packet to a worker.
func (e *engine) dispatch(p io.Packet) bool {
data := p.Data()
ipVersion := data[0] >> 4
var layerType gopacket.LayerType
if ipVersion == 4 {
layerType = layers.LayerTypeIPv4
} else if ipVersion == 6 {
layerType = layers.LayerTypeIPv6
} else {
// Unsupported network layer
_ = 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))
e.workers[index].Feed(&workerPacket{
StreamID: p.StreamID(),
Packet: packet,
SetVerdict: func(v io.Verdict, b []byte) error {
return e.io.SetVerdict(p, v, b)
},
})
return true
}
-52
View File
@@ -1,52 +0,0 @@
package engine
import (
"context"
"time"
"github.com/apernet/OpenGFW/io"
"github.com/apernet/OpenGFW/ruleset"
)
// Engine is the main engine for OpenGFW.
type Engine interface {
// UpdateRuleset updates the ruleset.
UpdateRuleset(ruleset.Ruleset) error
// Run runs the engine, until an error occurs or the context is cancelled.
Run(context.Context) error
}
// Config is the configuration for the engine.
type Config struct {
Logger Logger
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
}
// Logger is the combined logging interface for the engine, workers and analyzers.
type Logger interface {
WorkerStart(id int)
WorkerStop(id int)
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)
ModifyError(info ruleset.StreamInfo, err error)
AnalyzerDebugf(streamID int64, name string, format string, args ...interface{})
AnalyzerInfof(streamID int64, name string, format string, args ...interface{})
AnalyzerErrorf(streamID int64, name string, format string, args ...interface{})
}
-199
View File
@@ -1,199 +0,0 @@
package engine
import (
"net"
"testing"
"time"
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/analyzer/tcp"
"github.com/apernet/OpenGFW/analyzer/udp"
"github.com/apernet/OpenGFW/io"
"github.com/apernet/OpenGFW/ruleset"
"github.com/apernet/OpenGFW/ruleset/builtins/geo"
"github.com/bwmarrin/snowflake"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/reassembly"
)
type nullEngineLogger struct{}
func (nullEngineLogger) WorkerStart(int) {}
func (nullEngineLogger) WorkerStop(int) {}
func (nullEngineLogger) TCPStreamNew(int, ruleset.StreamInfo) {}
func (nullEngineLogger) TCPStreamPropUpdate(ruleset.StreamInfo, bool) {}
func (nullEngineLogger) TCPStreamAction(ruleset.StreamInfo, ruleset.Action, bool) {}
func (nullEngineLogger) TCPFlush(int, int, int) {}
func (nullEngineLogger) UDPStreamNew(int, ruleset.StreamInfo) {}
func (nullEngineLogger) UDPStreamPropUpdate(ruleset.StreamInfo, bool) {}
func (nullEngineLogger) UDPStreamAction(ruleset.StreamInfo, ruleset.Action, bool) {}
func (nullEngineLogger) ModifyError(ruleset.StreamInfo, error) {}
func (nullEngineLogger) AnalyzerDebugf(int64, string, string, ...interface{}) {}
func (nullEngineLogger) AnalyzerInfof(int64, string, string, ...interface{}) {}
func (nullEngineLogger) AnalyzerErrorf(int64, string, string, ...interface{}) {}
type nullRulesetLogger struct{}
func (nullRulesetLogger) Log(ruleset.StreamInfo, string) {}
func (nullRulesetLogger) MatchError(ruleset.StreamInfo, string, error) {}
func testRuleset(t *testing.T, rules ...ruleset.ExprRule) ruleset.Ruleset {
t.Helper()
ans := []analyzer.Analyzer{
&tcp.FETAnalyzer{}, &tcp.HTTPAnalyzer{}, &tcp.TLSAnalyzer{},
&udp.QUICAnalyzer{}, &udp.WireGuardAnalyzer{},
}
rs, err := ruleset.CompileExprRules(rules, ans, nil, &ruleset.BuiltinConfig{Logger: nullRulesetLogger{}, GeoMatcher: geo.NewGeoMatcher("", "")})
if err != nil {
t.Fatal(err)
}
return rs
}
// capturingFactory 记下工厂创建的每条 tcpStream,让测试能检查判定之后它的状态
type capturingFactory struct {
inner *tcpStreamFactory
streams []*tcpStream
}
func (f *capturingFactory) New(ipFlow, tcpFlow gopacket.Flow, tcp *layers.TCP, ac reassembly.AssemblerContext) reassembly.Stream {
s := f.inner.New(ipFlow, tcpFlow, tcp, ac).(*tcpStream)
f.streams = append(f.streams, s)
return s
}
func newTestAssembler(t *testing.T, rs ruleset.Ruleset) (*reassembly.Assembler, *capturingFactory) {
t.Helper()
node, err := snowflake.NewNode(1)
if err != nil {
t.Fatal(err)
}
cf := &capturingFactory{inner: &tcpStreamFactory{WorkerID: 1, Logger: nullEngineLogger{}, Node: node, Ruleset: rs}}
asm := reassembly.NewAssembler(reassembly.NewStreamPool(cf))
asm.MaxBufferedPagesTotal = defaultTCPMaxBufferedPagesTotal
asm.MaxBufferedPagesPerConnection = defaultTCPMaxBufferedPagesPerConnection
return asm, cf
}
var testIPFlow = gopacket.NewFlow(layers.EndpointIPv4, net.IPv4(10, 0, 0, 1).To4(), net.IPv4(203, 0, 113, 1).To4())
func tcpSegment(seq uint32, syn bool, payload []byte) *layers.TCP {
seg := &layers.TCP{SrcPort: 40000, DstPort: 80, Seq: seq, SYN: syn, ACK: !syn, PSH: len(payload) > 0, Window: 65535}
seg.Payload = payload
return seg
}
func feedTCP(asm *reassembly.Assembler, seg *layers.TCP, ts time.Time) tcpVerdict {
ctx := &tcpContext{PacketMetadata: &gopacket.PacketMetadata{CaptureInfo: gopacket.CaptureInfo{Timestamp: ts}}, Verdict: tcpVerdictAccept}
asm.AssembleWithContext(testIPFlow, seg, ctx)
return ctx.Verdict
}
var httpRequest = []byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
func TestTCPStreamReleasesStateAfterVerdict(t *testing.T) {
rs := testRuleset(t, ruleset.ExprRule{Name: "block-http", Action: "block", Expr: "http?.req != nil"})
asm, cf := newTestAssembler(t, rs)
ts := time.Now()
feedTCP(asm, tcpSegment(1000, true, nil), ts)
if v := feedTCP(asm, tcpSegment(1001, false, httpRequest), ts); v != tcpVerdictDropStream {
t.Fatalf("verdict = %v, want drop stream", v)
}
if len(cf.streams) != 1 {
t.Fatalf("streams created = %d, want 1", len(cf.streams))
}
s := cf.streams[0]
if !s.released || s.activeEntries != nil || s.info.Props != nil || s.ruleset != nil {
t.Fatalf("stream still holds state after verdict: released=%v entries=%d props=%v ruleset=%v", s.released, len(s.activeEntries), s.info.Props, s.ruleset != nil)
}
// 后续报文沿用判定,且不能碰已经释放的状态
if v := feedTCP(asm, tcpSegment(1001+uint32(len(httpRequest)), false, []byte("more")), ts); v != tcpVerdictDropStream {
t.Fatalf("verdict on a later segment = %v, want drop stream", v)
}
}
func TestTCPStreamReleasedWhenAllAnalyzersFinish(t *testing.T) {
// 规则只引用 tls;一个 HTTP 请求让 tls 分析器直接放弃,没有裁决,引擎应当发 AcceptStream 并释放
rs := testRuleset(t, ruleset.ExprRule{Name: "block-tls", Action: "block", Expr: "tls?.req != nil"})
asm, cf := newTestAssembler(t, rs)
ts := time.Now()
feedTCP(asm, tcpSegment(1000, true, nil), ts)
if v := feedTCP(asm, tcpSegment(1001, false, httpRequest), ts); v != tcpVerdictAcceptStream {
t.Fatalf("verdict = %v, want accept stream", v)
}
if s := cf.streams[0]; !s.released || s.info.Props != nil {
t.Fatalf("stream not released after all analyzers finished: released=%v props=%v", s.released, s.info.Props)
}
}
// 判定之前缓冲的乱序页会在超时 flush 时被补送到 ReassembledSG,那时流已经释放,不能崩
func TestTCPStreamSurvivesFlushDeliveringBufferedPageAfterRelease(t *testing.T) {
rs := testRuleset(t, ruleset.ExprRule{Name: "block-http", Action: "block", Expr: "http?.req != nil"})
asm, cf := newTestAssembler(t, rs)
ts := time.Now()
feedTCP(asm, tcpSegment(1000, true, nil), ts)
// 乱序段:序号跳过一大截,被缓冲起来等空洞补齐
feedTCP(asm, tcpSegment(9000, false, []byte("out of order")), ts)
if v := feedTCP(asm, tcpSegment(1001, false, httpRequest), ts); v != tcpVerdictDropStream {
t.Fatalf("verdict = %v, want drop stream", v)
}
s := cf.streams[0]
if !s.released {
t.Fatal("stream not released after verdict")
}
// 超时 flush 会带着空洞把那一页送进来
flushed, closed := asm.FlushCloseOlderThan(ts.Add(time.Hour))
if flushed == 0 || closed == 0 {
t.Fatalf("flush did nothing: flushed=%d closed=%d", flushed, closed)
}
if s.info.Props != nil || s.activeEntries != nil {
t.Fatal("flush resurrected released state")
}
}
func TestUDPStreamReleasesStateAfterVerdict(t *testing.T) {
rs := testRuleset(t, ruleset.ExprRule{Name: "allow-443", Action: "allow", Expr: "port.dst == 443"})
w, err := newWorker(workerConfig{ID: 1, Logger: nullEngineLogger{}, Ruleset: rs})
if err != nil {
t.Fatal(err)
}
pkt := &layers.UDP{SrcPort: 40000, DstPort: 443}
pkt.Payload = []byte("hello")
if v, _ := w.handleUDP(7, testIPFlow, pkt); v != io.VerdictAcceptStream {
t.Fatalf("verdict = %v, want accept stream", v)
}
value, ok := w.udpStreamManager.streams.Get(7)
if !ok {
t.Fatal("stream missing from the LRU")
}
if s := value.Stream; s.activeEntries != nil || s.info.Props != nil || s.ruleset != nil {
t.Fatalf("udp stream still holds state after verdict: entries=%d props=%v", len(s.activeEntries), s.info.Props)
}
if v, _ := w.handleUDP(7, testIPFlow, pkt); v != io.VerdictAcceptStream {
t.Fatalf("verdict on a later packet = %v, want accept stream", v)
}
}
func TestTCPFlushIntervalFollowsTimeout(t *testing.T) {
cases := []struct {
timeout, want time.Duration
}{
{30 * time.Second, 15 * time.Second},
{10 * time.Minute, time.Minute},
{time.Second, time.Second},
{0, time.Second},
}
for _, c := range cases {
if got := tcpFlushIntervalFor(c.timeout); got != c.want {
t.Errorf("timeout %v: interval = %v, want %v", c.timeout, got, c.want)
}
}
}
-343
View File
@@ -1,343 +0,0 @@
package engine
import (
"net"
"sync"
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/io"
"github.com/apernet/OpenGFW/ruleset"
"github.com/bwmarrin/snowflake"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/reassembly"
)
// tcpVerdict is a subset of io.Verdict for TCP streams.
// We don't allow modifying or dropping a single packet
// for TCP streams for now, as it doesn't make much sense.
type tcpVerdict io.Verdict
const (
tcpVerdictAccept = tcpVerdict(io.VerdictAccept)
tcpVerdictAcceptStream = tcpVerdict(io.VerdictAcceptStream)
tcpVerdictDropStream = tcpVerdict(io.VerdictDropStream)
)
const (
// tcpMetaPropKey is the namespace under which the engine exposes TCP
// handshake metadata to the ruleset. Analyzers only ever see reassembled
// payload, so header-level signals have to come from here. It must not
// collide with the name of any analyzer.
tcpMetaPropKey = "tcpmeta"
// tcpSynAckWindowMax is the SYN-ACK receive window at or below which we
// record that metadata. Real stacks advertise tens of thousands of bytes;
// a tiny window forces the peer to split its first request across segments,
// which defeats analyzers that classify on the first segment alone. The
// threshold is deliberately loose so that normal connections allocate
// nothing - the ruleset decides what actually counts as suspicious.
tcpSynAckWindowMax = 4096
// tcpFirstSegLenMax is the size at or below which we record the length of
// the client's first data segment. It is the other half of the same signal:
// whatever the peer does to the advertised window, the observable result is
// a first request split across segments. Normal first segments run to
// hundreds of bytes, so nothing is recorded for them.
tcpFirstSegLenMax = 64
)
type tcpContext struct {
*gopacket.PacketMetadata
Verdict tcpVerdict
}
func (ctx *tcpContext) GetCaptureInfo() gopacket.CaptureInfo {
return ctx.CaptureInfo
}
type tcpStreamFactory struct {
WorkerID int
Logger Logger
Node *snowflake.Node
RulesetMutex sync.RWMutex
Ruleset ruleset.Ruleset
}
func (f *tcpStreamFactory) New(ipFlow, tcpFlow gopacket.Flow, tcp *layers.TCP, ac reassembly.AssemblerContext) reassembly.Stream {
id := f.Node.Generate()
ipSrc, ipDst := net.IP(ipFlow.Src().Raw()), net.IP(ipFlow.Dst().Raw())
info := ruleset.StreamInfo{
ID: id.Int64(),
Protocol: ruleset.ProtocolTCP,
SrcIP: ipSrc,
DstIP: ipDst,
SrcPort: uint16(tcp.SrcPort),
DstPort: uint16(tcp.DstPort),
Props: make(analyzer.CombinedPropMap),
}
f.Logger.TCPStreamNew(f.WorkerID, info)
f.RulesetMutex.RLock()
rs := f.Ruleset
f.RulesetMutex.RUnlock()
ans := analyzersToTCPAnalyzers(rs.Analyzers(info))
// Create entries for each analyzer
entries := make([]*tcpStreamEntry, 0, len(ans))
for _, a := range ans {
entries = append(entries, &tcpStreamEntry{
Name: a.Name(),
Stream: a.NewTCP(analyzer.TCPInfo{
SrcIP: ipSrc,
DstIP: ipDst,
SrcPort: uint16(tcp.SrcPort),
DstPort: uint16(tcp.DstPort),
}, &analyzerLogger{
StreamID: id.Int64(),
Name: a.Name(),
Logger: f.Logger,
}),
HasLimit: a.Limit() > 0,
Quota: a.Limit(),
})
}
return &tcpStream{
info: info,
virgin: true,
logger: f.Logger,
ruleset: rs,
activeEntries: entries,
synAckWindow: -1,
synAckWscale: -1,
}
}
func (f *tcpStreamFactory) UpdateRuleset(r ruleset.Ruleset) error {
f.RulesetMutex.Lock()
defer f.RulesetMutex.Unlock()
f.Ruleset = r
return nil
}
type tcpStream struct {
info ruleset.StreamInfo
virgin bool // true if no packets have been processed
logger Logger
ruleset ruleset.Ruleset
activeEntries []*tcpStreamEntry
lastVerdict tcpVerdict
released bool // 最终判定已发出,分析器和属性表已经释放
firstSegSeen bool // true once the client's first data segment was measured
synAckWindow int // window of the observed SYN-ACK, -1 if none seen yet
synAckWscale int // its window scale factor, -1 if the option was absent
}
type tcpStreamEntry struct {
Name string
Stream analyzer.TCPStream
HasLimit bool
Quota int
}
func (s *tcpStream) Accept(tcp *layers.TCP, ci gopacket.CaptureInfo, dir reassembly.TCPFlowDirection, nextSeq reassembly.Sequence, start *bool, ac reassembly.AssemblerContext) bool {
if s.released {
// 最终判定已发出,后续报文一律沿用;Props 已经释放,不能再碰
ac.(*tcpContext).Verdict = s.lastVerdict
return false
}
if tcp.SYN && tcp.ACK && s.synAckWindow < 0 {
s.synAckWindow, s.synAckWscale = int(tcp.Window), tcpWindowScale(tcp)
if s.synAckWindow <= tcpSynAckWindowMax {
s.tcpMeta()
}
}
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
}
}
// tcpWindowScale returns the window scale factor a segment negotiates, or -1
// if it carries no such option.
func tcpWindowScale(tcp *layers.TCP) int {
for _, opt := range tcp.Options {
if opt.OptionType == layers.TCPOptionKindWindowScale && len(opt.OptionData) == 1 {
return int(opt.OptionData[0])
}
}
return -1
}
// tcpMeta returns the engine's TCP metadata for this stream, creating it on
// first use. Whatever prompted the call, the handshake fields are filled in
// too, so that a rule reading one field never trips over a missing sibling.
// Only streams worth reporting on get here, so normal traffic allocates nothing.
//
// Per RFC 7323 the window scale option does not apply to the window field of
// the SYN-ACK itself, so synack_window is a literal byte count. A stack whose
// receive buffer really is a few bytes would not also negotiate a large scale
// factor, which makes a small synack_window paired with a non-zero
// synack_wscale a strong sign that the field was rewritten in flight.
func (s *tcpStream) tcpMeta() analyzer.PropMap {
m := s.info.Props[tcpMetaPropKey]
if m == nil {
m = analyzer.PropMap{}
if s.synAckWindow >= 0 {
m["synack_window"] = s.synAckWindow
if s.synAckWscale >= 0 {
m["synack_wscale"] = s.synAckWscale
}
}
s.info.Props[tcpMetaPropKey] = m
}
return m
}
// recordFirstSegLen notes an unusually short first data segment from the client.
// Only the first segment counts, whatever its size, so that a short segment
// later in the stream is never mistaken for the opening one. It reports whether
// anything was written, so the caller knows to re-run the ruleset.
func (s *tcpStream) recordFirstSegLen(n int) bool {
if s.firstSegSeen {
return false
}
s.firstSegSeen = true
if n > tcpFirstSegLenMax {
return false
}
s.tcpMeta()["first_seg_len"] = n
return true
}
func (s *tcpStream) ReassembledSG(sg reassembly.ScatterGather, ac reassembly.AssemblerContext) {
if s.released {
// 判定已发出。还会走到这里只可能是超时 flush 补送的乱序页,没有东西可处理
return
}
dir, start, end, skip := sg.Info()
rev := dir == reassembly.TCPDirServerToClient
avail, _ := sg.Lengths()
data := sg.Fetch(avail)
updated := false
if !rev && len(data) > 0 {
// Force a ruleset match if this is worth reporting, since the analyzers
// may not produce an update of their own on this segment.
updated = s.recordFirstSegLen(len(data))
}
for i := len(s.activeEntries) - 1; i >= 0; i-- {
// Important: reverse order so we can remove entries
entry := s.activeEntries[i]
update, closeUpdate, done := s.feedEntry(entry, rev, start, end, skip, data)
up1 := processPropUpdate(s.info.Props, entry.Name, update)
up2 := processPropUpdate(s.info.Props, entry.Name, closeUpdate)
updated = updated || up1 || up2
if done {
s.activeEntries = append(s.activeEntries[:i], s.activeEntries[i+1:]...)
}
}
ctx := ac.(*tcpContext)
if updated || s.virgin {
s.virgin = false
s.logger.TCPStreamPropUpdate(s.info, false)
// Match properties against ruleset
result := s.ruleset.Match(s.info)
action := result.Action
if action != ruleset.ActionMaybe && action != ruleset.ActionModify {
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()
}
}
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)
}
if len(s.activeEntries) == 0 {
// 不会再有分析器读数据,也不会再匹配规则:从这一刻起 Accept 只看 lastVerdict
s.release()
}
}
func (s *tcpStream) ReassemblyComplete(ac reassembly.AssemblerContext) bool {
s.closeActiveEntries()
s.release()
return true
}
func (s *tcpStream) closeActiveEntries() {
// Signal close to all active entries
updated := false
for _, entry := range s.activeEntries {
update := entry.Stream.Close(false)
up := processPropUpdate(s.info.Props, entry.Name, update)
updated = updated || up
}
if updated {
s.logger.TCPStreamPropUpdate(s.info, true)
}
s.activeEntries = nil
}
// release 在最终判定发出后丢掉不会再用到的状态。
//
// 判定之后 Accept 只看 lastVerdict,分析器和属性表都不会再被读。但这条流本身还要在
// 连接池里挂到超时被 flush,而 gopacket 回收连接槽位时并不清掉 stream 指针,
// 要等下一条连接复用那个槽位才会覆盖。不主动放手的话,每条判定完的流会带着
// 全部分析器对象一直活到那时候;节点上的 heap profile 里这部分占到了八成。
func (s *tcpStream) release() {
s.released = true
s.activeEntries = nil
s.info.Props = nil
s.ruleset = nil
}
func (s *tcpStream) feedEntry(entry *tcpStreamEntry, rev, start, end bool, skip int, data []byte) (update *analyzer.PropUpdate, closeUpdate *analyzer.PropUpdate, done bool) {
if !entry.HasLimit {
update, done = entry.Stream.Feed(rev, start, end, skip, data)
} else {
qData := data
if len(qData) > entry.Quota {
qData = qData[:entry.Quota]
}
update, done = entry.Stream.Feed(rev, start, end, skip, qData)
entry.Quota -= len(qData)
if entry.Quota <= 0 {
// Quota exhausted, signal close & drop the entry
closeUpdate = entry.Stream.Close(true)
done = true
}
}
return
}
func analyzersToTCPAnalyzers(ans []analyzer.Analyzer) []analyzer.TCPAnalyzer {
tcpAns := make([]analyzer.TCPAnalyzer, 0, len(ans))
for _, a := range ans {
if tcpM, ok := a.(analyzer.TCPAnalyzer); ok {
tcpAns = append(tcpAns, tcpM)
}
}
return tcpAns
}
func actionToTCPVerdict(a ruleset.Action) tcpVerdict {
switch a {
case ruleset.ActionMaybe, ruleset.ActionAllow, ruleset.ActionModify:
return tcpVerdictAcceptStream
case ruleset.ActionBlock, ruleset.ActionDrop:
return tcpVerdictDropStream
default:
// Should never happen
return tcpVerdictAcceptStream
}
}
-171
View File
@@ -1,171 +0,0 @@
package engine
import (
"reflect"
"testing"
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/ruleset"
"github.com/google/gopacket/layers"
)
func newTestStream() *tcpStream {
return &tcpStream{
info: ruleset.StreamInfo{Props: analyzer.CombinedPropMap{}},
synAckWindow: -1,
synAckWscale: -1,
}
}
func TestTCPWindowScale(t *testing.T) {
mss := layers.TCPOption{OptionType: layers.TCPOptionKindMSS, OptionLength: 4, OptionData: []byte{0x05, 0xb4}}
tests := []struct {
name string
opts []layers.TCPOption
want int
}{
{
name: "present",
opts: []layers.TCPOption{mss, {OptionType: layers.TCPOptionKindWindowScale, OptionLength: 3, OptionData: []byte{7}}},
want: 7,
},
{name: "absent", opts: []layers.TCPOption{mss}, want: -1},
{
name: "malformed is ignored",
opts: []layers.TCPOption{{OptionType: layers.TCPOptionKindWindowScale, OptionLength: 2, OptionData: nil}},
want: -1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tcpWindowScale(&layers.TCP{Options: tt.opts}); got != tt.want {
t.Errorf("wscale = %d, want %d", got, tt.want)
}
})
}
}
// Whichever signal materializes the prop map, the handshake fields must come
// along with it. Otherwise a rule like `tcpmeta != nil && tcpmeta.synack_window
// <= 16` sees a map without that key and fails at runtime with a nil comparison.
func TestTCPMetaAlwaysCarriesHandshakeFields(t *testing.T) {
tests := []struct {
name string
window int // -1 for a stream where no SYN-ACK was observed
wscale int
want analyzer.PropMap
}{
{
name: "normal handshake, short first segment",
window: 64240,
wscale: 7,
want: analyzer.PropMap{"synack_window": 64240, "synack_wscale": 7, "first_seg_len": 21},
},
{
name: "rewritten window",
window: 1,
wscale: 7,
want: analyzer.PropMap{"synack_window": 1, "synack_wscale": 7, "first_seg_len": 21},
},
{
name: "no window scale negotiated",
window: 64240,
wscale: -1,
want: analyzer.PropMap{"synack_window": 64240, "first_seg_len": 21},
},
{
name: "return path not visible",
window: -1,
wscale: -1,
want: analyzer.PropMap{"first_seg_len": 21},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := newTestStream()
s.synAckWindow, s.synAckWscale = tt.window, tt.wscale
if !s.recordFirstSegLen(21) {
t.Fatal("first segment not recorded")
}
if got := s.info.Props[tcpMetaPropKey]; !reflect.DeepEqual(got, tt.want) {
t.Errorf("props = %#v, want %#v", got, tt.want)
}
})
}
}
func TestTCPRecordFirstSegLen(t *testing.T) {
tests := []struct {
name string
segLen int
recorded bool
}{
{name: "one byte segment", segLen: 1, recorded: true},
{name: "at the threshold", segLen: tcpFirstSegLenMax, recorded: true},
{name: "just over the threshold", segLen: tcpFirstSegLenMax + 1, recorded: false},
{name: "normal first segment", segLen: 517, recorded: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := newTestStream()
if got := s.recordFirstSegLen(tt.segLen); got != tt.recorded {
t.Errorf("recorded = %v, want %v", got, tt.recorded)
}
m := s.info.Props[tcpMetaPropKey]
if !tt.recorded {
// A normal connection must not allocate a prop map at all.
if m != nil {
t.Errorf("props = %#v, want none", m)
}
return
}
if got := m["first_seg_len"]; got != tt.segLen {
t.Errorf("first_seg_len = %v, want %d", got, tt.segLen)
}
})
}
}
func TestTCPRecordFirstSegLenIsOneShot(t *testing.T) {
s := newTestStream()
s.recordFirstSegLen(1)
if s.recordFirstSegLen(5) {
t.Error("a later segment was recorded as the first one")
}
if got := s.info.Props[tcpMetaPropKey]["first_seg_len"]; got != 1 {
t.Errorf("first_seg_len = %v, want 1", got)
}
}
// A stream that opens normally must not be able to look like a split one just
// because a short segment shows up later.
func TestTCPNormalFirstSegmentClosesTheWindow(t *testing.T) {
s := newTestStream()
if s.recordFirstSegLen(517) {
t.Fatal("a normal first segment should not be recorded")
}
if s.recordFirstSegLen(1) {
t.Error("a later short segment was recorded as the first one")
}
if m := s.info.Props[tcpMetaPropKey]; m != nil {
t.Errorf("props = %#v, want none", m)
}
}
func TestTCPSynAckWindowThreshold(t *testing.T) {
tests := []struct {
window uint16
record bool
}{
{window: 1, record: true},
{window: tcpSynAckWindowMax, record: true},
{window: tcpSynAckWindowMax + 1, record: false},
{window: 64240, record: false},
}
for _, tt := range tests {
if got := int(tt.window) <= tcpSynAckWindowMax; got != tt.record {
t.Errorf("window %d: recorded = %v, want %v", tt.window, got, tt.record)
}
}
}
-308
View File
@@ -1,308 +0,0 @@
package engine
import (
"errors"
"net"
"sync"
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/io"
"github.com/apernet/OpenGFW/modifier"
"github.com/apernet/OpenGFW/ruleset"
"github.com/bwmarrin/snowflake"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
lru "github.com/hashicorp/golang-lru/v2"
)
// udpVerdict is a subset of io.Verdict for UDP streams.
// For UDP, we support all verdicts.
type udpVerdict io.Verdict
const (
udpVerdictAccept = udpVerdict(io.VerdictAccept)
udpVerdictAcceptModify = udpVerdict(io.VerdictAcceptModify)
udpVerdictAcceptStream = udpVerdict(io.VerdictAcceptStream)
udpVerdictDrop = udpVerdict(io.VerdictDrop)
udpVerdictDropStream = udpVerdict(io.VerdictDropStream)
)
var errInvalidModifier = errors.New("invalid modifier")
type udpContext struct {
Verdict udpVerdict
Packet []byte
}
type udpStreamFactory struct {
WorkerID int
Logger Logger
Node *snowflake.Node
RulesetMutex sync.RWMutex
Ruleset ruleset.Ruleset
}
func (f *udpStreamFactory) New(ipFlow, udpFlow gopacket.Flow, udp *layers.UDP, uc *udpContext) *udpStream {
id := f.Node.Generate()
ipSrc, ipDst := net.IP(ipFlow.Src().Raw()), net.IP(ipFlow.Dst().Raw())
info := ruleset.StreamInfo{
ID: id.Int64(),
Protocol: ruleset.ProtocolUDP,
SrcIP: ipSrc,
DstIP: ipDst,
SrcPort: uint16(udp.SrcPort),
DstPort: uint16(udp.DstPort),
Props: make(analyzer.CombinedPropMap),
}
f.Logger.UDPStreamNew(f.WorkerID, info)
f.RulesetMutex.RLock()
rs := f.Ruleset
f.RulesetMutex.RUnlock()
ans := analyzersToUDPAnalyzers(rs.Analyzers(info))
// Create entries for each analyzer
entries := make([]*udpStreamEntry, 0, len(ans))
for _, a := range ans {
entries = append(entries, &udpStreamEntry{
Name: a.Name(),
Stream: a.NewUDP(analyzer.UDPInfo{
SrcIP: ipSrc,
DstIP: ipDst,
SrcPort: uint16(udp.SrcPort),
DstPort: uint16(udp.DstPort),
}, &analyzerLogger{
StreamID: id.Int64(),
Name: a.Name(),
Logger: f.Logger,
}),
HasLimit: a.Limit() > 0,
Quota: a.Limit(),
})
}
return &udpStream{
info: info,
virgin: true,
logger: f.Logger,
ruleset: rs,
activeEntries: entries,
}
}
func (f *udpStreamFactory) UpdateRuleset(r ruleset.Ruleset) error {
f.RulesetMutex.Lock()
defer f.RulesetMutex.Unlock()
f.Ruleset = r
return nil
}
type udpStreamManager struct {
factory *udpStreamFactory
streams *lru.Cache[uint32, *udpStreamValue]
}
type udpStreamValue struct {
Stream *udpStream
IPFlow gopacket.Flow
UDPFlow gopacket.Flow
}
func (v *udpStreamValue) Match(ipFlow, udpFlow gopacket.Flow) (ok, rev bool) {
fwd := v.IPFlow == ipFlow && v.UDPFlow == udpFlow
rev = v.IPFlow == ipFlow.Reverse() && v.UDPFlow == udpFlow.Reverse()
return fwd || rev, rev
}
func newUDPStreamManager(factory *udpStreamFactory, maxStreams int) (*udpStreamManager, error) {
ss, err := lru.New[uint32, *udpStreamValue](maxStreams)
if err != nil {
return nil, err
}
return &udpStreamManager{
factory: factory,
streams: ss,
}, nil
}
func (m *udpStreamManager) MatchWithContext(streamID uint32, ipFlow gopacket.Flow, udp *layers.UDP, uc *udpContext) {
rev := false
value, ok := m.streams.Get(streamID)
if !ok {
// New stream
value = &udpStreamValue{
Stream: m.factory.New(ipFlow, udp.TransportFlow(), udp, uc),
IPFlow: ipFlow,
UDPFlow: udp.TransportFlow(),
}
m.streams.Add(streamID, value)
} else {
// Stream ID exists, but is it really the same stream?
ok, rev = value.Match(ipFlow, udp.TransportFlow())
if !ok {
// It's not - close the old stream & replace it with a new one
value.Stream.Close()
value = &udpStreamValue{
Stream: m.factory.New(ipFlow, udp.TransportFlow(), udp, uc),
IPFlow: ipFlow,
UDPFlow: udp.TransportFlow(),
}
m.streams.Add(streamID, value)
}
}
if value.Stream.Accept(udp, rev, uc) {
value.Stream.Feed(udp, rev, uc)
}
}
type udpStream struct {
info ruleset.StreamInfo
virgin bool // true if no packets have been processed
logger Logger
ruleset ruleset.Ruleset
activeEntries []*udpStreamEntry
lastVerdict udpVerdict
}
type udpStreamEntry struct {
Name string
Stream analyzer.UDPStream
HasLimit bool
Quota int
}
func (s *udpStream) Accept(udp *layers.UDP, rev bool, uc *udpContext) bool {
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) {
updated := false
for i := len(s.activeEntries) - 1; i >= 0; i-- {
// Important: reverse order so we can remove entries
entry := s.activeEntries[i]
update, closeUpdate, done := s.feedEntry(entry, rev, udp.Payload)
up1 := processPropUpdate(s.info.Props, entry.Name, update)
up2 := processPropUpdate(s.info.Props, entry.Name, closeUpdate)
updated = updated || up1 || up2
if done {
s.activeEntries = append(s.activeEntries[:i], s.activeEntries[i+1:]...)
}
}
if updated || s.virgin {
s.virgin = false
s.logger.UDPStreamPropUpdate(s.info, false)
// Match properties against ruleset
result := s.ruleset.Match(s.info)
action := result.Action
if action == ruleset.ActionModify {
// Call the modifier instance
udpMI, ok := result.ModInstance.(modifier.UDPModifierInstance)
if !ok {
// Not for UDP, fallback to maybe
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
s.logger.ModifyError(s.info, err)
action = ruleset.ActionMaybe
}
}
}
if action != ruleset.ActionMaybe {
verdict, final := actionToUDPVerdict(action)
s.lastVerdict = verdict
uc.Verdict = verdict
s.logger.UDPStreamAction(s.info, action, false)
if final {
s.closeActiveEntries()
}
}
}
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)
}
if len(s.activeEntries) == 0 {
// 同 tcpStream.release:这条流会在 LRU 里一直待到被挤出去,别让它带着分析器一起等
s.release()
}
}
func (s *udpStream) Close() {
s.closeActiveEntries()
s.release()
}
func (s *udpStream) closeActiveEntries() {
// Signal close to all active entries
updated := false
for _, entry := range s.activeEntries {
update := entry.Stream.Close(false)
up := processPropUpdate(s.info.Props, entry.Name, update)
updated = updated || up
}
if updated {
s.logger.UDPStreamPropUpdate(s.info, true)
}
s.activeEntries = nil
}
// release 在最终判定发出后丢掉不会再用到的状态,Accept 此后只看 lastVerdict
func (s *udpStream) release() {
s.activeEntries = nil
s.info.Props = nil
s.ruleset = nil
}
func (s *udpStream) feedEntry(entry *udpStreamEntry, rev bool, data []byte) (update *analyzer.PropUpdate, closeUpdate *analyzer.PropUpdate, done bool) {
update, done = entry.Stream.Feed(rev, data)
if entry.HasLimit {
entry.Quota -= len(data)
if entry.Quota <= 0 {
// Quota exhausted, signal close & drop the entry
closeUpdate = entry.Stream.Close(true)
done = true
}
}
return
}
func analyzersToUDPAnalyzers(ans []analyzer.Analyzer) []analyzer.UDPAnalyzer {
udpAns := make([]analyzer.UDPAnalyzer, 0, len(ans))
for _, a := range ans {
if udpM, ok := a.(analyzer.UDPAnalyzer); ok {
udpAns = append(udpAns, udpM)
}
}
return udpAns
}
func actionToUDPVerdict(a ruleset.Action) (v udpVerdict, final bool) {
switch a {
case ruleset.ActionMaybe:
return udpVerdictAccept, false
case ruleset.ActionAllow:
return udpVerdictAcceptStream, true
case ruleset.ActionBlock:
return udpVerdictDropStream, true
case ruleset.ActionDrop:
return udpVerdictDrop, false
case ruleset.ActionModify:
return udpVerdictAcceptModify, false
default:
// Should never happen
return udpVerdictAccept, false
}
}
-50
View File
@@ -1,50 +0,0 @@
package engine
import "github.com/apernet/OpenGFW/analyzer"
var _ analyzer.Logger = (*analyzerLogger)(nil)
type analyzerLogger struct {
StreamID int64
Name string
Logger Logger
}
func (l *analyzerLogger) Debugf(format string, args ...interface{}) {
l.Logger.AnalyzerDebugf(l.StreamID, l.Name, format, args...)
}
func (l *analyzerLogger) Infof(format string, args ...interface{}) {
l.Logger.AnalyzerInfof(l.StreamID, l.Name, format, args...)
}
func (l *analyzerLogger) Errorf(format string, args ...interface{}) {
l.Logger.AnalyzerErrorf(l.StreamID, l.Name, format, args...)
}
func processPropUpdate(cpm analyzer.CombinedPropMap, name string, update *analyzer.PropUpdate) (updated bool) {
if update == nil || update.Type == analyzer.PropUpdateNone {
return false
}
switch update.Type {
case analyzer.PropUpdateMerge:
m := cpm[name]
if m == nil {
m = make(analyzer.PropMap, len(update.M))
cpm[name] = m
}
for k, v := range update.M {
m[k] = v
}
return true
case analyzer.PropUpdateReplace:
cpm[name] = update.M
return true
case analyzer.PropUpdateDelete:
delete(cpm, name)
return true
default:
// Invalid update type, ignore for now
return false
}
}
-215
View File
@@ -1,215 +0,0 @@
package engine
import (
"context"
"time"
"github.com/apernet/OpenGFW/io"
"github.com/apernet/OpenGFW/ruleset"
"github.com/bwmarrin/snowflake"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/google/gopacket/reassembly"
)
const (
defaultChanSize = 64
defaultTCPMaxBufferedPagesTotal = 65536
defaultTCPMaxBufferedPagesPerConnection = 16
defaultTCPTimeout = 10 * time.Minute
defaultUDPMaxStreams = 4096
// 超时 flush 的间隔范围。判定后的流走 connmark 旁路,FIN 到不了用户态,
// 只能靠定时 flush 把它们从连接池里清掉。间隔取超时的一半:
// 之前固定 1 分钟,30 秒的超时实际会让流在池里活到 90 秒。
tcpMaxFlushInterval = 1 * time.Minute
tcpMinFlushInterval = 1 * time.Second
)
// tcpFlushIntervalFor 返回给定 TCP 超时下的 flush 间隔
func tcpFlushIntervalFor(timeout time.Duration) time.Duration {
return min(tcpMaxFlushInterval, max(timeout/2, tcpMinFlushInterval))
}
type workerPacket struct {
StreamID uint32
Packet gopacket.Packet
SetVerdict func(io.Verdict, []byte) error
}
type worker struct {
id int
packetChan chan *workerPacket
logger Logger
tcpStreamFactory *tcpStreamFactory
tcpStreamPool *reassembly.StreamPool
tcpAssembler *reassembly.Assembler
tcpTimeout time.Duration
udpStreamFactory *udpStreamFactory
udpStreamManager *udpStreamManager
modSerializeBuffer gopacket.SerializeBuffer
}
type workerConfig struct {
ID int
ChanSize int
Logger Logger
Ruleset ruleset.Ruleset
TCPMaxBufferedPagesTotal int
TCPMaxBufferedPagesPerConn int
TCPTimeout time.Duration
UDPMaxStreams int
}
func (c *workerConfig) fillDefaults() {
if c.ChanSize <= 0 {
c.ChanSize = defaultChanSize
}
if c.TCPMaxBufferedPagesTotal <= 0 {
c.TCPMaxBufferedPagesTotal = defaultTCPMaxBufferedPagesTotal
}
if c.TCPMaxBufferedPagesPerConn <= 0 {
c.TCPMaxBufferedPagesPerConn = defaultTCPMaxBufferedPagesPerConnection
}
if c.TCPTimeout <= 0 {
c.TCPTimeout = defaultTCPTimeout
}
if c.UDPMaxStreams <= 0 {
c.UDPMaxStreams = defaultUDPMaxStreams
}
}
func newWorker(config workerConfig) (*worker, error) {
config.fillDefaults()
sfNode, err := snowflake.NewNode(int64(config.ID))
if err != nil {
return nil, err
}
tcpSF := &tcpStreamFactory{
WorkerID: config.ID,
Logger: config.Logger,
Node: sfNode,
Ruleset: config.Ruleset,
}
tcpStreamPool := reassembly.NewStreamPool(tcpSF)
tcpAssembler := reassembly.NewAssembler(tcpStreamPool)
tcpAssembler.MaxBufferedPagesTotal = config.TCPMaxBufferedPagesTotal
tcpAssembler.MaxBufferedPagesPerConnection = config.TCPMaxBufferedPagesPerConn
udpSF := &udpStreamFactory{
WorkerID: config.ID,
Logger: config.Logger,
Node: sfNode,
Ruleset: config.Ruleset,
}
udpSM, err := newUDPStreamManager(udpSF, config.UDPMaxStreams)
if err != nil {
return nil, err
}
return &worker{
id: config.ID,
packetChan: make(chan *workerPacket, config.ChanSize),
logger: config.Logger,
tcpStreamFactory: tcpSF,
tcpStreamPool: tcpStreamPool,
tcpAssembler: tcpAssembler,
tcpTimeout: config.TCPTimeout,
udpStreamFactory: udpSF,
udpStreamManager: udpSM,
modSerializeBuffer: gopacket.NewSerializeBuffer(),
}, nil
}
func (w *worker) Feed(p *workerPacket) {
w.packetChan <- p
}
func (w *worker) Run(ctx context.Context) {
w.logger.WorkerStart(w.id)
defer w.logger.WorkerStop(w.id)
tcpFlushTicker := time.NewTicker(tcpFlushIntervalFor(w.tcpTimeout))
defer tcpFlushTicker.Stop()
for {
select {
case <-ctx.Done():
return
case wPkt := <-w.packetChan:
if wPkt == nil {
// Closed
return
}
v, b := w.handle(wPkt.StreamID, wPkt.Packet)
_ = wPkt.SetVerdict(v, b)
case <-tcpFlushTicker.C:
w.flushTCP(w.tcpTimeout)
}
}
}
func (w *worker) UpdateRuleset(r ruleset.Ruleset) error {
if err := w.tcpStreamFactory.UpdateRuleset(r); err != nil {
return err
}
return w.udpStreamFactory.UpdateRuleset(r)
}
func (w *worker) handle(streamID uint32, p gopacket.Packet) (io.Verdict, []byte) {
netLayer, trLayer := p.NetworkLayer(), p.TransportLayer()
if netLayer == nil || trLayer == nil {
// Invalid packet
return io.VerdictAccept, nil
}
ipFlow := netLayer.NetworkFlow()
switch tr := trLayer.(type) {
case *layers.TCP:
return w.handleTCP(ipFlow, p.Metadata(), tr), nil
case *layers.UDP:
v, modPayload := w.handleUDP(streamID, ipFlow, tr)
if v == io.VerdictAcceptModify && modPayload != nil {
tr.Payload = modPayload
_ = tr.SetNetworkLayerForChecksum(netLayer)
_ = w.modSerializeBuffer.Clear()
err := gopacket.SerializePacket(w.modSerializeBuffer,
gopacket.SerializeOptions{
FixLengths: true,
ComputeChecksums: true,
}, p)
if err != nil {
// Just accept without modification for now
return io.VerdictAccept, nil
}
return v, w.modSerializeBuffer.Bytes()
}
return v, nil
default:
// Unsupported protocol
return io.VerdictAccept, nil
}
}
func (w *worker) handleTCP(ipFlow gopacket.Flow, pMeta *gopacket.PacketMetadata, tcp *layers.TCP) io.Verdict {
ctx := &tcpContext{
PacketMetadata: pMeta,
Verdict: tcpVerdictAccept,
}
w.tcpAssembler.AssembleWithContext(ipFlow, tcp, ctx)
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,
}
w.udpStreamManager.MatchWithContext(streamID, ipFlow, udp, ctx)
return io.Verdict(ctx.Verdict), ctx.Packet
}
-46
View File
@@ -1,46 +0,0 @@
module github.com/apernet/OpenGFW
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.16.3
github.com/florianl/go-nfqueue v1.3.2-0.20231218173729-f2bdeb033acf
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
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/fsnotify/fsnotify v1.7.0 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/native v1.0.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
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/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
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/text v0.14.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
)
-154
View File
@@ -1,154 +0,0 @@
github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8=
github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/expr-lang/expr v1.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.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=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
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=
github.com/mdlayher/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA=
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=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
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=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
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.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=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-62
View File
@@ -1,62 +0,0 @@
package io
import (
"context"
"net"
"time"
)
type Verdict int
const (
// VerdictAccept accepts the packet, but continues to process the stream.
VerdictAccept Verdict = iota
// VerdictAcceptModify is like VerdictAccept, but replaces the packet with a new one.
VerdictAcceptModify
// VerdictAcceptStream accepts the packet and stops processing the stream.
VerdictAcceptStream
// VerdictDrop drops the packet, but does not block the stream.
VerdictDrop
// VerdictDropStream drops the packet and blocks the stream.
VerdictDropStream
)
// Packet represents an IP packet.
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.
type PacketCallback func(Packet, error) bool
type PacketIO interface {
// Register registers a callback to be called for each packet received.
// The callback should be called in one or more separate goroutines,
// and stop when the context is cancelled.
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 {
Err error
}
func (e *ErrInvalidPacket) Error() string {
return "invalid packet: " + e.Err.Error()
}
-480
View File
@@ -1,480 +0,0 @@
package io
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 (
nfqueueDefaultQueueNum = 100
nfqueueMaxPacketLen = 0xFFFF
nfqueueDefaultQueueSize = 128
nfqueueDefaultConnMarkAccept = 1001
nftFamily = "inet"
nftDefaultTable = "opengfw"
)
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
}
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"}})
}
return rules, nil
}
var _ PacketIO = (*nfqueuePacketIO)(nil)
var errNotNFQueuePacket = errors.New("not an NFQueue packet")
type nfqueuePacketIO struct {
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
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
}
if config.QueueNum == nil {
queueNum := uint16(nfqueueDefaultQueueNum)
config.QueueNum = &queueNum
}
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: *config.QueueNum,
MaxPacketLen: nfqueueMaxPacketLen,
MaxQueueLen: config.QueueSize,
Copymode: nfqueue.NfQnlCopyPacket,
Flags: nfqueue.NfQaCfgFlagConntrack,
})
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,
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 ok, verdict := n.packetAttributeSanityCheck(a); !ok {
if a.PacketID != nil {
_ = n.n.SetVerdict(*a.PacketID, verdict)
}
return 0
}
p := &nfqueuePacket{
id: *a.PacketID,
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.rSet {
if n.ipt4 != nil {
err = n.setupIpt(false)
} else {
err = n.setupNft(false)
}
if err != nil {
return err
}
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 {
return &ErrInvalidPacket{Err: errNotNFQueuePacket}
}
switch v {
case VerdictAccept:
return n.n.SetVerdict(nP.id, nfqueue.NfAccept)
case VerdictAcceptModify:
return n.n.SetVerdictModPacket(nP.id, nfqueue.NfAccept, newPacket)
case VerdictAcceptStream:
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, n.connMarkDrop)
default:
// Invalid verdict, ignore for now
return nil
}
}
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
}
if remove {
err = iptsBatchDeleteIfExists([]*iptables.IPTables{n.ipt4, n.ipt6}, rules)
} else {
err = iptsBatchAppendUnique([]*iptables.IPTables{n.ipt4, n.ipt6}, rules)
}
if err != nil {
return err
}
return nil
}
var _ Packet = (*nfqueuePacket)(nil)
type nfqueuePacket struct {
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
}
func okBoolToInt(ok bool) int {
if ok {
return 0
} else {
return 1
}
}
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
}
func iptsBatchAppendUnique(ipts []*iptables.IPTables, rules []iptRule) error {
for _, r := range rules {
for _, ipt := range ipts {
err := ipt.AppendUnique(r.Table, r.Chain, r.RuleSpec...)
if err != nil {
return err
}
}
}
return nil
}
func iptsBatchDeleteIfExists(ipts []*iptables.IPTables, rules []iptRule) error {
for _, r := range rules {
for _, ipt := range ipts {
err := ipt.DeleteIfExists(r.Table, r.Chain, r.RuleSpec...)
if err != nil {
return err
}
}
}
return nil
}
func ctIDFromCtBytes(ct []byte) uint32 {
ctAttrs, err := netlink.UnmarshalAttributes(ct)
if err != nil {
return 0
}
for _, attr := range ctAttrs {
if attr.Type == 12 { // CTA_ID
return binary.BigEndian.Uint32(attr.Data)
}
}
return 0
}
-136
View File
@@ -1,136 +0,0 @@
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
}
-7
View File
@@ -1,7 +0,0 @@
package main
import "github.com/apernet/OpenGFW/cmd"
func main() {
cmd.Execute()
}
-32
View File
@@ -1,32 +0,0 @@
package modifier
type Modifier interface {
// Name returns the name of the modifier.
Name() string
// New returns a new modifier instance.
New(args map[string]interface{}) (Instance, error)
}
type Instance interface{}
type UDPModifierInstance interface {
Instance
// Process takes a UDP packet and returns a modified UDP packet.
Process(data []byte) ([]byte, error)
}
type ErrInvalidPacket struct {
Err error
}
func (e *ErrInvalidPacket) Error() string {
return "invalid packet: " + e.Err.Error()
}
type ErrInvalidArgs struct {
Err error
}
func (e *ErrInvalidArgs) Error() string {
return "invalid args: " + e.Err.Error()
}
-96
View File
@@ -1,96 +0,0 @@
package udp
import (
"errors"
"net"
"github.com/apernet/OpenGFW/modifier"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
)
var _ modifier.Modifier = (*DNSModifier)(nil)
var (
errInvalidIP = errors.New("invalid ip")
errNotValidDNSResponse = errors.New("not a valid dns response")
errEmptyDNSQuestion = errors.New("empty dns question")
)
type DNSModifier struct{}
func (m *DNSModifier) Name() string {
return "dns"
}
func (m *DNSModifier) New(args map[string]interface{}) (modifier.Instance, error) {
i := &dnsModifierInstance{}
aStr, ok := args["a"].(string)
if ok {
a := net.ParseIP(aStr).To4()
if a == nil {
return nil, &modifier.ErrInvalidArgs{Err: errInvalidIP}
}
i.A = a
}
aaaaStr, ok := args["aaaa"].(string)
if ok {
aaaa := net.ParseIP(aaaaStr).To16()
if aaaa == nil {
return nil, &modifier.ErrInvalidArgs{Err: errInvalidIP}
}
i.AAAA = aaaa
}
return i, nil
}
var _ modifier.UDPModifierInstance = (*dnsModifierInstance)(nil)
type dnsModifierInstance struct {
A net.IP
AAAA net.IP
}
func (i *dnsModifierInstance) Process(data []byte) ([]byte, error) {
dns := &layers.DNS{}
err := dns.DecodeFromBytes(data, gopacket.NilDecodeFeedback)
if err != nil {
return nil, &modifier.ErrInvalidPacket{Err: err}
}
if !dns.QR || dns.ResponseCode != layers.DNSResponseCodeNoErr {
return nil, &modifier.ErrInvalidPacket{Err: errNotValidDNSResponse}
}
if len(dns.Questions) == 0 {
return nil, &modifier.ErrInvalidPacket{Err: errEmptyDNSQuestion}
}
// In practice, most if not all DNS clients only send one question
// per packet, so we don't care about the rest for now.
q := dns.Questions[0]
switch q.Type {
case layers.DNSTypeA:
if i.A != nil {
dns.Answers = []layers.DNSResourceRecord{{
Name: q.Name,
Type: layers.DNSTypeA,
Class: layers.DNSClassIN,
IP: i.A,
}}
}
case layers.DNSTypeAAAA:
if i.AAAA != nil {
dns.Answers = []layers.DNSResourceRecord{{
Name: q.Name,
Type: layers.DNSTypeAAAA,
Class: layers.DNSClassIN,
IP: i.AAAA,
}}
}
}
buf := gopacket.NewSerializeBuffer() // Modifiers must be safe for concurrent use, so we can't reuse the buffer
err = gopacket.SerializeLayers(buf, gopacket.SerializeOptions{
FixLengths: true,
ComputeChecksums: true,
}, dns)
return buf.Bytes(), err
}
-18
View File
@@ -1,18 +0,0 @@
package builtins
import (
"net"
)
func MatchCIDR(ip string, cidr *net.IPNet) bool {
ipAddr := net.ParseIP(ip)
if ipAddr == nil {
return false
}
return cidr.Contains(ipAddr)
}
func CompileCIDR(cidr string) (*net.IPNet, error) {
_, ipNet, err := net.ParseCIDR(cidr)
return ipNet, err
}
-128
View File
@@ -1,128 +0,0 @@
package geo
import (
"io"
"net/http"
"os"
"time"
"github.com/apernet/OpenGFW/ruleset/builtins/geo/v2geo"
)
const (
geoipFilename = "geoip.dat"
geoipURL = "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geoip.dat"
geositeFilename = "geosite.dat"
geositeURL = "https://cdn.jsdelivr.net/gh/Loyalsoldier/v2ray-rules-dat@release/geosite.dat"
geoDefaultUpdateInterval = 7 * 24 * time.Hour // 7 days
)
var _ GeoLoader = (*V2GeoLoader)(nil)
// V2GeoLoader provides the on-demand GeoIP/MatchGeoSite database
// loading functionality required by the ACL engine.
// Empty filenames = automatic download from built-in URLs.
type V2GeoLoader struct {
GeoIPFilename string
GeoSiteFilename string
UpdateInterval time.Duration
DownloadFunc func(filename, url string)
DownloadErrFunc func(err error)
geoipMap map[string]*v2geo.GeoIP
geositeMap map[string]*v2geo.GeoSite
}
func NewDefaultGeoLoader(geoSiteFilename, geoIpFilename string) *V2GeoLoader {
return &V2GeoLoader{
GeoIPFilename: geoIpFilename,
GeoSiteFilename: geoSiteFilename,
DownloadFunc: func(filename, url string) {},
DownloadErrFunc: func(err error) {},
}
}
func (l *V2GeoLoader) shouldDownload(filename string) bool {
info, err := os.Stat(filename)
if os.IsNotExist(err) {
return true
}
dt := time.Since(info.ModTime())
if l.UpdateInterval == 0 {
return dt > geoDefaultUpdateInterval
} else {
return dt > l.UpdateInterval
}
}
func (l *V2GeoLoader) download(filename, url string) error {
l.DownloadFunc(filename, url)
resp, err := http.Get(url)
if err != nil {
l.DownloadErrFunc(err)
return err
}
defer resp.Body.Close()
f, err := os.Create(filename)
if err != nil {
l.DownloadErrFunc(err)
return err
}
defer f.Close()
_, err = io.Copy(f, resp.Body)
l.DownloadErrFunc(err)
return err
}
func (l *V2GeoLoader) LoadGeoIP() (map[string]*v2geo.GeoIP, error) {
if l.geoipMap != nil {
return l.geoipMap, nil
}
autoDL := false
filename := l.GeoIPFilename
if filename == "" {
autoDL = true
filename = geoipFilename
}
if autoDL && l.shouldDownload(filename) {
err := l.download(filename, geoipURL)
if err != nil {
return nil, err
}
}
m, err := v2geo.LoadGeoIP(filename)
if err != nil {
return nil, err
}
l.geoipMap = m
return m, nil
}
func (l *V2GeoLoader) LoadGeoSite() (map[string]*v2geo.GeoSite, error) {
if l.geositeMap != nil {
return l.geositeMap, nil
}
autoDL := false
filename := l.GeoSiteFilename
if filename == "" {
autoDL = true
filename = geositeFilename
}
if autoDL && l.shouldDownload(filename) {
err := l.download(filename, geositeURL)
if err != nil {
return nil, err
}
}
m, err := v2geo.LoadGeoSite(filename)
if err != nil {
return nil, err
}
l.geositeMap = m
return m, nil
}
-113
View File
@@ -1,113 +0,0 @@
package geo
import (
"net"
"strings"
"sync"
)
type GeoMatcher struct {
geoLoader GeoLoader
geoSiteMatcher map[string]hostMatcher
siteMatcherLock sync.Mutex
geoIpMatcher map[string]hostMatcher
ipMatcherLock sync.Mutex
}
func NewGeoMatcher(geoSiteFilename, geoIpFilename string) *GeoMatcher {
return &GeoMatcher{
geoLoader: NewDefaultGeoLoader(geoSiteFilename, geoIpFilename),
geoSiteMatcher: make(map[string]hostMatcher),
geoIpMatcher: make(map[string]hostMatcher),
}
}
func (g *GeoMatcher) MatchGeoIp(ip, condition string) bool {
g.ipMatcherLock.Lock()
defer g.ipMatcherLock.Unlock()
matcher, ok := g.geoIpMatcher[condition]
if !ok {
// GeoIP matcher
condition = strings.ToLower(condition)
country := condition
if len(country) == 0 {
return false
}
gMap, err := g.geoLoader.LoadGeoIP()
if err != nil {
return false
}
list, ok := gMap[country]
if !ok || list == nil {
return false
}
matcher, err = newGeoIPMatcher(list)
if err != nil {
return false
}
g.geoIpMatcher[condition] = matcher
}
parseIp := net.ParseIP(ip)
if parseIp == nil {
return false
}
ipv4 := parseIp.To4()
if ipv4 != nil {
return matcher.Match(HostInfo{IPv4: ipv4})
}
ipv6 := parseIp.To16()
if ipv6 != nil {
return matcher.Match(HostInfo{IPv6: ipv6})
}
return false
}
func (g *GeoMatcher) MatchGeoSite(site, condition string) bool {
g.siteMatcherLock.Lock()
defer g.siteMatcherLock.Unlock()
matcher, ok := g.geoSiteMatcher[condition]
if !ok {
// MatchGeoSite matcher
condition = strings.ToLower(condition)
name, attrs := parseGeoSiteName(condition)
if len(name) == 0 {
return false
}
gMap, err := g.geoLoader.LoadGeoSite()
if err != nil {
return false
}
list, ok := gMap[name]
if !ok || list == nil {
return false
}
matcher, err = newGeositeMatcher(list, attrs)
if err != nil {
return false
}
g.geoSiteMatcher[condition] = matcher
}
return matcher.Match(HostInfo{Name: site})
}
func (g *GeoMatcher) LoadGeoSite() error {
_, err := g.geoLoader.LoadGeoSite()
return err
}
func (g *GeoMatcher) LoadGeoIP() error {
_, err := g.geoLoader.LoadGeoIP()
return err
}
func parseGeoSiteName(s string) (string, []string) {
parts := strings.Split(s, "@")
base := strings.TrimSpace(parts[0])
attrs := parts[1:]
for i := range attrs {
attrs[i] = strings.TrimSpace(attrs[i])
}
return base, attrs
}
-27
View File
@@ -1,27 +0,0 @@
package geo
import (
"fmt"
"net"
"github.com/apernet/OpenGFW/ruleset/builtins/geo/v2geo"
)
type HostInfo struct {
Name string
IPv4 net.IP
IPv6 net.IP
}
func (h HostInfo) String() string {
return fmt.Sprintf("%s|%s|%s", h.Name, h.IPv4, h.IPv6)
}
type GeoLoader interface {
LoadGeoIP() (map[string]*v2geo.GeoIP, error)
LoadGeoSite() (map[string]*v2geo.GeoSite, error)
}
type hostMatcher interface {
Match(HostInfo) bool
}
-213
View File
@@ -1,213 +0,0 @@
package geo
import (
"bytes"
"errors"
"net"
"regexp"
"sort"
"strings"
"github.com/apernet/OpenGFW/ruleset/builtins/geo/v2geo"
)
var _ hostMatcher = (*geoipMatcher)(nil)
type geoipMatcher struct {
N4 []*net.IPNet // sorted
N6 []*net.IPNet // sorted
Inverse bool
}
// matchIP tries to match the given IP address with the corresponding IPNets.
// Note that this function does NOT handle the Inverse flag.
func (m *geoipMatcher) matchIP(ip net.IP) bool {
var n []*net.IPNet
if ip4 := ip.To4(); ip4 != nil {
// N4 stores IPv4 addresses in 4-byte form.
// Make sure we use it here too, otherwise bytes.Compare will fail.
ip = ip4
n = m.N4
} else {
n = m.N6
}
left, right := 0, len(n)-1
for left <= right {
mid := (left + right) / 2
if n[mid].Contains(ip) {
return true
} else if bytes.Compare(n[mid].IP, ip) < 0 {
left = mid + 1
} else {
right = mid - 1
}
}
return false
}
func (m *geoipMatcher) Match(host HostInfo) bool {
if host.IPv4 != nil {
if m.matchIP(host.IPv4) {
return !m.Inverse
}
}
if host.IPv6 != nil {
if m.matchIP(host.IPv6) {
return !m.Inverse
}
}
return m.Inverse
}
func newGeoIPMatcher(list *v2geo.GeoIP) (*geoipMatcher, error) {
n4 := make([]*net.IPNet, 0)
n6 := make([]*net.IPNet, 0)
for _, cidr := range list.Cidr {
if len(cidr.Ip) == 4 {
// IPv4
n4 = append(n4, &net.IPNet{
IP: cidr.Ip,
Mask: net.CIDRMask(int(cidr.Prefix), 32),
})
} else if len(cidr.Ip) == 16 {
// IPv6
n6 = append(n6, &net.IPNet{
IP: cidr.Ip,
Mask: net.CIDRMask(int(cidr.Prefix), 128),
})
} else {
return nil, errors.New("invalid IP length")
}
}
// Sort the IPNets, so we can do binary search later.
sort.Slice(n4, func(i, j int) bool {
return bytes.Compare(n4[i].IP, n4[j].IP) < 0
})
sort.Slice(n6, func(i, j int) bool {
return bytes.Compare(n6[i].IP, n6[j].IP) < 0
})
return &geoipMatcher{
N4: n4,
N6: n6,
Inverse: list.InverseMatch,
}, nil
}
var _ hostMatcher = (*geositeMatcher)(nil)
type geositeDomainType int
const (
geositeDomainPlain geositeDomainType = iota
geositeDomainRegex
geositeDomainRoot
geositeDomainFull
)
type geositeDomain struct {
Type geositeDomainType
Value string
Regex *regexp.Regexp
Attrs map[string]bool
}
type geositeMatcher struct {
Domains []geositeDomain
// Attributes are matched using "and" logic - if you have multiple attributes here,
// a domain must have all of those attributes to be considered a match.
Attrs []string
}
func (m *geositeMatcher) matchDomain(domain geositeDomain, host HostInfo) bool {
// Match attributes first
if len(m.Attrs) > 0 {
if len(domain.Attrs) == 0 {
return false
}
for _, attr := range m.Attrs {
if !domain.Attrs[attr] {
return false
}
}
}
switch domain.Type {
case geositeDomainPlain:
return strings.Contains(host.Name, domain.Value)
case geositeDomainRegex:
if domain.Regex != nil {
return domain.Regex.MatchString(host.Name)
}
case geositeDomainFull:
return host.Name == domain.Value
case geositeDomainRoot:
if host.Name == domain.Value {
return true
}
return strings.HasSuffix(host.Name, "."+domain.Value)
default:
return false
}
return false
}
func (m *geositeMatcher) Match(host HostInfo) bool {
for _, domain := range m.Domains {
if m.matchDomain(domain, host) {
return true
}
}
return false
}
func newGeositeMatcher(list *v2geo.GeoSite, attrs []string) (*geositeMatcher, error) {
domains := make([]geositeDomain, len(list.Domain))
for i, domain := range list.Domain {
switch domain.Type {
case v2geo.Domain_Plain:
domains[i] = geositeDomain{
Type: geositeDomainPlain,
Value: domain.Value,
Attrs: domainAttributeToMap(domain.Attribute),
}
case v2geo.Domain_Regex:
regex, err := regexp.Compile(domain.Value)
if err != nil {
return nil, err
}
domains[i] = geositeDomain{
Type: geositeDomainRegex,
Regex: regex,
Attrs: domainAttributeToMap(domain.Attribute),
}
case v2geo.Domain_Full:
domains[i] = geositeDomain{
Type: geositeDomainFull,
Value: domain.Value,
Attrs: domainAttributeToMap(domain.Attribute),
}
case v2geo.Domain_RootDomain:
domains[i] = geositeDomain{
Type: geositeDomainRoot,
Value: domain.Value,
Attrs: domainAttributeToMap(domain.Attribute),
}
default:
return nil, errors.New("unsupported domain type")
}
}
return &geositeMatcher{
Domains: domains,
Attrs: attrs,
}, nil
}
func domainAttributeToMap(attrs []*v2geo.Domain_Attribute) map[string]bool {
m := make(map[string]bool)
for _, attr := range attrs {
// Supposedly there are also int attributes,
// but nobody seems to use them, so we treat everything as boolean for now.
m[attr.Key] = true
}
return m
}
-44
View File
@@ -1,44 +0,0 @@
package v2geo
import (
"os"
"strings"
"google.golang.org/protobuf/proto"
)
// LoadGeoIP loads a GeoIP data file and converts it to a map.
// The keys of the map (country codes) are all normalized to lowercase.
func LoadGeoIP(filename string) (map[string]*GeoIP, error) {
bs, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
var list GeoIPList
if err := proto.Unmarshal(bs, &list); err != nil {
return nil, err
}
m := make(map[string]*GeoIP)
for _, entry := range list.Entry {
m[strings.ToLower(entry.CountryCode)] = entry
}
return m, nil
}
// LoadGeoSite loads a GeoSite data file and converts it to a map.
// The keys of the map (site keys) are all normalized to lowercase.
func LoadGeoSite(filename string) (map[string]*GeoSite, error) {
bs, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
var list GeoSiteList
if err := proto.Unmarshal(bs, &list); err != nil {
return nil, err
}
m := make(map[string]*GeoSite)
for _, entry := range list.Entry {
m[strings.ToLower(entry.CountryCode)] = entry
}
return m, nil
}
-745
View File
@@ -1,745 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.31.0
// protoc v4.24.4
// source: v2geo.proto
package v2geo
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// Type of domain value.
type Domain_Type int32
const (
// The value is used as is.
Domain_Plain Domain_Type = 0
// The value is used as a regular expression.
Domain_Regex Domain_Type = 1
// The value is a root domain.
Domain_RootDomain Domain_Type = 2
// The value is a domain.
Domain_Full Domain_Type = 3
)
// Enum value maps for Domain_Type.
var (
Domain_Type_name = map[int32]string{
0: "Plain",
1: "Regex",
2: "RootDomain",
3: "Full",
}
Domain_Type_value = map[string]int32{
"Plain": 0,
"Regex": 1,
"RootDomain": 2,
"Full": 3,
}
)
func (x Domain_Type) Enum() *Domain_Type {
p := new(Domain_Type)
*p = x
return p
}
func (x Domain_Type) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (Domain_Type) Descriptor() protoreflect.EnumDescriptor {
return file_v2geo_proto_enumTypes[0].Descriptor()
}
func (Domain_Type) Type() protoreflect.EnumType {
return &file_v2geo_proto_enumTypes[0]
}
func (x Domain_Type) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Domain_Type.Descriptor instead.
func (Domain_Type) EnumDescriptor() ([]byte, []int) {
return file_v2geo_proto_rawDescGZIP(), []int{0, 0}
}
// Domain for routing decision.
type Domain struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Domain matching type.
Type Domain_Type `protobuf:"varint,1,opt,name=type,proto3,enum=Domain_Type" json:"type,omitempty"`
// Domain value.
Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
// Attributes of this domain. May be used for filtering.
Attribute []*Domain_Attribute `protobuf:"bytes,3,rep,name=attribute,proto3" json:"attribute,omitempty"`
}
func (x *Domain) Reset() {
*x = Domain{}
if protoimpl.UnsafeEnabled {
mi := &file_v2geo_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Domain) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Domain) ProtoMessage() {}
func (x *Domain) ProtoReflect() protoreflect.Message {
mi := &file_v2geo_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Domain.ProtoReflect.Descriptor instead.
func (*Domain) Descriptor() ([]byte, []int) {
return file_v2geo_proto_rawDescGZIP(), []int{0}
}
func (x *Domain) GetType() Domain_Type {
if x != nil {
return x.Type
}
return Domain_Plain
}
func (x *Domain) GetValue() string {
if x != nil {
return x.Value
}
return ""
}
func (x *Domain) GetAttribute() []*Domain_Attribute {
if x != nil {
return x.Attribute
}
return nil
}
// IP for routing decision, in CIDR form.
type CIDR struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// IP address, should be either 4 or 16 bytes.
Ip []byte `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"`
// Number of leading ones in the network mask.
Prefix uint32 `protobuf:"varint,2,opt,name=prefix,proto3" json:"prefix,omitempty"`
}
func (x *CIDR) Reset() {
*x = CIDR{}
if protoimpl.UnsafeEnabled {
mi := &file_v2geo_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *CIDR) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CIDR) ProtoMessage() {}
func (x *CIDR) ProtoReflect() protoreflect.Message {
mi := &file_v2geo_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CIDR.ProtoReflect.Descriptor instead.
func (*CIDR) Descriptor() ([]byte, []int) {
return file_v2geo_proto_rawDescGZIP(), []int{1}
}
func (x *CIDR) GetIp() []byte {
if x != nil {
return x.Ip
}
return nil
}
func (x *CIDR) GetPrefix() uint32 {
if x != nil {
return x.Prefix
}
return 0
}
type GeoIP struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
CountryCode string `protobuf:"bytes,1,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"`
Cidr []*CIDR `protobuf:"bytes,2,rep,name=cidr,proto3" json:"cidr,omitempty"`
InverseMatch bool `protobuf:"varint,3,opt,name=inverse_match,json=inverseMatch,proto3" json:"inverse_match,omitempty"`
// resource_hash instruct simplified config converter to load domain from geo file.
ResourceHash []byte `protobuf:"bytes,4,opt,name=resource_hash,json=resourceHash,proto3" json:"resource_hash,omitempty"`
Code string `protobuf:"bytes,5,opt,name=code,proto3" json:"code,omitempty"`
}
func (x *GeoIP) Reset() {
*x = GeoIP{}
if protoimpl.UnsafeEnabled {
mi := &file_v2geo_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GeoIP) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoIP) ProtoMessage() {}
func (x *GeoIP) ProtoReflect() protoreflect.Message {
mi := &file_v2geo_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GeoIP.ProtoReflect.Descriptor instead.
func (*GeoIP) Descriptor() ([]byte, []int) {
return file_v2geo_proto_rawDescGZIP(), []int{2}
}
func (x *GeoIP) GetCountryCode() string {
if x != nil {
return x.CountryCode
}
return ""
}
func (x *GeoIP) GetCidr() []*CIDR {
if x != nil {
return x.Cidr
}
return nil
}
func (x *GeoIP) GetInverseMatch() bool {
if x != nil {
return x.InverseMatch
}
return false
}
func (x *GeoIP) GetResourceHash() []byte {
if x != nil {
return x.ResourceHash
}
return nil
}
func (x *GeoIP) GetCode() string {
if x != nil {
return x.Code
}
return ""
}
type GeoIPList struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Entry []*GeoIP `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"`
}
func (x *GeoIPList) Reset() {
*x = GeoIPList{}
if protoimpl.UnsafeEnabled {
mi := &file_v2geo_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GeoIPList) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoIPList) ProtoMessage() {}
func (x *GeoIPList) ProtoReflect() protoreflect.Message {
mi := &file_v2geo_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GeoIPList.ProtoReflect.Descriptor instead.
func (*GeoIPList) Descriptor() ([]byte, []int) {
return file_v2geo_proto_rawDescGZIP(), []int{3}
}
func (x *GeoIPList) GetEntry() []*GeoIP {
if x != nil {
return x.Entry
}
return nil
}
type GeoSite struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
CountryCode string `protobuf:"bytes,1,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"`
Domain []*Domain `protobuf:"bytes,2,rep,name=domain,proto3" json:"domain,omitempty"`
// resource_hash instruct simplified config converter to load domain from geo file.
ResourceHash []byte `protobuf:"bytes,3,opt,name=resource_hash,json=resourceHash,proto3" json:"resource_hash,omitempty"`
Code string `protobuf:"bytes,4,opt,name=code,proto3" json:"code,omitempty"`
}
func (x *GeoSite) Reset() {
*x = GeoSite{}
if protoimpl.UnsafeEnabled {
mi := &file_v2geo_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GeoSite) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoSite) ProtoMessage() {}
func (x *GeoSite) ProtoReflect() protoreflect.Message {
mi := &file_v2geo_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use MatchGeoSite.ProtoReflect.Descriptor instead.
func (*GeoSite) Descriptor() ([]byte, []int) {
return file_v2geo_proto_rawDescGZIP(), []int{4}
}
func (x *GeoSite) GetCountryCode() string {
if x != nil {
return x.CountryCode
}
return ""
}
func (x *GeoSite) GetDomain() []*Domain {
if x != nil {
return x.Domain
}
return nil
}
func (x *GeoSite) GetResourceHash() []byte {
if x != nil {
return x.ResourceHash
}
return nil
}
func (x *GeoSite) GetCode() string {
if x != nil {
return x.Code
}
return ""
}
type GeoSiteList struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Entry []*GeoSite `protobuf:"bytes,1,rep,name=entry,proto3" json:"entry,omitempty"`
}
func (x *GeoSiteList) Reset() {
*x = GeoSiteList{}
if protoimpl.UnsafeEnabled {
mi := &file_v2geo_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GeoSiteList) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GeoSiteList) ProtoMessage() {}
func (x *GeoSiteList) ProtoReflect() protoreflect.Message {
mi := &file_v2geo_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GeoSiteList.ProtoReflect.Descriptor instead.
func (*GeoSiteList) Descriptor() ([]byte, []int) {
return file_v2geo_proto_rawDescGZIP(), []int{5}
}
func (x *GeoSiteList) GetEntry() []*GeoSite {
if x != nil {
return x.Entry
}
return nil
}
type Domain_Attribute struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
// Types that are assignable to TypedValue:
//
// *Domain_Attribute_BoolValue
// *Domain_Attribute_IntValue
TypedValue isDomain_Attribute_TypedValue `protobuf_oneof:"typed_value"`
}
func (x *Domain_Attribute) Reset() {
*x = Domain_Attribute{}
if protoimpl.UnsafeEnabled {
mi := &file_v2geo_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Domain_Attribute) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Domain_Attribute) ProtoMessage() {}
func (x *Domain_Attribute) ProtoReflect() protoreflect.Message {
mi := &file_v2geo_proto_msgTypes[6]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Domain_Attribute.ProtoReflect.Descriptor instead.
func (*Domain_Attribute) Descriptor() ([]byte, []int) {
return file_v2geo_proto_rawDescGZIP(), []int{0, 0}
}
func (x *Domain_Attribute) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
func (m *Domain_Attribute) GetTypedValue() isDomain_Attribute_TypedValue {
if m != nil {
return m.TypedValue
}
return nil
}
func (x *Domain_Attribute) GetBoolValue() bool {
if x, ok := x.GetTypedValue().(*Domain_Attribute_BoolValue); ok {
return x.BoolValue
}
return false
}
func (x *Domain_Attribute) GetIntValue() int64 {
if x, ok := x.GetTypedValue().(*Domain_Attribute_IntValue); ok {
return x.IntValue
}
return 0
}
type isDomain_Attribute_TypedValue interface {
isDomain_Attribute_TypedValue()
}
type Domain_Attribute_BoolValue struct {
BoolValue bool `protobuf:"varint,2,opt,name=bool_value,json=boolValue,proto3,oneof"`
}
type Domain_Attribute_IntValue struct {
IntValue int64 `protobuf:"varint,3,opt,name=int_value,json=intValue,proto3,oneof"`
}
func (*Domain_Attribute_BoolValue) isDomain_Attribute_TypedValue() {}
func (*Domain_Attribute_IntValue) isDomain_Attribute_TypedValue() {}
var File_v2geo_proto protoreflect.FileDescriptor
var file_v2geo_proto_rawDesc = []byte{
0x0a, 0x0b, 0x76, 0x32, 0x67, 0x65, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x97, 0x02,
0x0a, 0x06, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x20, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0c, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2e,
0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61,
0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
0x12, 0x2f, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x03, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x41, 0x74, 0x74,
0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74,
0x65, 0x1a, 0x6c, 0x0a, 0x09, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x10,
0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79,
0x12, 0x1f, 0x0a, 0x0a, 0x62, 0x6f, 0x6f, 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02,
0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x09, 0x62, 0x6f, 0x6f, 0x6c, 0x56, 0x61, 0x6c, 0x75,
0x65, 0x12, 0x1d, 0x0a, 0x09, 0x69, 0x6e, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03,
0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65,
0x42, 0x0d, 0x0a, 0x0b, 0x74, 0x79, 0x70, 0x65, 0x64, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22,
0x36, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x6c, 0x61, 0x69, 0x6e,
0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x65, 0x67, 0x65, 0x78, 0x10, 0x01, 0x12, 0x0e, 0x0a,
0x0a, 0x52, 0x6f, 0x6f, 0x74, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x10, 0x02, 0x12, 0x08, 0x0a,
0x04, 0x46, 0x75, 0x6c, 0x6c, 0x10, 0x03, 0x22, 0x2e, 0x0a, 0x04, 0x43, 0x49, 0x44, 0x52, 0x12,
0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x69, 0x70, 0x12,
0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52,
0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x22, 0xa3, 0x01, 0x0a, 0x05, 0x47, 0x65, 0x6f, 0x49,
0x50, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x64,
0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79,
0x43, 0x6f, 0x64, 0x65, 0x12, 0x19, 0x0a, 0x04, 0x63, 0x69, 0x64, 0x72, 0x18, 0x02, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x05, 0x2e, 0x43, 0x49, 0x44, 0x52, 0x52, 0x04, 0x63, 0x69, 0x64, 0x72, 0x12,
0x23, 0x0a, 0x0d, 0x69, 0x6e, 0x76, 0x65, 0x72, 0x73, 0x65, 0x5f, 0x6d, 0x61, 0x74, 0x63, 0x68,
0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x69, 0x6e, 0x76, 0x65, 0x72, 0x73, 0x65, 0x4d,
0x61, 0x74, 0x63, 0x68, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x72, 0x65, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64,
0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x22, 0x29, 0x0a,
0x09, 0x47, 0x65, 0x6f, 0x49, 0x50, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x05, 0x65, 0x6e,
0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x06, 0x2e, 0x47, 0x65, 0x6f, 0x49,
0x50, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x22, 0x86, 0x01, 0x0a, 0x07, 0x47, 0x65, 0x6f,
0x53, 0x69, 0x74, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f,
0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e,
0x74, 0x72, 0x79, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1f, 0x0a, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69,
0x6e, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69, 0x6e,
0x52, 0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f,
0x75, 0x72, 0x63, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52,
0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x12, 0x0a,
0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, 0x64,
0x65, 0x22, 0x2d, 0x0a, 0x0b, 0x47, 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x4c, 0x69, 0x73, 0x74,
0x12, 0x1e, 0x0a, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x08, 0x2e, 0x47, 0x65, 0x6f, 0x53, 0x69, 0x74, 0x65, 0x52, 0x05, 0x65, 0x6e, 0x74, 0x72, 0x79,
0x42, 0x09, 0x5a, 0x07, 0x2e, 0x2f, 0x76, 0x32, 0x67, 0x65, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x33,
}
var (
file_v2geo_proto_rawDescOnce sync.Once
file_v2geo_proto_rawDescData = file_v2geo_proto_rawDesc
)
func file_v2geo_proto_rawDescGZIP() []byte {
file_v2geo_proto_rawDescOnce.Do(func() {
file_v2geo_proto_rawDescData = protoimpl.X.CompressGZIP(file_v2geo_proto_rawDescData)
})
return file_v2geo_proto_rawDescData
}
var file_v2geo_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_v2geo_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
var file_v2geo_proto_goTypes = []interface{}{
(Domain_Type)(0), // 0: Domain.Type
(*Domain)(nil), // 1: Domain
(*CIDR)(nil), // 2: CIDR
(*GeoIP)(nil), // 3: GeoIP
(*GeoIPList)(nil), // 4: GeoIPList
(*GeoSite)(nil), // 5: MatchGeoSite
(*GeoSiteList)(nil), // 6: GeoSiteList
(*Domain_Attribute)(nil), // 7: Domain.Attribute
}
var file_v2geo_proto_depIdxs = []int32{
0, // 0: Domain.type:type_name -> Domain.Type
7, // 1: Domain.attribute:type_name -> Domain.Attribute
2, // 2: GeoIP.cidr:type_name -> CIDR
3, // 3: GeoIPList.entry:type_name -> GeoIP
1, // 4: MatchGeoSite.domain:type_name -> Domain
5, // 5: GeoSiteList.entry:type_name -> MatchGeoSite
6, // [6:6] is the sub-list for method output_type
6, // [6:6] is the sub-list for method input_type
6, // [6:6] is the sub-list for extension type_name
6, // [6:6] is the sub-list for extension extendee
0, // [0:6] is the sub-list for field type_name
}
func init() { file_v2geo_proto_init() }
func file_v2geo_proto_init() {
if File_v2geo_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_v2geo_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Domain); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_v2geo_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*CIDR); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_v2geo_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GeoIP); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_v2geo_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GeoIPList); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_v2geo_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GeoSite); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_v2geo_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GeoSiteList); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_v2geo_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Domain_Attribute); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
file_v2geo_proto_msgTypes[6].OneofWrappers = []interface{}{
(*Domain_Attribute_BoolValue)(nil),
(*Domain_Attribute_IntValue)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_v2geo_proto_rawDesc,
NumEnums: 1,
NumMessages: 7,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_v2geo_proto_goTypes,
DependencyIndexes: file_v2geo_proto_depIdxs,
EnumInfos: file_v2geo_proto_enumTypes,
MessageInfos: file_v2geo_proto_msgTypes,
}.Build()
File_v2geo_proto = out.File
file_v2geo_proto_rawDesc = nil
file_v2geo_proto_goTypes = nil
file_v2geo_proto_depIdxs = nil
}
-76
View File
@@ -1,76 +0,0 @@
syntax = "proto3";
option go_package = "./v2geo";
// This file is copied from
// https://github.com/v2fly/v2ray-core/blob/master/app/router/routercommon/common.proto
// with some modifications.
// Domain for routing decision.
message Domain {
// Type of domain value.
enum Type {
// The value is used as is.
Plain = 0;
// The value is used as a regular expression.
Regex = 1;
// The value is a root domain.
RootDomain = 2;
// The value is a domain.
Full = 3;
}
// Domain matching type.
Type type = 1;
// Domain value.
string value = 2;
message Attribute {
string key = 1;
oneof typed_value {
bool bool_value = 2;
int64 int_value = 3;
}
}
// Attributes of this domain. May be used for filtering.
repeated Attribute attribute = 3;
}
// IP for routing decision, in CIDR form.
message CIDR {
// IP address, should be either 4 or 16 bytes.
bytes ip = 1;
// Number of leading ones in the network mask.
uint32 prefix = 2;
}
message GeoIP {
string country_code = 1;
repeated CIDR cidr = 2;
bool inverse_match = 3;
// resource_hash instruct simplified config converter to load domain from geo file.
bytes resource_hash = 4;
string code = 5;
}
message GeoIPList {
repeated GeoIP entry = 1;
}
message GeoSite {
string country_code = 1;
repeated Domain domain = 2;
// resource_hash instruct simplified config converter to load domain from geo file.
bytes resource_hash = 3;
string code = 4;
}
message GeoSiteList {
repeated GeoSite entry = 1;
}
-377
View File
@@ -1,377 +0,0 @@
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"
"github.com/expr-lang/expr/conf"
"github.com/expr-lang/expr/vm"
"gopkg.in/yaml.v3"
"github.com/apernet/OpenGFW/analyzer"
"github.com/apernet/OpenGFW/modifier"
"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"`
}
type ModifierEntry struct {
Name string `yaml:"name"`
Args map[string]interface{} `yaml:"args"`
}
func ExprRulesFromYAML(file string) ([]ExprRule, error) {
bs, err := os.ReadFile(file)
if err != nil {
return nil, err
}
var rules []ExprRule
err = yaml.Unmarshal(bs, &rules)
return rules, err
}
// compiledExprRule is the internal, compiled representation of an expression rule.
type compiledExprRule struct {
Name string
Action *Action // fallthrough if nil
Log bool
ModInstance modifier.Instance
Program *vm.Program
}
var _ Ruleset = (*exprRuleset)(nil)
type exprRuleset struct {
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 {
env := streamInfoToExprEnv(info)
for _, rule := range r.Rules {
v, err := vm.Run(rule.Program, env)
if err != nil {
// Log the error and continue to the next rule.
r.Logger.MatchError(info, rule.Name, err)
continue
}
if vBool, ok := v.(bool); ok && vBool {
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,
}
}
// CompileExprRules compiles a list of expression rules into a ruleset.
// It returns an error if any of the rules are invalid, or if any of the analyzers
// used by the rules are unknown (not provided in the analyzer list).
func CompileExprRules(rules []ExprRule, ans []analyzer.Analyzer, mods []modifier.Modifier, config *BuiltinConfig) (Ruleset, error) {
var compiledRules []compiledExprRule
fullAnMap := analyzersToMap(ans)
fullModMap := modifiersToMap(mods)
depAnMap := make(map[string]analyzer.Analyzer)
funcMap := buildFunctionMap(config)
// Compile all rules and build a map of analyzers that are used by the rules.
for _, rule := range rules {
if rule.Action == "" && !rule.Log {
return nil, fmt.Errorf("rule %q must have at least one of action or log", rule.Name)
}
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, 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 {
// Skip built-in analyzers & user-defined variables
if isBuiltInAnalyzer(name) || visitor.Variables[name] {
continue
}
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 != 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)
}
modInst, err := mod.New(rule.Modifier.Args)
if err != nil {
return nil, fmt.Errorf("rule %q failed to create modifier instance: %w", rule.Name, err)
}
cr.ModInstance = modInst
}
compiledRules = append(compiledRules, cr)
}
// Convert the analyzer map to a list.
var depAns []analyzer.Analyzer
for _, a := range depAnMap {
depAns = append(depAns, a)
}
return &exprRuleset{
Rules: compiledRules,
Ans: depAns,
Logger: config.Logger,
}, nil
}
func streamInfoToExprEnv(info StreamInfo) map[string]interface{} {
m := map[string]interface{}{
"id": info.ID,
"proto": info.Protocol.String(),
"ip": map[string]string{
"src": info.SrcIP.String(),
"dst": info.DstIP.String(),
},
"port": map[string]uint16{
"src": info.SrcPort,
"dst": info.DstPort,
},
}
for anName, anProps := range info.Props {
if len(anProps) != 0 {
// Ignore analyzers with empty properties
m[anName] = anProps
}
}
return m
}
func isBuiltInAnalyzer(name string) bool {
switch name {
case "id", "proto", "ip", "port":
return true
default:
return false
}
}
func actionStringToAction(action string) (Action, bool) {
switch strings.ToLower(action) {
case "allow":
return ActionAllow, true
case "block":
return ActionBlock, true
case "drop":
return ActionDrop, true
case "modify":
return ActionModify, true
default:
return ActionMaybe, false
}
}
// analyzersToMap converts a list of analyzers to a map of name -> analyzer.
// This is for easier lookup when compiling rules.
func analyzersToMap(ans []analyzer.Analyzer) map[string]analyzer.Analyzer {
anMap := make(map[string]analyzer.Analyzer)
for _, a := range ans {
anMap[a.Name()] = a
}
return anMap
}
// modifiersToMap converts a list of modifiers to a map of name -> modifier.
// This is for easier lookup when compiling rules.
func modifiersToMap(mods []modifier.Modifier) map[string]modifier.Modifier {
modMap := make(map[string]modifier.Modifier)
for _, m := range mods {
modMap[m.Name()] = m
}
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 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)),
},
},
}
}
-108
View File
@@ -1,108 +0,0 @@
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
const (
// ActionMaybe indicates that the ruleset hasn't seen anything worth blocking based on
// current information, but that may change if volatile fields change in the future.
ActionMaybe Action = iota
// ActionAllow indicates that the stream should be allowed regardless of future changes.
ActionAllow
// ActionBlock indicates that the stream should be blocked.
ActionBlock
// ActionDrop indicates that the current packet should be dropped,
// but the stream should be allowed to continue.
// Only valid for UDP streams. Equivalent to ActionBlock for TCP streams.
ActionDrop
// ActionModify indicates that the current packet should be modified,
// and the stream should be allowed to continue.
// Only valid for UDP streams. Equivalent to ActionMaybe for TCP streams.
ActionModify
)
func (a Action) String() string {
switch a {
case ActionMaybe:
return "maybe"
case ActionAllow:
return "allow"
case ActionBlock:
return "block"
case ActionDrop:
return "drop"
case ActionModify:
return "modify"
default:
return "unknown"
}
}
type Protocol int
func (p Protocol) String() string {
switch p {
case ProtocolTCP:
return "tcp"
case ProtocolUDP:
return "udp"
default:
return "unknown"
}
}
const (
ProtocolTCP Protocol = iota
ProtocolUDP
)
type StreamInfo struct {
ID int64
Protocol Protocol
SrcIP, DstIP net.IP
SrcPort, DstPort uint16
Props analyzer.CombinedPropMap
}
func (i StreamInfo) SrcString() string {
return net.JoinHostPort(i.SrcIP.String(), strconv.Itoa(int(i.SrcPort)))
}
func (i StreamInfo) DstString() string {
return net.JoinHostPort(i.DstIP.String(), strconv.Itoa(int(i.DstPort)))
}
type MatchResult struct {
Action Action
ModInstance modifier.Instance
}
type Ruleset interface {
// Analyzers returns the list of analyzers to use for a stream.
// It must be safe for concurrent use by multiple workers.
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
}
// 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 {
Logger Logger
GeoMatcher *geo.GeoMatcher
ProtectedDialContext func(ctx context.Context, network, address string) (net.Conn, error)
}