Compare commits

...

12 Commits

Author SHA1 Message Date
世界
6021715674
documentation: Bump version 2024-02-10 22:42:27 +08:00
世界
0520bfdd04
Update quic-go to v0.41.0 2024-02-10 22:40:15 +08:00
世界
bc644715c9
documentation: Add description for client_subnet options 2024-02-10 22:40:15 +08:00
世界
d31c60a37c
documentation: Add description for address filter DNS rules 2024-02-10 22:40:15 +08:00
世界
ac8232afdd
Update dependencies 2024-02-10 22:40:15 +08:00
世界
01f9749dd6
Fixed order for Clash modes 2024-02-10 22:40:15 +08:00
世界
801b8ae8e3
Add support for client-subnet DNS options 2024-02-10 22:40:15 +08:00
世界
3daa477514
Add address filter support for DNS rules 2024-02-10 22:39:13 +08:00
世界
f77e01e93d
Fix timezone for Android and iOS 2024-02-10 22:39:13 +08:00
世界
1ea44d1e69
Improve domain suffix match behavior
For historical reasons, sing-box's `domain_suffix` rule matches literal prefixes instead of the same as other projects.

This change modifies the behavior of `domain_suffix`: If the rule value is prefixed with `.`,
the behavior is unchanged, otherwise it matches `(domain|.+\.domain)` instead.
2024-02-10 22:39:12 +08:00
世界
80d95e373b
Remove PROCESS_NAME_NATIVE dwFlag in process query output
The `process_path` rule of sing-box is inherited from Clash,
the original code uses the local system's path format (e.g. `\Device\HarddiskVolume1\folder\program.exe`),
but when the device has multiple disks, the HarddiskVolume serial number is not stable.

This change make QueryFullProcessImageNameW output a Win32 path (such as `C:\folder\program.exe`),
which will disrupt the existing `process_path` use cases in Windows.
2024-02-10 22:39:12 +08:00
世界
cbacd8cde7
badtls: Support uTLS and TLS ECH for read waiter 2024-02-10 22:39:12 +08:00
62 changed files with 994 additions and 391 deletions

View File

