本文最后更新于 2024-05-22,

若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益, 请联系我 删除。

本站只有Telegram群组为唯一交流群组, 点击加入

文章内容有误?申请成为本站文章修订者或作者? 向站长提出申请

OpenGFW 是一个 Linux 上灵活、易用、开源的 DIY GFW 实现,并且在许多方面比真正的 GFW 更强大。为何让那些掌权者独享乐趣?是时候把权力归还给人民,人人有墙建了。立即安装可以部署在家用路由器上的网络主权 - 你也能是老大哥。

1716318156627.webp

项目地址: https://github.com/apernet/OpenGFW/tree/master

项目文档: https://gfw.dev/

Telegram 群组: https://t.me/OpGFW

功能

  • 完整的 IP/TCP 重组,各种协议解析器
  • 同等支持 IPv4 和 IPv6
  • 基于流的多核负载均衡
  • 连接 offloading
  • 基于 expr 的强大规则引擎
  • 规则可以热重载 (发送 SIGHUP 信号)
  • 灵活的协议解析和修改框架
  • 可扩展的 IO 实现 (目前只有 NFQueue)
  • [开发中] Web UI

使用场景

  • 广告拦截
  • 家长控制
  • 恶意软件防护
  • VPN/代理服务滥用防护
  • 流量分析 (纯日志模式)
  • 助你实现你的独裁野心

对于我来说只是想让使用这台服务器代理的人不能访问一些乱七八糟的网站罢了(服务器借给别人作为代理服务器了)

以下是官方文档,使用其实也是非常简单,你只需要去github下载编译好的二进制文件(你也可以自己用golang来编译)然后只需要写好 config.yaml配置文件和 rules.yaml规则文件即可。需要限制的协议和网站主要也是在rules.yaml里面编写

构建与运行

构建

export CGO_ENABLED=0
go build

运行

export OPENGFW_LOG_LEVEL=debug
./OpenGFW -c config.yaml rules.yaml

其中 config.yaml 是配置文件,rules.yaml 是规则文件。

pcap 文件回放模式

./OpenGFW -p your.pcap -c config.yaml rules.yaml

在 pcap 模式下,规则中的所有动作都没有任何效果。此模式主要用于调试。

OpenWrt

OpenGFW 在 OpenWrt 23.05 上测试可用(其他版本应该也可以,暂时未经验证)。

安装依赖:

opkg install nftables kmod-nft-queue kmod-nf-conntrack-netlink

样例配置

io:
  queueSize: 1024
  rcvBuf: 4194304
  sndBuf: 4194304
  local: true 
  rst: false 

workers:
  count: 4 
  queueSize: 64
  tcpMaxBufferedPagesTotal: 65536
  tcpMaxBufferedPagesPerConn: 16
  tcpTimeout: 10m 
  udpMaxStreams: 4096

# 指定的 geoip/geosite 档案路径
# 如果未设置,将自动从 https://github.com/Loyalsoldier/v2ray-rules-dat 下载
# ruleset:
#   geoip: geoip.dat
#   geosite: geosite.dat

replay:
  realtime: false 

规则文件

规则文件是规则的集合,用于定义对匹配到的连接进行何种操作。一个规则的基本格式如下:

- name: block v2ex https
  action: block
  log: true
  expr: string(tls?.req?.sni) endsWith "v2ex.com"

其中 name 是规则的名称,action 是进行的动作,log 是是否记录日志,expr 是匹配表达式。actionlog 中必须至少存在一个。

表达式采用的是 Expr 语言,语法请参考其 语法定义

可供匹配的数据来自分析器,请参考 分析器文档

支持的动作 (action)

动作 TCP UDP
allow 放行连接,不再处理后续的包。 放行连接,不再处理后续的包。
block 阻断连接,不再处理后续的包。 阻断连接,不再处理后续的包。
drop 效果同 block 丢弃触发规则的包,但继续处理同一流中的后续包。
modify 效果同 allow 用指定的修改器修改触发规则的包,然后继续处理同一流中的后续包。

规则样例

