# mosproxy

源码: <https://github.com/IrineSistiana/mosproxy>

License: [GNU General Public License v3.0](https://github.com/IrineSistiana/mosproxy/blob/dev/LICENSE)

Docker image: <https://github.com/IrineSistiana/mosproxy/pkgs/container/mosproxy>

## 简介

一个 DNS 转发工具/框架。

* 支持主流 DNS 协议。(UDP/TCP/TLS/HTTPS/HTTP3/QUIC)
* 支持转发/复写/缓存 ECS (EDNS0 Client Subnet)。
* 支持上游负载均衡 (QPS 限制，健康检查，自动 fallback)。
* 可根据请求的 域名/HTTP Path/SNI/来源 IP 决定转发目的地。
* 提供 middleware 插件接口可扩展转发逻辑。
* 支持双向 TLS 验证 (mTLS) 。
* 内置 prometheus metrics 统计。

性能优化:

* 网络 IO
  * 所有出站协议会尽可能的连接/socket复用。
  * [fasthttp](https://github.com/valyala/fasthttp) 实现的高性能 HTTP/1 服务端。适合反向代理。
* 高效的域名和 IP 匹配器。规则会优化成一个二进制索引，0(log(n)) 匹配复杂度，瞬间(百纳秒级别)完成匹配。0 碎片内存，0 GC压力，更少内存占用，以 100w 条规则为例，heap object 数量 300w -> 1 。
* 缓存:
  * [otter](https://github.com/maypok86/otter) 实现的内存缓存。
  * [rueidis](https://github.com/redis/rueidis) 实现的 redis 缓存。
  * 内存和 redis 缓存可同时启用，组成多级缓存。一级内存，二级 redis。相比单 redis 缓存，因为如果一级内存缓存命中则无需再请求 redis，能大幅降低 mosproxy (约 30%) 和 redis (约 95%) 负载。
  * 支持基于物理地域的缓存 (geo based cache)。可以让相同物理地域(比如，相同省市和运营商)的客户共享缓存。相比传统按照 ECS 网段缓存方案，可将一个地区碎片的 IP 段聚合为一个缓存，提高缓存效率。
  * 缓存数据使用 [S2 压缩](https://github.com/klauspost/compress/tree/master/s2#s2-compression) 。降低内存占用和约 90% heap object 数量 (n 条缓存 heap object 数量 \~20n -> 2n)。
* 1vCPU 可承受约 5w/qps 请求量。

## 命令

`mosproxy router -c config.yaml`

### proxy

如果仅需要简单的将所有请求转发至上游，无需复杂的路由逻辑，可以使用 proxy 子命令在命令行直接启动一个转发服务，无需配置文件。

<pre><code><strong># 示例: 转发至 Google DoH
</strong>mosproxy proxy -l 0.0.0.0:5353 -u https://dns.google/dns-query

# 更复杂的示例: 转发至 8.8.8.8，如果不可用，自动使用 1.1.1.1 做 fallback ，
# 缓存大小 1M，转发 ECS。
mosproxy proxy -l 0.0.0.0:5353 --cache 1048576 -u 8.8.8.8 -u 1.1.1.1 --upstream-lb fall_through --ecs
</code></pre>

<details>

<summary>proxy 子命令帮助 (可能不是最新，以 <code>mosproxy proxy -h</code> 命令输出为准)</summary>

```
Start a dns proxy service via command line. e.g:
Proxy from 0.0.0.0:5353 to Google DoH.
        proxy -l 0.0.0.0:5353 -u https://dns.google/dns-query
Proxy to Cloudflare DNS, send client subnet to upstreams, and use Google DNS as fallback.
        proxy -l 0.0.0.0:5353 -u 1.1.1.1 -u 8.8.8.8 --upstream-lb fall_through --ecs

Usage:
   proxy -l listen_addr -u upstream_addr [-u upstream_addr]... [flags]

Flags:
  -l, --listen string              Listening address.
  -p, --protocol string            Listening protocol.
                                   One of [udp|tcp|tls|http|fasthttp|https|quic].
                                   Default is listening both udp and tcp.
      --tls-cert string            Path to a file of PEM certificate.
      --tls-key string             Path to a file of PEM private key.
      --http-path string           Http path of DoH server.
  -u, --upstream strings           Upstream addr (can be specified multiple times).
                                   Format: [protocol://]host[:port][/path][?arg_key=arg_value][&arg_key=arg_value]...
                                   protocol can be one of: [udp|tcp[+pipeline]|tls[+pipeline]|https|h3|quic]
                                   e.g.: tcp://8.8.8.8:53, https://dns.google/dns-query?dial_addr=8.8.8.8&tag=google_doh
                                   Supported arg_key:
                                        tag: Tag name for this upstream, useful for logging.
                                        dial_addr: Specifying the IP address of the host.
                                                (Eliminate the need for host name resolution and establish connections faster)
                                        weight: Weight of this upstream. (See --upstream-lb flag)
                                        qps: QPS limit of this upstream. (See --upstream-lb flag, fall_through mode)
                                        max_fails: If the upstream failed max_fails times continuously, mark it as offline. (default 5)
                                                (See --upstream-lb flag)
                                        insecure: [true] Disable TLS server verification.
      --upstream-lb string         Upstream load balance mode:
                                        random: Use random online upstream.
                                        fall_through: Fallback to next online upstream if previous one was offline or reached QPS limit.
                                                Note: Upstream weights are ignored in this mode.
                                        qname_hash: Queries with same domain name will use the same online upstream.
                                        client_ip_hash: Queries from same ip will use the same online upstream. (default "random")
      --ecs                        Send EDNS0 Client Subnet (ECS) to upstream.
      --ecs-ip-zone string         Path to ecs ip zone file.
      --cache int                  Memory cache size in bytes (0: disable memory cache). (default 4194304)
      --cache-redis string         Redis cache backend address.
                                   If memory and redis cache are both enabled, redis will work as a second level cache.
                                   Format:
                                        redis://<user>:<password>@<host>:<port>/<db_number>?addr=<host2>:<port2>&addr=<host3>:<port3>
                                        unix://<user>:<password>@</path/to/redis.sock>?db=<db_number>
      --cache-optimistic-ttl int   If > 0, enable optimistic cache.
      --access-log                 Print all queries to log.
      --api string                 API server listening address.
  -h, --help                       help for proxy
```

</details>

## 配置

<pre class="language-yaml" data-line-numbers data-full-width="false"><code class="lang-yaml">log:
  # 是否打印请求信息。(请求的来源，域名，类型，应答 rcode，延时等...)
  queries: false
  # 是否打印**收到和发送的所有** DNS 数据包。会打印大量信息。调试用。
  trace_msgs: false

api:
  addr: "127.0.0.1:8888" # api 监听地址。
  
servers:  # 入站
    # (必需) 协议，支持: udp/tcp/tls/http/fasthttp/https/http3/quic
    #  http: go 标准库实现的 http/1，h2c 服务端。
    #  fasthttp: fasthttp 实现的 http/1 服务端，比 go 标准库快很多。
  - protocol: "udp" 
<strong>    # (必需) 监听地址。基于 TCP 的协议支持用 "@" 开头监听 abstract unix socket。
</strong>    listen: "127.0.0.1:5353"
    idle_timeout: 0 # 空连接超时。单位s。默认 tcp/tls 10，其他 30。一般不需要改。
    udp: # udp 相关设置
      # 仅 linux。
      # 如果服务器有多个 IP 地址并且没有指定监听地址时 (服务器客户端之间存在多个路由可能),
      # 启用该选项可让 linux 内核发送应答时，按照请求的来源路径原路返回。
      multi_routes: false
    tcp:  # tcp 相关设置。(tcp/tls)
      # 单条连接最大并发处理请求数。默认 100 。超出的请求会排队。
      max_concurrent_queries: 100
    tls: # tls 设置。(tls/https/http3/quic)
      cert: "" # pem 证书。
      key: "" # pem 私钥。
      ca: "" # pem ca 证书。
      insecure_skip_verify: false # 客户端禁用服务器证书验证。(服务端无用)
      verify_client_cert: false # 服务端启用客户端证书验证。(mTLS)
    http: # http 设置。(http/fasthttp/http3/https)
      path: "" # 路径。为空不会检查请求 url 路径。
      client_addr_header: "X-Forwarded-For" # 从 这个 header 获取客户端 IP。
    quic: # quic 设置。(http3/quic)
      max_streams: 100 # 单个 quic 连接最大并发请求数。默认 100 。
    socket: # linux so 设置。
      so_reuseport: false
      so_rcvbuf: 0
      so_sndbuf: 0
      so_mark: 0
      so_bindtodevice: ""
<strong>     
</strong>cache: # 缓存。
  # 内存大小，单位 byte。
  # 默认: 0 = 无内存缓存。
  mem_size: 1048576 
  # redis 缓存。
  # redis://&#x3C;user>:&#x3C;password>@&#x3C;host>:&#x3C;port>/&#x3C;db_number>
  # redis://&#x3C;user>:&#x3C;password>@&#x3C;host>:&#x3C;port>?addr=&#x3C;host2>:&#x3C;port2>&#x26;addr=&#x3C;host3>:&#x3C;port3>
  # unix://&#x3C;user>:&#x3C;password>@&#x3C;/path/to/redis.sock>?db=&#x3C;db_number>
  # 如果和内存缓存同时启用，则 redis 为二级缓存。先查找内存缓存，如果没有命中，
  # 则查找 redis，如果 redis 缓存命中，则会更新内存缓存。
  # 自带 health (ping) check，如果 redis 服务端失联会自动禁用 redis 缓存，
  # 直到重新连接。检查间隔/反应时间为 1s 。
  redis: ""
  # 最大 ttl。默认 10min。
  maximum_ttl: 600
  # 预取阈值。(0~1)。当 缓存的当前TTL &#x3C; threshold*原TTL 会触发*被动预取*。
  # 被动预取: 当缓存命中时才会检查 TTL 并决定是否预取。不产生格外流量和负载。
  # 默认 0.25
  prefetch_threshold: 0.25
  # 乐观缓存 ttl。
  # 即使应答缓存过期，仍在继续保留 optimistic_ttl 秒，后续请求会返回过期应答，
  # 但会立即触发预取。
  # 默认 0 无乐观缓存。
  optimistic_ttl: 0
 
# ECS 设置
# ECS 来源优先级: 客户端 IP &#x3C; 请求自带 ECS &#x3C; overwrite
# ECS 处理逻辑: 
#   1. 解析 客户端 IP 或 "请求自带 ECS"，作为 "即将转发至上游的 ECS"
#   2. "即将转发至上游的 ECS" 匹配 zone，确定请求所属 zone
#   3. zone 匹配 overwrite，复写 "即将转发至上游的 ECS"
ecs:
  enabled: false # 启用 ECS 支持。将发送 ECS 至上游。默认发送客户端 IP。
  forward: false # 是否转发请求自带的 ECS。
  ip_zone: ""        # ip zone 文件路径。启用基于 zone 的缓存。
  zone_overwrite: "" # zone overwrite 文件路径。
        
upstreams: # 出站。
  - tag: "google" # (必需)标签，需唯一。
    # (必需)地址 [protocol://]host[:port][/path]
    # protocol 为 udp/tcp[+pipeline]/tls[+pipeline]/https/h3/quic
    # 提示: pipeline 是 RFC 7766 定义的新的基于 TCP 的传输模式，连接利用率更高，但兼容性差，
    #   **必需**确定服务器支持才能使用。如不确定，建议考虑更现代的 DoH/QUIC 等协议。
    # e.g.:
    #   "8.8.8.8" 
    #   "tls://8.8.8.8"
    #   "h3://dns.google/dns-query" 
    addr: "https://dns.google/dns-query" 
    # 手动指定 IP 协议层面建立连接时使用的地址。端口号可选。
    # 一般用于手动指定 addr 里域名地址的 IP。建立连接不再需要每次解析 addr 域名，
    # 能更快连接到上游。
    dial_addr: "8.8.8.8"    
    tls: # tls 设置。见上。
      cert: ""
      key: ""
      ca: ""
      insecure_skip_verify: false
      verify_client_cert: false # (客户端无用)
    socket: # linux so 设置。
      so_reuseport: false
      so_rcvbuf: 0
      so_sndbuf: 0
      so_mark: 0
      so_bindtodevice: ""
    health_check: # health check 设定。另见下文 负载均衡。
      # 如果 max_fails > 0，启动 health check。
      # 如果上游连续失败 max_fails 次，标记为 offline 。
      # 被标记 offline 的上游将不再转发任何请求。
      # 默认 0。如需 health check，建议值 5~10。
      max_fails: 0
      # ping 测试(最大)间隔。单位 s。默认 120 。
      # 上游 offline 后，会进行 ping 测试。ping 成功后上游将重新标记为 online 。
      # 如果上游无请求 (使用负载均衡将请求 fallback 至其他上游)，ping 间隔会逐渐
      # 递增: 1,2,4,...,2^n,ping_interval 。
      # 如果上游仍然持续有请求 (直接使用该上游)，上游会持续进行 ping 测试，间隔会缩短至约 5s。
      # 现有 ping 逻辑足以将服务不可用时间控制在几秒内。不建议更改该值。没必要。
      ping_interval: 120

load_balancers: # 负载均衡组
  - tag: "lb1" # (必需) 负载均衡组标签，需唯一。
    # 方案:
    # random: (默认) 随机选取后端。支持 weight 值。
    # qname_hash: 随机选取后端，但相同的 请求域名 将分配到相同的后端。支持 weight 值。
    # client_ip_hash: 随机选取后端，但相同的 客户端 IP 将分配到相同的后端。支持 weight 值。
    # fall_through: 顺序选取后端。支持 qps 值。一后端如果达到 qps 限制会尝试
    #   使用下一个后端。
    method: "random"
    # 后端组。如果后端设置了 health check 。标记为 offline 的后端
    # 会被自动屏蔽 (请求不再分配给该后端)，online 后会恢复。
    backends:
      - tag: "google" # (必需) upstreams 的 tag 。
        weight: 1 # 权重。默认 1 。
        qps: 0    # QPS 限制。默认 0 无限制。 
 
domain_sets: # 域名表。
  - tag: "" # (必需)标签，需唯一。
    files: [] # 文件(s)。

rules: # 请求转发规则
  # 多个匹配之间的关系为 AND，有一个匹配不满足，规则就会被跳过。
  # 如果没有规则生效，会返回 REFUSE。
  # 匹配:
  - reverse: false # 规则取反
    domain: "" # 域名集的 tag。
    server: "" # server 的 tag。
    server_name: "" # TLS 连接的 SNI。
    path: "" # http 的 path，如果 "/" 结尾，则会匹配所有子 path。
    client_ip: # 客户端 ip。可以是单个 ip，cidr，ip 范围。
      - "192.168.1.100"
      - "192.168.1.0/24"
      - "192.168.1.100-192.168.1.255"
    # 处理:
    reject: 0 # > 0 会用这个 rcode 拒绝请求
    forward: "google" # 出站的 tag。可以是 upstream，可以是 load_balancer。

middleware: # 中间件设置
  - type: "" # 类型
  # ...      # 中间件参数

addons: {} # 保留。目前没用。
</code></pre>

### ENV

* `MOSPROXY_JSONLOGGER=true` 可以让日志以纯 json 形式输出。性能更好并且便于日志处理。

### API

* `GET /metrics`: prometheus metrics
* `GET /ctl/reload`: 重载资源文件（ecs ip zone,ecs zone overwrite, domain set）。如果成功，返回 `200` 。否则 `500` 。只有全部文件成功重载才会应用更改，不会出现重载一半的情况。

### 域名表文件

支持 3 种匹配规则。

格式: `[domain:|full:|regexp:]<规则> # 注释`

* `domain:` (默认) 域匹配。匹配这个域名和其子域。
* `full:` 全匹配。
* `regexp:` 正则匹配。

域匹配和全匹配非常高效。所有规则会优化成一个二进制索引，0(log(n)) 匹配复杂度。以载入 [Cloudflare radar 全球热门 100w 域名](https://radar.cloudflare.com/domains) 为例，文件大小 14M，载入用时约 1s，载入后二进制索引约 40M，匹配用时约 100ns。

正则匹配无法优化，是遍历匹配。会在 域匹配和全匹配 之后匹配。

### ECS IP Zone 文件

格式: `<起始IP(包含)>,<结束IP(包含)>,<zone标识> # 注释`

IP 段不能有重叠。`<zone标识>` 不要过长，该字符串会成为缓存的 key 的一部分。

示例:&#x20;

```
1.1.1.0,1.1.1.255,zone_name1 # comment
1.1.2.0,1.1.2.255,zone_name1
192.168.0.0,192.168.255.255,zone_name2
::1,::ffff,zone_name3
```

载入时整个文件会优化成一个二进制索引，0(log(n)) 匹配复杂度。100w 条规则索引约占 25M。

IP Zone 会匹配请求发往上游的 ECS 地址，并用 `<zone标识>` 标记请求。具有相同 `<zone标识>` 的请求将共享缓存。

### ECS Zone Overwrite 文件

格式: `<zone标识>,[<CIDR>|"-"] # 注释`

示例:

```
zone_name1,1.1.1.0/24 # comment
zone_name2,::1/128
zone_name3,-
,-
```

`<zone标识>` 不可重复。 空 `<zone标识>` 可匹配没有 `<zone标识>` 的请求。`CIDR` 的后缀代表 ECS 的 mask。`-` 代表不转发 ECS。

具有 `<zone标识>` 请求转发至上游时将会附带指定的 ECS。

## 其他

* golang 支持的所有平台理论上都能编译并且能用，但本项目只针对 linux 开发/优化。

### Middleware 自定义请求处理逻辑

接口声明见 <https://github.com/IrineSistiana/mosproxy/blob/dev/app/router/router_middleware.go>

示例 <https://github.com/IrineSistiana/mosproxy/blob/dev/app/router/middleware/limit/limit.go>

## 版本状态

alpha


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://irine-sistiana.gitbook.io/mosproxy-wiki/mosproxy.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