@ -51,11 +51,13 @@ type InboundContext struct {
// rule cache
IPCIDRMatchSource bool
SourceAddressMatch bool
SourcePortMatch bool
DestinationAddressMatch bool
DestinationPortMatch bool
IPCIDRMatchSource bool
SourceAddressMatch bool
SourcePortMatch bool
DestinationAddressMatch bool
DestinationPortMatch bool
DidMatch bool
IgnoreDestinationIPCIDRMatch bool
}
func (c *InboundContext) ResetRuleCache() {

View File

@ -84,6 +84,9 @@ type DNSRule interface {
Rule
DisableCache() bool
RewriteTTL() *uint32
ClientSubnet() *netip.Addr
WithAddressLimit() bool
MatchAddressLimit(metadata *InboundContext) bool
}
type RuleSet interface {
@ -97,6 +100,7 @@ type RuleSet interface {
type RuleSetMetadata struct {
ContainsProcessRule bool
ContainsWIFIRule bool
ContainsIPCIDRRule bool
}
type RuleSetStartContext interface {

View File

@ -4,6 +4,8 @@ package badtls
import (
"bytes"
"context"
"net"
"os"
"reflect"
"sync"
@ -18,20 +20,32 @@ import (
var _ N.ReadWaiter = (*ReadWaitConn)(nil)
type ReadWaitConn struct {
*tls.STDConn
halfAccess *sync.Mutex
rawInput *bytes.Buffer
input *bytes.Reader
hand *bytes.Buffer
readWaitOptions N.ReadWaitOptions
tls.Conn
halfAccess *sync.Mutex
rawInput *bytes.Buffer
input *bytes.Reader
hand *bytes.Buffer
readWaitOptions N.ReadWaitOptions
tlsReadRecord func() error
tlsHandlePostHandshakeMessage func() error
}
func NewReadWaitConn(conn tls.Conn) (tls.Conn, error) {
stdConn, isSTDConn := conn.(*tls.STDConn)
if !isSTDConn {
var (
loaded bool
tlsReadRecord func() error
tlsHandlePostHandshakeMessage func() error
)
for _, tlsCreator := range tlsRegistry {
loaded, tlsReadRecord, tlsHandlePostHandshakeMessage = tlsCreator(conn)
if loaded {
break
}
}
if !loaded {
return nil, os.ErrInvalid
}
rawConn := reflect.Indirect(reflect.ValueOf(stdConn))
rawConn := reflect.Indirect(reflect.ValueOf(conn))
rawHalfConn := rawConn.FieldByName("in")
if !rawHalfConn.IsValid() || rawHalfConn.Kind() != reflect.Struct {
return nil, E.New("badtls: invalid half conn")
@ -57,11 +71,13 @@ func NewReadWaitConn(conn tls.Conn) (tls.Conn, error) {
}
hand := (*bytes.Buffer)(unsafe.Pointer(rawHand.UnsafeAddr()))
return &ReadWaitConn{
STDConn: stdConn,
halfAccess: halfAccess,
rawInput: rawInput,
input: input,
hand: hand,
Conn: conn,
halfAccess: halfAccess,
rawInput: rawInput,
input: input,
hand: hand,
tlsReadRecord: tlsReadRecord,
tlsHandlePostHandshakeMessage: tlsHandlePostHandshakeMessage,
}, nil
}
@ -71,19 +87,19 @@ func (c *ReadWaitConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy
}
func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) {
err = c.Handshake()
err = c.HandshakeContext(context.Background())
if err != nil {
return
}
c.halfAccess.Lock()
defer c.halfAccess.Unlock()
for c.input.Len() == 0 {
err = tlsReadRecord(c.STDConn)
err = c.tlsReadRecord()
if err != nil {
return
}
for c.hand.Len() > 0 {
err = tlsHandlePostHandshakeMessage(c.STDConn)
err = c.tlsHandlePostHandshakeMessage()
if err != nil {
return
}
@ -100,7 +116,7 @@ func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) {
if n != 0 && c.input.Len() == 0 && c.rawInput.Len() > 0 &&
// recordType(c.rawInput.Bytes()[0]) == recordTypeAlert {
c.rawInput.Bytes()[0] == 21 {
_ = tlsReadRecord(c.STDConn)
_ = c.tlsReadRecord()
// return n, err // will be io.EOF on closeNotify
}
@ -109,11 +125,27 @@ func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) {
}
func (c *ReadWaitConn) Upstream() any {
return c.STDConn
return c.Conn
}
//go:linkname tlsReadRecord crypto/tls.(*Conn).readRecord
func tlsReadRecord(c *tls.STDConn) error
var tlsRegistry []func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error)
//go:linkname tlsHandlePostHandshakeMessage crypto/tls.(*Conn).handlePostHandshakeMessage
func tlsHandlePostHandshakeMessage(c *tls.STDConn) error
func init() {
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
tlsConn, loaded := conn.(*tls.STDConn)
if !loaded {
return
}
return true, func() error {
return stdTLSReadRecord(tlsConn)
}, func() error {
return stdTLSHandlePostHandshakeMessage(tlsConn)
}
})
}
//go:linkname stdTLSReadRecord crypto/tls.(*Conn).readRecord
func stdTLSReadRecord(c *tls.STDConn) error
//go:linkname stdTLSHandlePostHandshakeMessage crypto/tls.(*Conn).handlePostHandshakeMessage
func stdTLSHandlePostHandshakeMessage(c *tls.STDConn) error

View File

@ -0,0 +1,31 @@
//go:build go1.21 && !without_badtls && with_ech
package badtls
import (
"net"
_ "unsafe"
"github.com/sagernet/cloudflare-tls"
"github.com/sagernet/sing/common"
)
func init() {
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
tlsConn, loaded := common.Cast[*tls.Conn](conn)
if !loaded {
return
}
return true, func() error {
return echReadRecord(tlsConn)
}, func() error {
return echHandlePostHandshakeMessage(tlsConn)
}
})
}
//go:linkname echReadRecord github.com/sagernet/cloudflare-tls.(*Conn).readRecord
func echReadRecord(c *tls.Conn) error
//go:linkname echHandlePostHandshakeMessage github.com/sagernet/cloudflare-tls.(*Conn).handlePostHandshakeMessage
func echHandlePostHandshakeMessage(c *tls.Conn) error

View File

@ -0,0 +1,31 @@
//go:build go1.21 && !without_badtls && with_utls
package badtls
import (
"net"
_ "unsafe"
"github.com/sagernet/sing/common"
"github.com/sagernet/utls"
)
func init() {
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
tlsConn, loaded := common.Cast[*tls.UConn](conn)
if !loaded {
return
}
return true, func() error {
return utlsReadRecord(tlsConn.Conn)
}, func() error {
return utlsHandlePostHandshakeMessage(tlsConn.Conn)
}
})
}
//go:linkname utlsReadRecord github.com/sagernet/utls.(*Conn).readRecord
func utlsReadRecord(c *tls.Conn) error
//go:linkname utlsHandlePostHandshakeMessage github.com/sagernet/utls.(*Conn).handlePostHandshakeMessage
func utlsHandlePostHandshakeMessage(c *tls.Conn) error

View File

@ -223,7 +223,7 @@ func getExecPathFromPID(pid uint32) (string, error) {
r1, _, err := syscall.SyscallN(
procQueryFullProcessImageNameW.Addr(),
uintptr(h),
uintptr(1),
uintptr(0),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(unsafe.Pointer(&size)),
)

5
constant/quic.go Normal file
View File

@ -0,0 +1,5 @@
//go:build with_quic
package constant
const WithQUIC = true

5
constant/quic_stub.go Normal file
View File

@ -0,0 +1,5 @@
//go:build !with_quic
package constant
const WithQUIC = false

View File

@ -2,6 +2,50 @@
icon: material/alert-decagram
---
#### 1.9.0-alpha.4
* Fixes and improvements
#### 1.9.0-alpha.3
* Update `quic-go` to v0.41.0
* Fixes and improvements
#### 1.9.0-alpha.2
* Add support for `client-subnet` DNS options **1**
* Fixes and improvements
**1**:
See [DNS](/configuration/dns), [DNS Server](/configuration/dns/server) and [DNS Rules](/configuration/dns/rule).
Since this feature makes the scenario mentioned in `alpha.1` no longer leak DNS requests,
the [Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) has been updated.
#### 1.9.0-alpha.1
* `domain_suffix` behavior update **1**
* `process_path` format update on Windows **2**
* Add address filter DNS rule items **3**
**1**:
See [Migration](/migration/#domain_suffix-behavior-update).
**2**:
See [Migration](/migration/#process_path-format-update-on-windows).
**3**:
The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS
if using this method.
See [Address Filter Fields](/configuration/dns/rule#address-filter-fields).
[Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated.
#### 1.8.5
* Fixes and improvements
@ -100,8 +144,8 @@ Also, starting with this release, uTLS requires at least Go 1.20.
**11**:
Updated `cloudflare-tls`, `gomobile`, `smux`, `tfo-go` and `wireguard-go` to latest, `quic-go` to `0.40.1` and `gvisor` to `20231204.0`
Updated `cloudflare-tls`, `gomobile`, `smux`, `tfo-go` and `wireguard-go` to latest, `quic-go` to `0.40.1` and `gvisor`
to `20231204.0`
#### 1.8.0-rc.11

View File

@ -1,3 +1,11 @@
---
icon: material/new-box
---
!!! quote "Changes in sing-box 1.9.0"
:material-plus: [client_subnet](#client_subnet)
# DNS
### Structure
@ -13,6 +21,7 @@
"disable_expire": false,
"independent_cache": false,
"reverse_mapping": false,
"client_subnet": "",
"fakeip": {}
}
}
@ -21,8 +30,8 @@
### Fields
| Key | Format |
|----------|--------------------------------|
| Key | Format |
|----------|---------------------------------|
| `server` | List of [DNS Server](./server/) |
| `rules` | List of [DNS Rule](./rule/) |
| `fakeip` | [FakeIP](./fakeip/) |
@ -60,6 +69,10 @@ Stores a reverse mapping of IP addresses after responding to a DNS query in orde
Since this process relies on the act of resolving domain names by an application before making a request, it can be
problematic in environments such as macOS, where DNS is proxied and cached by the system.
#### fakeip
#### client_subnet
[FakeIP](./fakeip/) settings.
!!! question "Since sing-box 1.9.0"
Append a `edns0-subnet` OPT extra record with the specified IP address to every query by default.
Can be overrides by `servers.[].client_subnet` or `rules.[].client_subnet`.

View File

@ -1,3 +1,11 @@
---
icon: material/new-box
---
!!! quote "sing-box 1.9.0 中的更改"
:material-plus: [client_subnet](#client_subnet)
# DNS
### 结构
@ -13,6 +21,7 @@
"disable_expire": false,
"independent_cache": false,
"reverse_mapping": false,
"client_subnet": "",
"fakeip": {}
}
}
@ -58,6 +67,14 @@
由于此过程依赖于应用程序在发出请求之前解析域名的行为,因此在 macOS 等 DNS 由系统代理和缓存的环境中可能会出现问题。
#### client_subnet
!!! question "自 sing-box 1.9.0 起"
默认情况下,将带有指定 IP 地址的 `edns0-subnet` OPT 附加记录附加到每个查询。
可以被 `servers.[].client_subnet``rules.[].client_subnet` 覆盖。
#### fakeip
[FakeIP](./fakeip/) 设置。

View File

@ -1,7 +1,14 @@
---
icon: material/alert-decagram
icon: material/new-box
---
!!! quote "Changes in sing-box 1.9.0"
:material-plus: [geoip](#geoip)
:material-plus: [ip_cidr](#ip_cidr)
:material-plus: [ip_is_private](#ip_is_private)
:material-plus: [client_subnet](#client_subnet)
!!! quote "Changes in sing-box 1.8.0"
:material-plus: [rule_set](#rule_set)
@ -53,11 +60,19 @@ icon: material/alert-decagram
"source_geoip": [
"private"
],
"geoip": [
"cn"
],
"source_ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"source_ip_is_private": false,
"ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"ip_is_private": false,
"source_port": [
12345
],
@ -107,7 +122,8 @@ icon: material/alert-decagram
],
"server": "local",
"disable_cache": false,
"rewrite_ttl": 100
"rewrite_ttl": 100,
"client_subnet": "127.0.0.1"
},
{
"type": "logical",
@ -115,7 +131,8 @@ icon: material/alert-decagram
"rules": [],
"server": "local",
"disable_cache": false,
"rewrite_ttl": 100
"rewrite_ttl": 100,
"client_subnet": "127.0.0.1"
}
]
}
@ -266,8 +283,6 @@ Match Clash mode.
#### wifi_ssid
<!-- md:version 1.7.0-beta.4 -->
!!! quote ""
Only supported in graphical clients on Android and iOS.
@ -312,6 +327,40 @@ Disable cache and save cache in this query.
Rewrite TTL in DNS responses.
#### client_subnet
!!! question "Since sing-box 1.9.0"
Append a `edns0-subnet` OPT extra record with the specified IP address to every query by default.
Will overrides `dns.client_subnet` and `servers.[].client_subnet`.
### Address Filter Fields
Only takes effect for IP address requests. When the query results do not match the address filtering rule items, the current rule will be skipped.
!!! note ""
`ip_cidr` items in included rule sets also takes effect as an address filtering field.
#### geoip
!!! question "Since sing-box 1.9.0"
Match GeoIP with query response.
#### ip_cidr
!!! question "Since sing-box 1.9.0"
Match IP CIDR with query response.
#### ip_is_private
!!! question "Since sing-box 1.9.0"
Match private IP with query response.
### Logical Fields
#### type

View File

@ -1,7 +1,14 @@
---
icon: material/alert-decagram
icon: material/new-box
---
!!! quote "sing-box 1.9.0 中的更改"
:material-plus: [geoip](#geoip)
:material-plus: [ip_cidr](#ip_cidr)
:material-plus: [ip_is_private](#ip_is_private)
:material-plus: [client_subnet](#client_subnet)
!!! quote "sing-box 1.8.0 中的更改"
:material-plus: [rule_set](#rule_set)
@ -53,10 +60,19 @@ icon: material/alert-decagram
"source_geoip": [
"private"
],
"geoip": [
"cn"
],
"source_ip_cidr": [
"10.0.0.0/24"
"10.0.0.0/24",
"192.168.0.1"
],
"source_ip_is_private": false,
"ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"ip_is_private": false,
"source_port": [
12345
],
@ -105,14 +121,16 @@ icon: material/alert-decagram
"direct"
],
"server": "local",
"disable_cache": false
"disable_cache": false,
"client_subnet": "127.0.0.1"
},
{
"type": "logical",
"mode": "and",
"rules": [],
"server": "local",
"disable_cache": false
"disable_cache": false,
"client_subnet": "127.0.0.1"
}
]
}
@ -307,6 +325,40 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
重写 DNS 回应中的 TTL。
#### client_subnet
!!! question "自 sing-box 1.9.0 起"
默认情况下,将带有指定 IP 地址的 `edns0-subnet` OPT 附加记录附加到每个查询。
将覆盖 `dns.client_subnet``servers.[].client_subnet`
### 地址筛选字段
仅对IP地址请求生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。
!!! note ""
引用的规则集中的 `ip_cidr` 项也作为地址筛选字段生效。
#### geoip
!!! question "自 sing-box 1.8.0 起"
与查询响应匹配 GeoIP。
#### ip_cidr
!!! question "自 sing-box 1.8.0 起"
与查询相应匹配 IP CIDR。
#### ip_is_private
!!! question "自 sing-box 1.8.0 起"
与查询响应匹配非公开 IP。
### 逻辑字段
#### type

View File

@ -1,3 +1,11 @@
---
icon: material/new-box
---
!!! quote "Changes in sing-box 1.9.0"
:material-plus: [client_subnet](#client_subnet)
### Structure
```json
@ -5,17 +13,17 @@
"dns": {
"servers": [
{
"tag": "google",
"address": "tls://dns.google",
"address_resolver": "local",
"address_strategy": "prefer_ipv4",
"strategy": "ipv4_only",
"detour": "direct"
"tag": "",
"address": "",
"address_resolver": "",
"address_strategy": "",
"strategy": "",
"detour": "",
"client_subnet": ""
}
]
}
}
```
### Fields
@ -80,10 +88,20 @@ Default domain strategy for resolving the domain names.
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
Take no effect if override by other settings.
Take no effect if overridden by other settings.
#### detour
Tag of an outbound for connecting to the dns server.
Default outbound will be used if empty.
#### client_subnet
!!! question "Since sing-box 1.9.0"
Append a `edns0-subnet` OPT extra record with the specified IP address to every query by default.
Can be overrides by `rules.[].client_subnet`.
Will overrides `dns.client_subnet`.

View File

@ -1,3 +1,11 @@
---
icon: material/new-box
---
!!! quote "sing-box 1.9.0 中的更改"
:material-plus: [client_subnet](#client_subnet)
### 结构
```json
@ -5,17 +13,17 @@
"dns": {
"servers": [
{
"tag": "google",
"address": "tls://dns.google",
"address_resolver": "local",
"address_strategy": "prefer_ipv4",
"strategy": "ipv4_only",
"detour": "direct"
"tag": "",
"address": "",
"address_resolver": "",
"address_strategy": "",
"strategy": "",
"detour": "",
"client_subnet": ""
}
]
}
}
```
### 字段
@ -87,3 +95,13 @@ DNS 服务器的地址。
用于连接到 DNS 服务器的出站的标签。
如果为空,将使用默认出站。
#### client_subnet
!!! question "自 sing-box 1.9.0 起"
默认情况下,将带有指定 IP 地址的 `edns0-subnet` OPT 附加记录附加到每个查询。
可以被 `rules.[].client_subnet` 覆盖。
将覆盖 `dns.client_subnet`

View File

@ -1,7 +1,3 @@
---
icon: material/new-box
---
!!! question "Since sing-box 1.8.0"
### Structure

View File

@ -1,7 +1,3 @@
---
icon: material/new-box
---
!!! question "自 sing-box 1.8.0 起"
### 结构

View File

@ -1,7 +1,3 @@
---
icon: material/alert-decagram
---
!!! quote "Changes in sing-box 1.8.0"
:material-delete-alert: [store_mode](#store_mode)

View File

@ -1,7 +1,3 @@
---
icon: material/alert-decagram
---
!!! quote "sing-box 1.8.0 中的更改"
:material-delete-alert: [store_mode](#store_mode)

View File

@ -1,7 +1,3 @@
---
icon: material/alert-decagram
---
# Experimental
!!! quote "Changes in sing-box 1.8.0"

View File

@ -1,7 +1,3 @@
---
icon: material/alert-decagram
---
# 实验性
!!! quote "sing-box 1.8.0 中的更改"

View File

@ -1,7 +1,3 @@
---
icon: material/alert-decagram
---
!!! quote "Changes in sing-box 1.8.0"
:material-plus: [gso](#gso)

View File

@ -1,7 +1,3 @@
---
icon: material/alert-decagram
---
!!! quote "sing-box 1.8.0 中的更改"
:material-plus: [gso](#gso)

View File

@ -1,7 +1,3 @@
---
icon: material/new-box
---
!!! quote "Changes in sing-box 1.8.0"
:material-plus: [gso](#gso)

View File

@ -1,7 +1,3 @@
---
icon: material/new-box
---
!!! quote "sing-box 1.8.0 中的更改"
:material-plus: [gso](#gso)

View File

@ -1,7 +1,3 @@
---
icon: material/alert-decagram
---
# Route
!!! quote "Changes in sing-box 1.8.0"

View File

@ -1,7 +1,3 @@
---
icon: material/alert-decagram
---
# 路由
!!! quote "sing-box 1.8.0 中的更改"

View File

@ -1,7 +1,3 @@
---
icon: material/alert-decagram
---
!!! quote "Changes in sing-box 1.8.0"
:material-plus: [rule_set](#rule_set)

View File

@ -1,7 +1,3 @@
---
icon: material/alert-decagram
---
!!! quote "sing-box 1.8.0 中的更改"
:material-plus: [rule_set](#rule_set)

View File

@ -1,7 +1,3 @@
---
icon: material/new-box
---
### Structure
!!! question "Since sing-box 1.8.0"

View File

@ -1,7 +1,3 @@
---
icon: material/new-box
---
# Rule Set
!!! question "Since sing-box 1.8.0"

View File

@ -1,7 +1,3 @@
---
icon: material/new-box
---
# Source Format
!!! question "Since sing-box 1.8.0"

View File

@ -1,8 +1,3 @@
---
icon: material/alert-decagram
---
!!! quote "Changes in sing-box 1.8.0"
:material-alert-decagram: [utls](#utls)

View File

@ -1,7 +1,3 @@
---
icon: material/alert-decagram
---
!!! quote "sing-box 1.8.0 中的更改"
:material-alert-decagram: [utls](#utls)

View File

@ -290,52 +290,6 @@ flowchart TB
=== ":material-dns: DNS rules"
!!! info
DNS rules are optional if FakeIP is used.
```json
{
"dns": {
"servers": [
{
"tag": "google",
"address": "tls://8.8.8.8"
},
{
"tag": "local",
"address": "223.5.5.5",
"detour": "direct"
}
],
"rules": [
{
"outbound": "any",
"server": "local"
},
{
"clash_mode": "Direct",
"server": "local"
},
{
"clash_mode": "Global",
"server": "google"
},
{
"geosite": "geolocation-cn",
"server": "local"
}
]
}
}
```
=== ":material-dns: DNS rules (1.8.0+)"
!!! info
DNS rules are optional if FakeIP is used.
```json
{
"dns": {
@ -382,74 +336,135 @@ flowchart TB
}
```
=== ":material-router-network: Route rules"
=== ":material-dns: DNS rules (1.9.0+)"
```json
{
"outbounds": [
=== ":material-shield-off: With DNS Leaks"
```json
{
"type": "direct",
"tag": "direct"
},
{
"type": "block",
"tag": "block"
}
],
"route": {
"rules": [
{
"type": "logical",
"mode": "or",
"rules": [
"dns": {
"servers": [
{
"protocol": "dns"
"tag": "google",
"address": "tls://8.8.8.8"
},
{
"port": 53
"tag": "local",
"address": "https://223.5.5.5/dns-query",
"detour": "direct"
}
],
"outbound": "dns"
},
{
"geoip": "private",
"outbound": "direct"
},
{
"clash_mode": "Direct",
"outbound": "direct"
},
{
"clash_mode": "Global",
"outbound": "default"
},
{
"type": "logical",
"mode": "or",
"rules": [
{
"port": 853
"outbound": "any",
"server": "local"
},
{
"network": "udp",
"port": 443
"clash_mode": "Direct",
"server": "local"
},
{
"protocol": "stun"
"clash_mode": "Global",
"server": "google"
},
{
"rule_set": "geosite-geolocation-cn",
"server": "local"
},
{
"clash_mode": "Default",
"server": "google"
},
{
"rule_set": "geoip-cn",
"server": "local"
}
],
"outbound": "block"
]
},
{
"geosite": "geolocation-cn",
"outbound": "direct"
"route": {
"rule_set": [
{
"type": "remote",
"tag": "geosite-geolocation-cn",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs"
},
{
"type": "remote",
"tag": "geoip-cn",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs"
}
]
},
"experimental": {
"clash_api": {
"default_mode": "Leak"
}
}
]
}
}
```
}
```
=== ":material-router-network: Route rules (1.8.0+)"
=== ":material-security: Without DNS Leaks (1.9.0-alpha.2+)"
```json
{
"dns": {
"servers": [
{
"tag": "google",
"address": "tls://8.8.8.8"
},
{
"tag": "local",
"address": "https://223.5.5.5/dns-query",
"detour": "direct"
}
],
"rules": [
{
"outbound": "any",
"server": "local"
},
{
"clash_mode": "Direct",
"server": "local"
},
{
"clash_mode": "Global",
"server": "google"
},
{
"rule_set": "geosite-geolocation-cn",
"server": "local"
},
{
"rule_set": "geoip-cn",
"server": "google",
"client_subnet": "114.114.114.114" // Any China client IP address
}
]
},
"route": {
"rule_set": [
{
"type": "remote",
"tag": "geosite-geolocation-cn",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs"
},
{
"type": "remote",
"tag": "geoip-cn",
"format": "binary",
"url": "https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs"
}
]
}
}
```
=== ":material-router-network: Route rules"
```json
{

View File

@ -2,6 +2,28 @@
icon: material/arrange-bring-forward
---
## 1.9.0
!!! warning "Unstable"
This version is still under development, and the following migration guide may be changed in the future.
### `domain_suffix` behavior update
For historical reasons, sing-box's `domain_suffix` rule matches literal prefixes instead of the same as other projects.
sing-box 1.9.0 modifies the behavior of `domain_suffix`: If the rule value is prefixed with `.`,
the behavior is unchanged, otherwise it matches `(domain|.+\.domain)` instead.
### `process_path` format update on Windows
The `process_path` rule of sing-box is inherited from Clash,
the original code uses the local system's path format (e.g. `\Device\HarddiskVolume1\folder\program.exe`),
but when the device has multiple disks, the HarddiskVolume serial number is not stable.
sing-box 1.9.0 make QueryFullProcessImageNameW output a Win32 path (such as `C:\folder\program.exe`),
which will disrupt the existing `process_path` use cases in Windows.
## 1.8.0
### :material-close-box: Migrate cache file from Clash API to independent options

View File

@ -2,6 +2,27 @@
icon: material/arrange-bring-forward
---
## 1.9.0
!!! warning "不稳定的"
该版本仍在开发中,迁移指南可能将在未来更改。
### `domain_suffix` 行为更新
由于历史原因sing-box 的 `domain_suffix` 规则匹配字面前缀,而不与其他项目相同。
sing-box 1.9.0 修改了 `domain_suffix` 的行为:如果规则值以 `.` 为前缀则行为不变,否则改为匹配 `(domain|.+\.domain)`
### 对 Windows 上 `process_path` 格式的更新
sing-box 的 `process_path` 规则继承自Clash
原始代码使用本地系统的路径格式(例如 `\Device\HarddiskVolume1\folder\program.exe`
但是当设备有多个硬盘时,该 HarddiskVolume 系列号并不稳定。
sing-box 1.9.0 使 QueryFullProcessImageNameW 输出 Win32 路径(如 `C:\folder\program.exe`
这将会破坏现有的 Windows `process_path` 用例。
## 1.8.0
### :material-close-box: 将缓存文件从 Clash API 迁移到独立选项

View File

@ -3,6 +3,7 @@ package experimental
import (
"context"
"os"
"sort"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
@ -27,11 +28,26 @@ func NewClashServer(ctx context.Context, router adapter.Router, logFactory log.O
}
func CalculateClashModeList(options option.Options) []string {
var clashMode []string
clashMode = append(clashMode, extraClashModeFromRule(common.PtrValueOrDefault(options.Route).Rules)...)
clashMode = append(clashMode, extraClashModeFromDNSRule(common.PtrValueOrDefault(options.DNS).Rules)...)
clashMode = common.FilterNotDefault(common.Uniq(clashMode))
return clashMode
var clashModes []string
clashModes = append(clashModes, extraClashModeFromRule(common.PtrValueOrDefault(options.Route).Rules)...)
clashModes = append(clashModes, extraClashModeFromDNSRule(common.PtrValueOrDefault(options.DNS).Rules)...)
clashModes = common.FilterNotDefault(common.Uniq(clashModes))
predefinedOrder := []string{
"Rule", "Global", "Direct",
}
var newClashModes []string
for _, mode := range clashModes {
if !common.Contains(predefinedOrder, mode) {
newClashModes = append(newClashModes, mode)
}
}
sort.Strings(newClashModes)
for _, mode := range predefinedOrder {
if common.Contains(clashModes, mode) {
newClashModes = append(newClashModes, mode)
}
}
return newClashModes
}
func extraClashModeFromRule(rules []option.Rule) []string {

View File

@ -9,9 +9,7 @@ import (
"github.com/sagernet/sing-dns"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/task"
mDNS "github.com/miekg/dns"
@ -25,9 +23,11 @@ type LocalDNSTransport interface {
func RegisterLocalDNSTransport(transport LocalDNSTransport) {
if transport == nil {
dns.RegisterTransport([]string{"local"}, dns.CreateLocalTransport)
dns.RegisterTransport([]string{"local"}, func(options dns.TransportOptions) (dns.Transport, error) {
return dns.NewLocalTransport(options), nil
})
} else {
dns.RegisterTransport([]string{"local"}, func(name string, ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, link string) (dns.Transport, error) {
dns.RegisterTransport([]string{"local"}, func(options dns.TransportOptions) (dns.Transport, error) {
return &platformLocalDNSTransport{
iif: transport,
}, nil

View File

@ -7,6 +7,7 @@ import (
"github.com/sagernet/sing-box/common/humanize"
C "github.com/sagernet/sing-box/constant"
_ "github.com/sagernet/sing-box/include"
)
var (

22
go.mod
View File

@ -12,7 +12,7 @@ require (
github.com/go-chi/cors v1.2.1
github.com/go-chi/render v1.0.3
github.com/gofrs/uuid/v5 v5.0.0
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2
github.com/insomniacslk/dhcp v0.0.0-20240204152450-ca2dc33955c1
github.com/libdns/alidns v1.0.3
github.com/libdns/cloudflare v0.1.0
github.com/logrusorgru/aurora v2.0.3+incompatible
@ -24,12 +24,12 @@ require (
github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1
github.com/sagernet/gomobile v0.1.1
github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e
github.com/sagernet/quic-go v0.40.1
github.com/sagernet/quic-go v0.41.0-beta.2
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
github.com/sagernet/sing v0.3.0
github.com/sagernet/sing-dns v0.1.12
github.com/sagernet/sing v0.3.1-beta.1
github.com/sagernet/sing-dns v0.2.0-beta.1
github.com/sagernet/sing-mux v0.2.0
github.com/sagernet/sing-quic v0.1.8
github.com/sagernet/sing-quic v0.1.9-beta.1
github.com/sagernet/sing-shadowsocks v0.2.6
github.com/sagernet/sing-shadowsocks2 v0.2.0
github.com/sagernet/sing-shadowtls v0.1.4
@ -44,11 +44,11 @@ require (
github.com/stretchr/testify v1.8.4
go.uber.org/zap v1.26.0
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.18.0
golang.org/x/net v0.20.0
golang.org/x/sys v0.16.0
golang.org/x/crypto v0.19.0
golang.org/x/net v0.21.0
golang.org/x/sys v0.17.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
google.golang.org/grpc v1.60.1
google.golang.org/grpc v1.61.0
google.golang.org/protobuf v1.32.0
howett.net/plist v1.0.1
)
@ -86,12 +86,12 @@ require (
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.17.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.2.1 // indirect

48
go.sum
View File

@ -42,15 +42,15 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk=
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA=
github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI=
github.com/insomniacslk/dhcp v0.0.0-20240204152450-ca2dc33955c1 h1:L3pm9Kf2G6gJVYawz2SrI5QnV1wzHYbqmKnSHHXJAb8=
github.com/insomniacslk/dhcp v0.0.0-20240204152450-ca2dc33955c1/go.mod h1:izxuNQZeFrbx2nK2fAyN5iNUB34Fe9j0nK4PwLzAkKw=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
@ -104,19 +104,19 @@ github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e h1:DOkjByVeAR56dks
github.com/sagernet/gvisor v0.0.0-20231209105102-8d27a30e436e/go.mod h1:fLxq/gtp0qzkaEwywlRRiGmjOK5ES/xUzyIKIFP2Asw=
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagernet/quic-go v0.40.1 h1:qLeTIJR0d0JWRmDWo346nLsVN6EWihd1kalJYPEd0TM=
github.com/sagernet/quic-go v0.40.1/go.mod h1:CcKTpzTAISxrM4PA5M20/wYuz9Tj6Tx4DwGbNl9UQrU=
github.com/sagernet/quic-go v0.41.0-beta.2 h1:NtFC1Ief+SYJkfRq68D1OEqZQTNh2jYSpyRLhjT+m6U=
github.com/sagernet/quic-go v0.41.0-beta.2/go.mod h1:X10Mf9DVHuSEReOLov/XuflD13MVLH3WtppVVFnSItU=
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
github.com/sagernet/sing v0.3.0 h1:PIDVFZHnQAAYRL1UYqNM+0k5s8f/tb1lUW6UDcQiOc8=
github.com/sagernet/sing v0.3.0/go.mod h1:9pfuAH6mZfgnz/YjP6xu5sxx882rfyjpcrTdUpd6w3g=
github.com/sagernet/sing-dns v0.1.12 h1:1HqZ+ln+Rezx/aJMStaS0d7oPeX2EobSV1NT537kyj4=
github.com/sagernet/sing-dns v0.1.12/go.mod h1:rx/DTOisneQpCgNQ4jbFU/JNEtnz0lYcHXenlVzpjEU=
github.com/sagernet/sing v0.3.1-beta.1 h1:5s4hUokrd58xgmiT2baXQJmZDUvjf7qzXXIqVbPTa3o=
github.com/sagernet/sing v0.3.1-beta.1/go.mod h1:9pfuAH6mZfgnz/YjP6xu5sxx882rfyjpcrTdUpd6w3g=
github.com/sagernet/sing-dns v0.2.0-beta.1 h1:Lj+zQ3Y38GJpP2vr0/5ELQbYjhPc92Lq7/JaB/GuPTw=
github.com/sagernet/sing-dns v0.2.0-beta.1/go.mod h1:IxOqfSb6Zt6UVCy8fJpDxb2XxqzHUytNqeOuJfaiLu8=
github.com/sagernet/sing-mux v0.2.0 h1:4C+vd8HztJCWNYfufvgL49xaOoOHXty2+EAjnzN3IYo=
github.com/sagernet/sing-mux v0.2.0/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ=
github.com/sagernet/sing-quic v0.1.8 h1:G4iBXAKIII+uTzd55oZ/9cAQswGjlvHh/0yKMQioDS0=
github.com/sagernet/sing-quic v0.1.8/go.mod h1:2w7DZXtf4MPjIGpovA3+vpI6bvOf1n1f9cQ1E2qQJSg=
github.com/sagernet/sing-quic v0.1.9-beta.1 h1:rCmgLUq2d4EA643EAvjbfUYYVMPCss0GpmS4pJCT2Lw=
github.com/sagernet/sing-quic v0.1.9-beta.1/go.mod h1:F4AXCZiwtRtYdLUTjVMO6elTpA/lLJe17sFlHhHmDVw=
github.com/sagernet/sing-shadowsocks v0.2.6 h1:xr7ylAS/q1cQYS8oxKKajhuQcchd5VJJ4K4UZrrpp0s=
github.com/sagernet/sing-shadowsocks v0.2.6/go.mod h1:j2YZBIpWIuElPFL/5sJAj470bcn/3QQ5lxZUNKLDNAM=
github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg=
@ -168,16 +168,16 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
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-20240205201215-2c58cdc269a3 h1:/RIbNt/Zr7rVhIkQhooTxCxFcdWLGIKnZA4IXNFSrvo=
golang.org/x/exp v0.0.0-20240205201215-2c58cdc269a3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -188,10 +188,10 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
@ -204,10 +204,10 @@ golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvYQH2OU3/TnxLx97WDSUDRABfT18pCOYwc2GE=
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97 h1:6GQBEOdGkX6MMTLT9V+TjtIRZCw9VPD5Z+yHY9wMgS0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231002182017-d307bd883b97/go.mod h1:v7nGkzlmW8P3n/bKmWBn2WpBjpOEx8Q6gMueudAmKfY=
google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA=
google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0=
google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=

View File

@ -15,7 +15,6 @@ import (
"github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/common/uot"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
@ -109,8 +108,8 @@ func (n *Naive) Start() error {
if common.Contains(n.network, N.NetworkUDP) {
err := n.configureHTTP3Listener()
if !include.WithQUIC && len(n.network) > 1 {
log.Warn(E.Cause(err, "naive http3 disabled"))
if !C.WithQUIC && len(n.network) > 1 {
n.logger.Warn(E.Cause(err, "naive http3 disabled"))
} else if err != nil {
return err
}

View File

@ -3,16 +3,12 @@
package include
import (
"context"
"github.com/sagernet/sing-dns"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
N "github.com/sagernet/sing/common/network"
)
func init() {
dns.RegisterTransport([]string{"dhcp"}, func(name string, ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, link string) (dns.Transport, error) {
dns.RegisterTransport([]string{"dhcp"}, func(options dns.TransportOptions) (dns.Transport, error) {
return nil, E.New(`DHCP is not included in this build, rebuild with -tags with_dhcp`)
})
}

View File

@ -6,5 +6,3 @@ import (
_ "github.com/sagernet/sing-box/transport/v2rayquic"
_ "github.com/sagernet/sing-dns/quic"
)
const WithQUIC = true

View File

@ -11,15 +11,12 @@ import (
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/transport/v2ray"
"github.com/sagernet/sing-dns"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
const WithQUIC = false
func init() {
dns.RegisterTransport([]string{"quic", "h3"}, func(name string, ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, link string) (dns.Transport, error) {
dns.RegisterTransport([]string{"quic", "h3"}, func(options dns.TransportOptions) (dns.Transport, error) {
return nil, C.ErrQUICNotIncluded
})
v2ray.RegisterQUICConstructor(

21
include/tz_android.go Normal file
View File

@ -0,0 +1,21 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// kanged from https://github.com/golang/mobile/blob/c713f31d574bb632a93f169b2cc99c9e753fef0e/app/android.go#L89
package include
// #include <time.h>
import "C"
import "time"
func init() {
var currentT C.time_t
var currentTM C.struct_tm
C.time(&currentT)
C.localtime_r(&currentT, &currentTM)
tzOffset := int(currentTM.tm_gmtoff)
tz := C.GoString(currentTM.tm_zone)
time.Local = time.FixedZone(tz, tzOffset)
}

30
include/tz_ios.go Normal file
View File

@ -0,0 +1,30 @@
package include
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation
#import <Foundation/Foundation.h>
const char* getSystemTimeZone() {
NSTimeZone *timeZone = [NSTimeZone systemTimeZone];
NSString *timeZoneName = [timeZone description];
return [timeZoneName UTF8String];
}
*/
import "C"
import (
"strings"
"time"
)
func init() {
tzDescription := C.GoString(C.getSystemTimeZone())
if len(tzDescription) == 0 {
return
}
location, err := time.LoadLocation(strings.Split(tzDescription, " ")[0])
if err != nil {
return
}
time.Local = location
}

View File

@ -19,6 +19,7 @@ type DNSServerOptions struct {
AddressFallbackDelay Duration `json:"address_fallback_delay,omitempty"`
Strategy DomainStrategy `json:"strategy,omitempty"`
Detour string `json:"detour,omitempty"`
ClientSubnet *ListenAddress `json:"client_subnet,omitempty"`
}
type DNSClientOptions struct {
@ -26,6 +27,7 @@ type DNSClientOptions struct {
DisableCache bool `json:"disable_cache,omitempty"`
DisableExpire bool `json:"disable_expire,omitempty"`
IndependentCache bool `json:"independent_cache,omitempty"`
ClientSubnet *ListenAddress `json:"client_subnet,omitempty"`
}
type DNSFakeIPOptions struct {

View File

@ -77,6 +77,9 @@ type DefaultDNSRule struct {
DomainRegex Listable[string] `json:"domain_regex,omitempty"`
Geosite Listable[string] `json:"geosite,omitempty"`
SourceGeoIP Listable[string] `json:"source_geoip,omitempty"`
GeoIP Listable[string] `json:"geoip,omitempty"`
IPCIDR Listable[string] `json:"ip_cidr,omitempty"`
IPIsPrivate bool `json:"ip_is_private,omitempty"`
SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"`
SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"`
SourcePort Listable[uint16] `json:"source_port,omitempty"`
@ -97,6 +100,7 @@ type DefaultDNSRule struct {
Server string `json:"server,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *ListenAddress `json:"client_subnet,omitempty"`
}
func (r DefaultDNSRule) IsValid() bool {
@ -105,16 +109,18 @@ func (r DefaultDNSRule) IsValid() bool {
defaultValue.Server = r.Server
defaultValue.DisableCache = r.DisableCache
defaultValue.RewriteTTL = r.RewriteTTL
defaultValue.ClientSubnet = r.ClientSubnet
return !reflect.DeepEqual(r, defaultValue)
}
type LogicalDNSRule struct {
Mode string `json:"mode"`
Rules []DNSRule `json:"rules,omitempty"`
Invert bool `json:"invert,omitempty"`
Server string `json:"server,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
Mode string `json:"mode"`
Rules []DNSRule `json:"rules,omitempty"`
Invert bool `json:"invert,omitempty"`
Server string `json:"server,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
ClientSubnet *ListenAddress `json:"client_subnet,omitempty"`
}
func (r LogicalDNSRule) IsValid() bool {

View File

@ -222,7 +222,20 @@ func NewRouter(
return nil, E.New("parse dns server[", tag, "]: missing address_resolver")
}
}
transport, err := dns.CreateTransport(tag, ctx, logFactory.NewLogger(F.ToString("dns/transport[", tag, "]")), detour, server.Address)
var clientSubnet netip.Addr
if server.ClientSubnet != nil {
clientSubnet = server.ClientSubnet.Build()
} else if dnsOptions.ClientSubnet != nil {
clientSubnet = dnsOptions.ClientSubnet.Build()
}
transport, err := dns.CreateTransport(dns.TransportOptions{
Context: ctx,
Logger: logFactory.NewLogger(F.ToString("dns/transport[", tag, "]")),
Name: tag,
Dialer: detour,
Address: server.Address,
ClientSubnet: clientSubnet,
})
if err != nil {
return nil, E.Cause(err, "parse dns server[", tag, "]")
}
@ -262,7 +275,11 @@ func NewRouter(
}
if defaultTransport == nil {
if len(transports) == 0 {
transports = append(transports, dns.NewLocalTransport("local", N.SystemDialer))
transports = append(transports, common.Must1(dns.CreateTransport(dns.TransportOptions{
Context: ctx,
Name: "local",
Dialer: common.Must1(dialer.NewDefault(router, option.DialerOptions{})),
})))
}
defaultTransport = transports[0]
}

View File

@ -2,13 +2,13 @@ package route
import (
"context"
"errors"
"net/netip"
"strings"
"time"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-dns"
"github.com/sagernet/sing/common/cache"
E "github.com/sagernet/sing/common/exceptions"
@ -37,41 +37,54 @@ func (m *DNSReverseMapping) Query(address netip.Addr) (string, bool) {
return domain, loaded
}
func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool) (context.Context, dns.Transport, dns.DomainStrategy) {
func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, index int) (context.Context, dns.Transport, dns.DomainStrategy, adapter.DNSRule, int) {
metadata := adapter.ContextFrom(ctx)
if metadata == nil {
panic("no context")
}
for i, rule := range r.dnsRules {
metadata.ResetRuleCache()
if rule.Match(metadata) {
detour := rule.Outbound()
transport, loaded := r.transportMap[detour]
if !loaded {
r.dnsLogger.ErrorContext(ctx, "transport not found: ", detour)
continue
}
if _, isFakeIP := transport.(adapter.FakeIPTransport); isFakeIP && !allowFakeIP {
continue
}
r.dnsLogger.DebugContext(ctx, "match[", i, "] ", rule.String(), " => ", detour)
if rule.DisableCache() {
ctx = dns.ContextWithDisableCache(ctx, true)
}
if rewriteTTL := rule.RewriteTTL(); rewriteTTL != nil {
ctx = dns.ContextWithRewriteTTL(ctx, *rewriteTTL)
}
if domainStrategy, dsLoaded := r.transportDomainStrategy[transport]; dsLoaded {
return ctx, transport, domainStrategy
} else {
return ctx, transport, r.defaultDomainStrategy
if index < len(r.dnsRules) {
dnsRules := r.dnsRules
if index != -1 {
dnsRules = dnsRules[index+1:]
}
for ruleIndex, rule := range dnsRules {
metadata.ResetRuleCache()
if rule.Match(metadata) {
detour := rule.Outbound()
transport, loaded := r.transportMap[detour]
if !loaded {
r.dnsLogger.ErrorContext(ctx, "transport not found: ", detour)
continue
}
if _, isFakeIP := transport.(adapter.FakeIPTransport); isFakeIP && !allowFakeIP {
continue
}
displayRuleIndex := ruleIndex
if index != -1 {
displayRuleIndex += index + 1
}
r.dnsLogger.DebugContext(ctx, "match[", displayRuleIndex, "] ", rule.String(), " => ", detour)
if rule.DisableCache() {
ctx = dns.ContextWithDisableCache(ctx, true)
}
if rewriteTTL := rule.RewriteTTL(); rewriteTTL != nil {
ctx = dns.ContextWithRewriteTTL(ctx, *rewriteTTL)
}
if clientSubnet := rule.ClientSubnet(); clientSubnet != nil {
ctx = dns.ContextWithClientSubnet(ctx, *clientSubnet)
}
if domainStrategy, dsLoaded := r.transportDomainStrategy[transport]; dsLoaded {
return ctx, transport, domainStrategy, rule, ruleIndex
} else {
return ctx, transport, r.defaultDomainStrategy, rule, ruleIndex
}
}
}
}
if domainStrategy, dsLoaded := r.transportDomainStrategy[r.defaultTransport]; dsLoaded {
return ctx, r.defaultTransport, domainStrategy
return ctx, r.defaultTransport, domainStrategy, nil, -1
} else {
return ctx, r.defaultTransport, r.defaultDomainStrategy
return ctx, r.defaultTransport, r.defaultDomainStrategy, nil, -1
}
}
@ -86,7 +99,8 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er
)
response, cached = r.dnsClient.ExchangeCache(ctx, message)
if !cached {
ctx, metadata := adapter.AppendContext(ctx)
var metadata *adapter.InboundContext
ctx, metadata = adapter.AppendContext(ctx)
if len(message.Question) > 0 {
metadata.QueryType = message.Question[0].Qtype
switch metadata.QueryType {
@ -97,17 +111,47 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er
}
metadata.Domain = fqdnToDomain(message.Question[0].Name)
}
ctx, transport, strategy := r.matchDNS(ctx, true)
ctx, cancel := context.WithTimeout(ctx, C.DNSTimeout)
defer cancel()
response, err = r.dnsClient.Exchange(ctx, transport, message, strategy)
if err != nil && len(message.Question) > 0 {
r.dnsLogger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", formatQuestion(message.Question[0].String())))
var (
transport dns.Transport
strategy dns.DomainStrategy
rule adapter.DNSRule
ruleIndex int
)
ruleIndex = -1
for {
var (
dnsCtx context.Context
cancel context.CancelFunc
addressLimit bool
)
dnsCtx, transport, strategy, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex)
dnsCtx, cancel = context.WithTimeout(dnsCtx, C.DNSTimeout)
if rule != nil && rule.WithAddressLimit() && isAddressQuery(message) {
addressLimit = true
response, err = r.dnsClient.ExchangeWithResponseCheck(dnsCtx, transport, message, strategy, func(response *mDNS.Msg) bool {
metadata.DestinationAddresses, _ = dns.MessageToAddresses(response)
return rule.MatchAddressLimit(metadata)
})
} else {
addressLimit = false
response, err = r.dnsClient.Exchange(dnsCtx, transport, message, strategy)
}
cancel()
if err != nil {
if errors.Is(err, dns.ErrResponseRejected) {
r.dnsLogger.DebugContext(ctx, E.Cause(err, "response rejected for ", formatQuestion(message.Question[0].String())))
} else if len(message.Question) > 0 {
r.dnsLogger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", formatQuestion(message.Question[0].String())))
} else {
r.dnsLogger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
}
}
if !addressLimit || err == nil {
break
}
}
}
if len(message.Question) > 0 && response != nil {
LogDNSAnswers(r.dnsLogger, ctx, message.Question[0].Name, response.Answer)
}
if r.dnsReverseMapping != nil && len(message.Question) > 0 && response != nil && len(response.Answer) > 0 {
for _, answer := range response.Answer {
switch record := answer.(type) {
@ -125,22 +169,57 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS
r.dnsLogger.DebugContext(ctx, "lookup domain ", domain)
ctx, metadata := adapter.AppendContext(ctx)
metadata.Domain = domain
ctx, transport, transportStrategy := r.matchDNS(ctx, false)
if strategy == dns.DomainStrategyAsIS {
strategy = transportStrategy
var (
transport dns.Transport
transportStrategy dns.DomainStrategy
rule adapter.DNSRule
ruleIndex int
resultAddrs []netip.Addr
err error
)
ruleIndex = -1
for {
var (
dnsCtx context.Context
cancel context.CancelFunc
addressLimit bool
)
metadata.ResetRuleCache()
metadata.DestinationAddresses = nil
dnsCtx, transport, transportStrategy, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex)
if strategy == dns.DomainStrategyAsIS {
strategy = transportStrategy
}
dnsCtx, cancel = context.WithTimeout(dnsCtx, C.DNSTimeout)
if rule != nil && rule.WithAddressLimit() {
addressLimit = true
resultAddrs, err = r.dnsClient.LookupWithResponseCheck(dnsCtx, transport, domain, strategy, func(responseAddrs []netip.Addr) bool {
metadata.DestinationAddresses = responseAddrs
return rule.MatchAddressLimit(metadata)
})
} else {
addressLimit = false
resultAddrs, err = r.dnsClient.Lookup(dnsCtx, transport, domain, strategy)
}
cancel()
if err != nil {
if errors.Is(err, dns.ErrResponseRejected) {
r.dnsLogger.DebugContext(ctx, "response rejected for ", domain)
} else {
r.dnsLogger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain))
}
} else if len(resultAddrs) == 0 {
r.dnsLogger.ErrorContext(ctx, "lookup failed for ", domain, ": empty result")
err = dns.RCodeNameError
}
if !addressLimit || err == nil {
break
}
}
ctx, cancel := context.WithTimeout(ctx, C.DNSTimeout)
defer cancel()
addrs, err := r.dnsClient.Lookup(ctx, transport, domain, strategy)
if len(addrs) > 0 {
r.dnsLogger.InfoContext(ctx, "lookup succeed for ", domain, ": ", strings.Join(F.MapToString(addrs), " "))
} else if err != nil {
r.dnsLogger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain))
} else {
r.dnsLogger.ErrorContext(ctx, "lookup failed for ", domain, ": empty result")
err = dns.RCodeNameError
if len(resultAddrs) > 0 {
r.dnsLogger.InfoContext(ctx, "lookup succeed for ", domain, ": ", strings.Join(F.MapToString(resultAddrs), " "))
}
return addrs, err
return resultAddrs, err
}
func (r *Router) LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error) {
@ -154,10 +233,13 @@ func (r *Router) ClearDNSCache() {
}
}
func LogDNSAnswers(logger log.ContextLogger, ctx context.Context, domain string, answers []mDNS.RR) {
for _, answer := range answers {
logger.InfoContext(ctx, "exchanged ", domain, " ", mDNS.Type(answer.Header().Rrtype).String(), " ", formatQuestion(answer.String()))
func isAddressQuery(message *mDNS.Msg) bool {
for _, question := range message.Question {
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
return true
}
}
return false
}
func fqdnToDomain(fqdn string) string {

View File

@ -97,3 +97,7 @@ func isWIFIDNSRule(rule option.DefaultDNSRule) bool {
func isWIFIHeadlessRule(rule option.DefaultHeadlessRule) bool {
return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0
}
func isIPCIDRHeadlessRule(rule option.DefaultHeadlessRule) bool {
return len(rule.IPCIDR) > 0 || rule.IPSet != nil
}

View File

@ -15,6 +15,7 @@ type abstractDefaultRule struct {
sourceAddressItems []RuleItem
sourcePortItems []RuleItem
destinationAddressItems []RuleItem
destinationIPCIDRItems []RuleItem
destinationPortItems []RuleItem
allItems []RuleItem
ruleSetItem RuleItem
@ -64,6 +65,7 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
}
if len(r.sourceAddressItems) > 0 && !metadata.SourceAddressMatch {
metadata.DidMatch = true
for _, item := range r.sourceAddressItems {
if item.Match(metadata) {
metadata.SourceAddressMatch = true
@ -73,6 +75,7 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
}
if len(r.sourcePortItems) > 0 && !metadata.SourcePortMatch {
metadata.DidMatch = true
for _, item := range r.sourcePortItems {
if item.Match(metadata) {
metadata.SourcePortMatch = true
@ -82,6 +85,7 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
}
if len(r.destinationAddressItems) > 0 && !metadata.DestinationAddressMatch {
metadata.DidMatch = true
for _, item := range r.destinationAddressItems {
if item.Match(metadata) {
metadata.DestinationAddressMatch = true
@ -90,7 +94,18 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
}
}
if !metadata.IgnoreDestinationIPCIDRMatch && len(r.destinationIPCIDRItems) > 0 && !metadata.DestinationAddressMatch {
metadata.DidMatch = true
for _, item := range r.destinationIPCIDRItems {
if item.Match(metadata) {
metadata.DestinationAddressMatch = true
break
}
}
}
if len(r.destinationPortItems) > 0 && !metadata.DestinationPortMatch {
metadata.DidMatch = true
for _, item := range r.destinationPortItems {
if item.Match(metadata) {
metadata.DestinationPortMatch = true
@ -100,6 +115,9 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
}
for _, item := range r.items {
if _, isRuleSet := item.(*RuleSetItem); !isRuleSet {
metadata.DidMatch = true
}
if !item.Match(metadata) {
return r.invert
}
@ -113,7 +131,7 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
return r.invert
}
if len(r.destinationAddressItems) > 0 && !metadata.DestinationAddressMatch {
if ((!metadata.IgnoreDestinationIPCIDRMatch && len(r.destinationIPCIDRItems) > 0) || len(r.destinationAddressItems) > 0) && !metadata.DestinationAddressMatch {
return r.invert
}
@ -121,6 +139,10 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
return r.invert
}
if !metadata.DidMatch {
return false
}
return !r.invert
}

View File

@ -109,7 +109,7 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt
}
if len(options.GeoIP) > 0 {
item := NewGeoIPItem(router, logger, false, options.GeoIP)
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.SourceIPCIDR) > 0 {
@ -130,12 +130,12 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt
if err != nil {
return nil, E.Cause(err, "ipcidr")
}
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
rule.allItems = append(rule.allItems, item)
}
if options.IPIsPrivate {
item := NewIPIsPrivateItem(false)
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.SourcePort) > 0 {

View File

@ -1,10 +1,13 @@
package route
import (
"net/netip"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
@ -37,6 +40,7 @@ type DefaultDNSRule struct {
abstractDefaultRule
disableCache bool
rewriteTTL *uint32
clientSubnet *netip.Addr
}
func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options option.DefaultDNSRule) (*DefaultDNSRule, error) {
@ -47,6 +51,7 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options
},
disableCache: options.DisableCache,
rewriteTTL: options.RewriteTTL,
clientSubnet: (*netip.Addr)(options.ClientSubnet),
}
if len(options.Inbound) > 0 {
item := NewInboundRule(options.Inbound)
@ -111,6 +116,11 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options
rule.sourceAddressItems = append(rule.sourceAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.GeoIP) > 0 {
item := NewGeoIPItem(router, logger, false, options.GeoIP)
rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.SourceIPCIDR) > 0 {
item, err := NewIPCIDRItem(true, options.SourceIPCIDR)
if err != nil {
@ -119,11 +129,24 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options
rule.sourceAddressItems = append(rule.sourceAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.IPCIDR) > 0 {
item, err := NewIPCIDRItem(false, options.IPCIDR)
if err != nil {
return nil, E.Cause(err, "ip_cidr")
}
rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
rule.allItems = append(rule.allItems, item)
}
if options.SourceIPIsPrivate {
item := NewIPIsPrivateItem(true)
rule.sourceAddressItems = append(rule.sourceAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if options.IPIsPrivate {
item := NewIPIsPrivateItem(false)
rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.SourcePort) > 0 {
item := NewPortItem(true, options.SourcePort)
rule.sourcePortItems = append(rule.sourcePortItems, item)
@ -211,12 +234,45 @@ func (r *DefaultDNSRule) RewriteTTL() *uint32 {
return r.rewriteTTL
}
func (r *DefaultDNSRule) ClientSubnet() *netip.Addr {
return r.clientSubnet
}
func (r *DefaultDNSRule) WithAddressLimit() bool {
if len(r.destinationIPCIDRItems) > 0 {
return true
}
for _, rawRule := range r.items {
ruleSet, isRuleSet := rawRule.(*RuleSetItem)
if !isRuleSet {
continue
}
if ruleSet.ContainsIPCIDRRule() {
return true
}
}
return false
}
func (r *DefaultDNSRule) Match(metadata *adapter.InboundContext) bool {
metadata.IgnoreDestinationIPCIDRMatch = true
defer func() {
metadata.IgnoreDestinationIPCIDRMatch = false
}()
return r.abstractDefaultRule.Match(metadata)
}
func (r *DefaultDNSRule) MatchAddressLimit(metadata *adapter.InboundContext) bool {
return r.abstractDefaultRule.Match(metadata)
}
var _ adapter.DNSRule = (*LogicalDNSRule)(nil)
type LogicalDNSRule struct {
abstractLogicalRule
disableCache bool
rewriteTTL *uint32
clientSubnet *netip.Addr
}
func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) {
@ -254,3 +310,51 @@ func (r *LogicalDNSRule) DisableCache() bool {
func (r *LogicalDNSRule) RewriteTTL() *uint32 {
return r.rewriteTTL
}
func (r *LogicalDNSRule) ClientSubnet() *netip.Addr {
return r.clientSubnet
}
func (r *LogicalDNSRule) WithAddressLimit() bool {
for _, rawRule := range r.rules {
switch rule := rawRule.(type) {
case *DefaultDNSRule:
if rule.WithAddressLimit() {
return true
}
case *LogicalDNSRule:
if rule.WithAddressLimit() {
return true
}
}
}
return false
}
func (r *LogicalDNSRule) Match(metadata *adapter.InboundContext) bool {
if r.mode == C.LogicalTypeAnd {
return common.All(r.rules, func(it adapter.HeadlessRule) bool {
metadata.ResetRuleCache()
return it.(adapter.DNSRule).Match(metadata)
}) != r.invert
} else {
return common.Any(r.rules, func(it adapter.HeadlessRule) bool {
metadata.ResetRuleCache()
return it.(adapter.DNSRule).Match(metadata)
}) != r.invert
}
}
func (r *LogicalDNSRule) MatchAddressLimit(metadata *adapter.InboundContext) bool {
if r.mode == C.LogicalTypeAnd {
return common.All(r.rules, func(it adapter.HeadlessRule) bool {
metadata.ResetRuleCache()
return it.(adapter.DNSRule).MatchAddressLimit(metadata)
}) != r.invert
} else {
return common.Any(r.rules, func(it adapter.HeadlessRule) bool {
metadata.ResetRuleCache()
return it.(adapter.DNSRule).MatchAddressLimit(metadata)
}) != r.invert
}
}

View File

@ -80,11 +80,11 @@ func NewDefaultHeadlessRule(router adapter.Router, options option.DefaultHeadles
if err != nil {
return nil, E.Cause(err, "ipcidr")
}
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
rule.allItems = append(rule.allItems, item)
} else if options.IPSet != nil {
item := NewRawIPCIDRItem(false, options.IPSet)
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.destinationIPCIDRItems = append(rule.destinationIPCIDRItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.SourcePort) > 0 {

View File

@ -4,6 +4,7 @@ import (
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
)
@ -13,7 +14,7 @@ var _ RuleItem = (*RuleSetItem)(nil)
type RuleSetItem struct {
router adapter.Router
tagList []string
setList []adapter.HeadlessRule
setList []adapter.RuleSet
ipcidrMatchSource bool
}
@ -46,6 +47,12 @@ func (r *RuleSetItem) Match(metadata *adapter.InboundContext) bool {
return false
}
func (r *RuleSetItem) ContainsIPCIDRRule() bool {
return common.Any(r.setList, func(ruleSet adapter.RuleSet) bool {
return ruleSet.Metadata().ContainsIPCIDRRule
})
}
func (r *RuleSetItem) String() string {
if len(r.tagList) == 1 {
return F.ToString("rule_set=", r.tagList[0])

View File

@ -55,6 +55,7 @@ func NewLocalRuleSet(router adapter.Router, options option.RuleSet) (*LocalRuleS
var metadata adapter.RuleSetMetadata
metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule)
metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule)
metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule)
return &LocalRuleSet{rules, metadata}, nil
}

View File

@ -150,6 +150,7 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error {
}
s.metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule)
s.metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule)
s.metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule)
s.rules = rules
return nil
}

View File

@ -21,9 +21,6 @@ import (
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/task"
"github.com/sagernet/sing/common/x/list"
@ -32,14 +29,14 @@ import (
)
func init() {
dns.RegisterTransport([]string{"dhcp"}, NewTransport)
dns.RegisterTransport([]string{"dhcp"}, func(options dns.TransportOptions) (dns.Transport, error) {
return NewTransport(options)
})
}
type Transport struct {
name string
ctx context.Context
options dns.TransportOptions
router adapter.Router
logger logger.Logger
interfaceName string
autoInterface bool
interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback]
@ -48,23 +45,20 @@ type Transport struct {
updatedAt time.Time
}
func NewTransport(name string, ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, link string) (dns.Transport, error) {
linkURL, err := url.Parse(link)
func NewTransport(options dns.TransportOptions) (*Transport, error) {
linkURL, err := url.Parse(options.Address)
if err != nil {
return nil, err
}
if linkURL.Host == "" {
return nil, E.New("missing interface name for DHCP")
}
router := adapter.RouterFromContext(ctx)
router := adapter.RouterFromContext(options.Context)
if router == nil {
return nil, E.New("missing router in context")
}
transport := &Transport{
name: name,
ctx: ctx,
router: router,
logger: logger,
interfaceName: linkURL.Host,
autoInterface: linkURL.Host == "auto",
}
@ -72,7 +66,7 @@ func NewTransport(name string, ctx context.Context, logger logger.ContextLogger,
}
func (t *Transport) Name() string {
return t.name
return t.options.Name
}
func (t *Transport) Start() error {
@ -158,8 +152,8 @@ func (t *Transport) updateServers() error {
return E.Cause(err, "dhcp: prepare interface")
}
t.logger.Info("dhcp: query DNS servers on ", iface.Name)
fetchCtx, cancel := context.WithTimeout(t.ctx, C.DHCPTimeout)
t.options.Logger.Info("dhcp: query DNS servers on ", iface.Name)
fetchCtx, cancel := context.WithTimeout(t.options.Context, C.DHCPTimeout)
err = t.fetchServers0(fetchCtx, iface)
cancel()
if err != nil {
@ -175,7 +169,7 @@ func (t *Transport) updateServers() error {
func (t *Transport) interfaceUpdated(int) {
err := t.updateServers()
if err != nil {
t.logger.Error("update servers: ", err)
t.options.Logger.Error("update servers: ", err)
}
}
@ -187,7 +181,7 @@ func (t *Transport) fetchServers0(ctx context.Context, iface *net.Interface) err
if runtime.GOOS == "linux" || runtime.GOOS == "android" {
listenAddr = "255.255.255.255:68"
}
packetConn, err := listener.ListenPacket(t.ctx, "udp4", listenAddr)
packetConn, err := listener.ListenPacket(t.options.Context, "udp4", listenAddr)
if err != nil {
return err
}
@ -225,17 +219,17 @@ func (t *Transport) fetchServersResponse(iface *net.Interface, packetConn net.Pa
dhcpPacket, err := dhcpv4.FromBytes(buffer.Bytes())
if err != nil {
t.logger.Trace("dhcp: parse DHCP response: ", err)
t.options.Logger.Trace("dhcp: parse DHCP response: ", err)
return err
}
if dhcpPacket.MessageType() != dhcpv4.MessageTypeOffer {
t.logger.Trace("dhcp: expected OFFER response, but got ", dhcpPacket.MessageType())
t.options.Logger.Trace("dhcp: expected OFFER response, but got ", dhcpPacket.MessageType())
continue
}
if dhcpPacket.TransactionID != transactionID {
t.logger.Trace("dhcp: expected transaction ID ", transactionID, ", but got ", dhcpPacket.TransactionID)
t.options.Logger.Trace("dhcp: expected transaction ID ", transactionID, ", but got ", dhcpPacket.TransactionID)
continue
}
@ -255,20 +249,22 @@ func (t *Transport) fetchServersResponse(iface *net.Interface, packetConn net.Pa
func (t *Transport) recreateServers(iface *net.Interface, serverAddrs []netip.Addr) error {
if len(serverAddrs) > 0 {
t.logger.Info("dhcp: updated DNS servers from ", iface.Name, ": [", strings.Join(common.Map(serverAddrs, func(it netip.Addr) string {
t.options.Logger.Info("dhcp: updated DNS servers from ", iface.Name, ": [", strings.Join(common.Map(serverAddrs, func(it netip.Addr) string {
return it.String()
}), ","), "]")
}
serverDialer := common.Must1(dialer.NewDefault(t.router, option.DialerOptions{
BindInterface: iface.Name,
UDPFragmentDefault: true,
}))
var transports []dns.Transport
for _, serverAddr := range serverAddrs {
serverTransport, err := dns.NewUDPTransport(t.name, t.ctx, serverDialer, M.Socksaddr{Addr: serverAddr, Port: 53})
newOptions := t.options
newOptions.Address = serverAddr.String()
newOptions.Dialer = serverDialer
serverTransport, err := dns.NewUDPTransport(newOptions)
if err != nil {
return err
return E.Cause(err, "create UDP transport from DHCP result: ", serverAddr)
}
transports = append(transports, serverTransport)
}

View File

@ -9,7 +9,6 @@ import (
"github.com/sagernet/sing-dns"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
N "github.com/sagernet/sing/common/network"
mDNS "github.com/miekg/dns"
)
@ -20,7 +19,9 @@ var (
)
func init() {
dns.RegisterTransport([]string{"fakeip"}, NewTransport)
dns.RegisterTransport([]string{"fakeip"}, func(options dns.TransportOptions) (dns.Transport, error) {
return NewTransport(options)
})
}
type Transport struct {
@ -30,15 +31,15 @@ type Transport struct {
logger logger.ContextLogger
}
func NewTransport(name string, ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, link string) (dns.Transport, error) {
router := adapter.RouterFromContext(ctx)
func NewTransport(options dns.TransportOptions) (*Transport, error) {
router := adapter.RouterFromContext(options.Context)
if router == nil {
return nil, E.New("missing router in context")
}
return &Transport{
name: name,
name: options.Name,
router: router,
logger: logger,
logger: options.Logger,
}, nil
}