记录 SNI 中包含特定关键字的连接

- name: log horny people
  log: true
  expr: let sni = string(tls?.req?.sni); sni contains "porn" || sni contains "hentai"

阻断访问 v2ex.com 的 HTTP/HTTPS/QUIC 连接

- name: block v2ex http
  action: block
  expr: string(http?.req?.headers?.host) endsWith "v2ex.com"

- name: block v2ex https
  action: block
  expr: string(tls?.req?.sni) endsWith "v2ex.com"

- name: block v2ex quic
  action: block
  expr: string(quic?.req?.sni) endsWith "v2ex.com"

阻断并记录 Shadowsocks, VMess, Trojan 连接

- name: block shadowsocks and vmess
  action: block
  log: true
  expr: fet != nil && fet.yes

- name: block trojan
  action: block
  log: true
  expr: trojan != nil && trojan.yes

将 v2ex.com 域名 DNS 污染到 0.0.0.0 和 ::

- name: v2ex dns poisoning
  action: modify
  modifier:
    name: dns
    args:
      a: "0.0.0.0"
      aaaa: "::"
  expr: dns != nil && dns.qr && any(dns.questions, {.name endsWith "v2ex.com"})

阻断 SOCKS 代理访问 google.com:80

- name: block google socks
  action: block
  expr: string(socks?.req?.addr) endsWith "google.com" && socks?.req?.port == 80

根据握手响应阻断 WireGuard

- name: block wireguard by handshake response
  action: drop
  expr: wireguard?.handshake_response?.receiver_index_matched == true

根据 GeoSite 数据库阻断 Bilibili 的所有域名

- name: block bilibili geosite
  action: block
  expr: geosite(string(tls?.req?.sni), "bilibili")

根据 GeoIP 数据库阻断所有目标 IP 为中国的连接

- name: block CN geoip
  action: block
  expr: geoip(string(ip.dst), "cn")

根据 CIDR 阻断特定 IP 段的连接

- name: block cidr
  action: block
  expr: cidr(string(ip.dst), "192.168.0.0/16")

阻断 Xray Reality/ShadowTLS 连接

原理:Xray Reality/ShadowTLS 等协议的 TLS 握手是 "盗用" 其他正常网站的,但连接的目标 IP 是代理服务器而并非这些网站的真正 IP。因此可以通过 DNS 查询 SNI 域名解析到的地址,如果连接的目标 IP 不在这些地址中,则阻断连接。

Warning

为了尽量降低误伤,下面提供的规则中除了使用系统默认 DNS 外还通过另外两个服务器进行查询,只要目标 IP 在任何结果中出现则放行。请根据实际网络环境对规则进行调整。如果域名无法解析,则 lookup 函数会出错导致此条规则出错,也不会阻断连接。

- name: SNI mismatch
  action: block
  expr: tls?.req?.sni != nil && ip.dst not in concat(lookup(tls.req.sni), lookup(t

分析器

分析器是 OpenGFW 的重要组件之一,作用是分析连接,检查是否是支持的协议,并从该连接中提取信息,作为提供给规则引擎的属性,以便与用户提供的规则进行匹配。OpenGFW 会自动分析提供的规则中引用了哪些分析器,仅启用需要的分析器。

本文档列出了每个分析器提供的属性。所有列出的属性都可以在规则中使用。

Tip

许多分析器并非一次性提供全部属性,而是会随着传输进行逐步增加/更新字段。例如,当一个 HTTP 连接发送了请求但还没有收到响应时,HTTP 分析器只会有 req 部分而没有 resp。每个连接会在任何属性发生变化时匹配一次规则。规则需要能正确处理需要的字段为 nil 的情况。(如 http != nil && http.resp != nil 或利用 ? 操作符 http?.resp

内置

每个连接都会有以下内置属性:

{
  "id": 123456,
  "proto": "tcp", // tcp, udp
  "ip": {
    "src": "123.123.123.123",
    "dst": "7.8.9.0"
  },
  "port": {
    "src": 1234,
    "dst": 80
  }
}

阻止到 8.8.8.8 的 UDP 流量:

- name: Block 8.8.8.8 UDP
  action: block
  expr: ip.dst == "8.8.8.8" && proto == "udp"

DNS (TCP & UDP)

对于请求:

{
  "dns": {
    "aa": false,
    "id": 41953,
    "opcode": 0,
    "qr": false,
    "questions": [
      {
        "class": 1,
        "name": "www.google.com",
        "type": 1
      }
    ],
    "ra": false,
    "rcode": 0,
    "rd": true,
    "tc": false,
    "z": 0
  }
}

对于相应:

{
  "dns": {
    "aa": false,
    "answers": [
      {
        "a": "142.251.32.36",
        "class": 1,
        "name": "www.google.com",
        "ttl": 255,
        "type": 1
      }
    ],
    "id": 41953,
    "opcode": 0,
    "qr": true,
    "questions": [
      {
        "class": 1,
        "name": "www.google.com",
        "type": 1
      }
    ],
    "ra": true,
    "rcode": 0,
    "rd": true,
    "tc": false,
    "z": 0
  }
}

丢弃 www.google.com 的 DNS 请求:

- name: Block Google DNS
  action: drop
  expr: dns != nil && !dns.qr && any(dns.questions, {.name == "www.google.com"})

FET (全加密连接)

请阅读论文 https://www.usenix.org/system/files/usenixsecurity23-wu-mingshi.pdf 以获取更多信息。

{
  "fet": {
    "ex1": 3.7560976,
    "ex2": true,
    "ex3": 0.9512195,
    "ex4": 39,
    "ex5": false,
    "yes": false
  }
}

屏蔽全加密连接:

- name: Block suspicious proxy traffic
  action: block
  expr: fet != nil && fet.yes

HTTP

{
  "http": {
    "req": {
      "headers": {
        "accept": "*/*",
        "host": "ipinfo.io",
        "user-agent": "curl/7.81.0"
      },
      "method": "GET",
      "path": "/",
      "version": "HTTP/1.1"
    },
    "resp": {
      "headers": {
        "access-control-allow-origin": "*",
        "content-length": "333",
        "content-type": "application/json; charset=utf-8",
        "date": "Wed, 24 Jan 2024 05:41:44 GMT",
        "referrer-policy": "strict-origin-when-cross-origin",
        "server": "nginx/1.24.0",
        "strict-transport-security": "max-age=2592000; includeSubDomains",
        "via": "1.1 google",
        "x-content-type-options": "nosniff",
        "x-envoy-upstream-service-time": "2",
        "x-frame-options": "SAMEORIGIN",
        "x-xss-protection": "1; mode=block"
      },
      "status": 200,
      "version": "HTTP/1.1"
    }
  }
}

屏蔽对 ipinfo.io 的 HTTP 请求:

- name: Block ipinfo.io HTTP
  action: block
  expr: http != nil && http.req != nil && http.req.headers != nil && http.req.headers.host == "ipinfo.io"

SSH

{
  "ssh": {
    "server": {
      "comments": "Ubuntu-3ubuntu0.6",
      "protocol": "2.0",
      "software": "OpenSSH_8.9p1"
    },
    "client": {
      "comments": "IMHACKER",
      "protocol": "2.0",
      "software": "OpenSSH_8.9p1"
    }
  }
}

屏蔽 SSH:

- name: Block SSH
  action: block
  expr: ssh != nil

TLS

{
  "tls": {
    "req": {
      "alpn": ["h2", "http/1.1"],
      "ciphers": [
        4866, 4867, 4865, 49196, 49200, 159, 52393, 52392, 52394, 49195, 49199,
        158, 49188, 49192, 107, 49187, 49191, 103, 49162, 49172, 57, 49161,
        49171, 51, 157, 156, 61, 60, 53, 47, 255
      ],
      "compression": "AA==",
      "random": "UqfPi+EmtMgusILrKcELvVWwpOdPSM/My09nPXl84dg=",
      "session": "jCTrpAzHpwrfuYdYx4FEjZwbcQxCuZ52HGIoOcbw1vA=",
      "sni": "ipinfo.io",
      "supported_versions": [772, 771],
      "version": 771,
      "ech": true
    },
    "resp": {
      "cipher": 4866,
      "compression": 0,
      "random": "R/Cy1m9pktuBMZQIHahD8Y83UWPRf8j8luwNQep9yJI=",
      "session": "jCTrpAzHpwrfuYdYx4FEjZwbcQxCuZ52HGIoOcbw1vA=",
      "supported_versions": 772,
      "version": 771
    }
  }
}

屏蔽对 ipinfo.io 的 TLS 请求:

- name: Block ipinfo.io TLS
  action: block
  expr: tls != nil && tls.req != nil && tls.req.sni == "ipinfo.io"

QUIC

QUIC 解析器的格式与 TLS 一样,但是目前只支持请求 (req) 部分:

{
  "quic": {
    "req": {
      "alpn": ["h3"],
      "ciphers": [4865, 4866, 4867],
      "compression": "AA==",
      "ech": true,
      "random": "FUYLceFReLJl9dRQ0HAus7fi2ZGuKIAApF4keeUqg00=",
      "session": "",
      "sni": "quic.rocks",
      "supported_versions": [772],
      "version": 771
    }
  }
}

屏蔽对 quic.rocks 的 QUIC 请求:

- name: Block quic.rocks QUIC
  action: block
  expr: quic != nil && quic.req != nil && quic.req.sni == "quic.rocks"

Trojan (代理协议)

{
  "trojan": {
    "seq": [682, 4540, 1310, 1031],
    "yes": true
  }
}

屏蔽 Trojan 连接:

- name: Block Trojan
  action: block
  expr: trojan != nil && trojan.yes

Warning

Trojan 检测目前依赖基于流量特征的启发式算法,并不保证完全准确。有大概 0.6% 的假阳性率和 10% 的假阴性率。像上述规则这样直接屏蔽所有疑似 Trojan 的连接可能导致误伤正常 TLS 连接。目前建议使用日志模式记录下 IP 地址,进行额外的人工审查。

SOCKS

SOCKS4:

{
  "socks": {
    "version": 4,
    "req": {
      "cmd": 1,
      "addr_type": 1, // same as socks5
      "addr": "1.1.1.1",
      // for socks4a
      // "addr_type": 3,
      // "addr": "google.com",
      "port": 443,
      "auth": {
        "user_id": "user"
      }
    },
    "resp": {
      "rep": 90, // 0x5A(90) granted
      "addr_type": 1,
      "addr": "1.1.1.1",
      "port": 443
    }
  }
}

SOCKS5 无验证:

{
  "socks": {
    "version": 5,
    "req": {
      "cmd": 1, // 0x01: connect, 0x02: bind, 0x03: udp
      "addr_type": 3, // 0x01: ipv4, 0x03: domain, 0x04: ipv6
      "addr": "google.com",
      "port": 80,
      "auth": {
        "method": 0 // 0x00: no auth, 0x02: username/password
      }
    },
    "resp": {
      "rep": 0, // 0x00: success
      "addr_type": 1, // 0x01: ipv4, 0x03: domain, 0x04: ipv6
      "addr": "198.18.1.31",
      "port": 80,
      "auth": {
        "method": 0 // 0x00: no auth, 0x02: username/password
      }
    }
  }
}

SOCKS5 带验证:

{
  "socks": {
    "version": 5,
    "req": {
      "cmd": 1, // 0x01: connect, 0x02: bind, 0x03: udp
      "addr_type": 3, // 0x01: ipv4, 0x03: domain, 0x04: ipv6
      "addr": "google.com",
      "port": 80,
      "auth": {
        "method": 2, // 0x00: no auth, 0x02: username/password
        "username": "user",
        "password": "pass"
      }
    },
    "resp": {
      "rep": 0, // 0x00: success
      "addr_type": 1, // 0x01: ipv4, 0x03: domain, 0x04: ipv6
      "addr": "198.18.1.31",
      "port": 80,
      "auth": {
        "method": 2, // 0x00: no auth, 0x02: username/password
        "status": 0 // 0x00: success, 0x01: failure
      }
    }
  }
}

屏蔽到 google.com:80 的 SOCKS 请求,以及带有用户名 foobar 的 SOCKS 请求:

- name: Block SOCKS google.com:80
  action: block
  expr: string(socks?.req?.addr) endsWith "google.com" && socks?.req?.port == 80

- name: Block SOCKS user foobar
  action: block
  expr: socks?.req?.auth?.method == 2 && socks?.req?.auth?.username == "foobar"

WireGuard

{
  "wireguard": {
    "message_type": 1, // 0x1: handshake_initiation, 0x2: handshake_response, 0x3: packet_cookie_reply, 0x4: packet_data
    "handshake_initiation": {
      "sender_index": 0x12345678
    },
    "handshake_response": {
      "sender_index": 0x12345678,
      "receiver_index": 0x87654321,
      "receiver_index_matched": true
    },
    "packet_data": {
      "receiver_index": 0x12345678,
      "receiver_index_matched": true
    },
    "packet_cookie_reply": {
      "receiver_index": 0x12345678,
      "receiver_index_matched": true
    }
  }
}

屏蔽 WireGuard:

# 误伤: 高
- name: Block all WireGuard-like traffic
  action: block
  expr: wireguard != nil

# 误伤: 中
- name: Block WireGuard by handshake_initiation
  action: drop
  expr: wireguard?.handshake_initiation != nil

# 误伤: 低
- name: Block WireGuard by handshake_response
  action: drop
  expr: wireguard?.handshake_response?.receiver_index_matched == true

# 误伤: 极低
- name: Block WireGuard by packet_data
  action: block
  expr: wireguard?.packet_data?.receiver_index_matched == true

OpenVPN

OpenVPN 分析器对于 TCP 和 UDP 模式都可以检测。注意如果你的 OpenVPN 配置包含了 tls-crypt 则不能正常工作,因为在这种情况下连接会被一个预共享密钥加密,成为全加密连接。

{
  "openvpn": {
    "rx_pkt_cnt": 88,
    "tx_pkt_cnt": 23
  }
}

屏蔽 OpenVPN(如果检测到超过 50 个 OpenVPN 包,防止误伤):

- name: Block OpenVPN
  action: block
  expr: openvpn != nil && openvpn.rx_pkt_cnt + openvpn.tx_pkt_cnt > 50

内置函数

除了 expr 本身内置的函数以外,我们还提供了一些额外的内置函数,可以在表达式中使用:

cidr

cidr(ip: string, cidr: string) -> bool

检查一个 IP 地址是否在一个 CIDR 范围内。示例:

- name: block cidr
  action: block
  expr: cidr(string(ip.dst), "192.168.0.0/16")

geoip

geoip(ip: string, country: string) -> bool

检查一个 IP 地址是否来自一个国家,使用来自 https://github.com/Loyalsoldier/v2ray-rules-dat 的数据。

示例:

- name: block CN geoip
  action: block
  expr: geoip(string(ip.dst), "cn")

geosite

geosite(domain: string, category: string) -> bool

检查一个域名是否属于一个特定的类别,使用来自 https://github.com/Loyalsoldier/v2ray-rules-dat 的数据。

示例:

- name: block bilibili geosite
  action: block
  expr: geosite(string(tls?.req?.sni), "bilibili")

lookup

lookup(domain: string) -> list<string>
lookup(domain: string, server: string) -> list<string>

对指定的域名进行 DNS 查询,返回 IP 地址列表(同时包括 A 和 AAAA 记录)。如果未指定服务器地址,则使用系统默认的 DNS。此操作使用标准 DNS 协议(不是 DNS over TLS、DNS over HTTPS 等),且服务器地址必须同时包括 IP 地址和端口(如 8.8.8.8:53)。

示例:

- name: SNI mismatch
  log: true
  expr: tls?.req?.sni != nil && ip.dst not in lookup(tls.req.sni)