Compare commits

...

5 Commits

Author SHA1 Message Date
世界
6d63f9255f
documentation: Update changelog 2023-04-08 09:37:58 +08:00
世界
6f2cc9761d
Add multi-peer support for wireguard outbound 2023-04-08 09:37:58 +08:00
世界
b484d9bca6
Add fakeip support 2023-04-08 09:37:58 +08:00
世界
58c4fd745a
Add L3 routing support 2023-04-08 09:17:12 +08:00
世界
7d1e6affb3
Add dns reverse mapping 2023-04-08 09:17:03 +08:00
87 changed files with 3009 additions and 1103 deletions

View File

@ -13,6 +13,7 @@ type ClashServer interface {
PreStarter
Mode() string
StoreSelected() bool
StoreFakeIP() bool
CacheFile() ClashCacheFile
HistoryStorage() *urltest.HistoryStorage
RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker)
@ -22,6 +23,7 @@ type ClashServer interface {
type ClashCacheFile interface {
LoadSelected(group string) string
StoreSelected(group string, selected string) error
FakeIPStorage
}
type Tracker interface {

23
adapter/fakeip.go Normal file
View File

@ -0,0 +1,23 @@
package adapter
import (
"net/netip"
"github.com/sagernet/sing-dns"
)
type FakeIPStore interface {
Service
Contains(address netip.Addr) bool
Create(domain string, strategy dns.DomainStrategy) (netip.Addr, error)
Lookup(address netip.Addr) (string, bool)
Reset() error
}
type FakeIPStorage interface {
FakeIPMetadata() *FakeIPMetadata
FakeIPSaveMetadata(metadata *FakeIPMetadata) error
FakeIPStore(address netip.Addr, domain string) error
FakeIPLoad(address netip.Addr) (string, bool)
FakeIPReset() error
}

View File

@ -0,0 +1,50 @@
package adapter
import (
"bytes"
"encoding"
"encoding/binary"
"io"
"net/netip"
"github.com/sagernet/sing/common"
)
type FakeIPMetadata struct {
Inet4Range netip.Prefix
Inet6Range netip.Prefix
Inet4Current netip.Addr
Inet6Current netip.Addr
}
func (m *FakeIPMetadata) MarshalBinary() (data []byte, err error) {
var buffer bytes.Buffer
for _, marshaler := range []encoding.BinaryMarshaler{m.Inet4Range, m.Inet6Range, m.Inet4Current, m.Inet6Current} {
data, err = marshaler.MarshalBinary()
if err != nil {
return
}
common.Must(binary.Write(&buffer, binary.BigEndian, uint16(len(data))))
buffer.Write(data)
}
data = buffer.Bytes()
return
}
func (m *FakeIPMetadata) UnmarshalBinary(data []byte) error {
reader := bytes.NewReader(data)
for _, unmarshaler := range []encoding.BinaryUnmarshaler{&m.Inet4Range, &m.Inet6Range, &m.Inet4Current, &m.Inet6Current} {
var length uint16
common.Must(binary.Read(reader, binary.BigEndian, &length))
element := make([]byte, length)
_, err := io.ReadFull(reader, element)
if err != nil {
return err
}
err = unmarshaler.UnmarshalBinary(element)
if err != nil {
return err
}
}
return nil
}

View File

@ -27,7 +27,7 @@ type InjectableInbound interface {
type InboundContext struct {
Inbound string
InboundType string
IPVersion int
IPVersion uint8
Network string
Source M.Socksaddr
Destination M.Socksaddr

View File

@ -4,6 +4,7 @@ import (
"context"
"net"
"github.com/sagernet/sing-tun"
N "github.com/sagernet/sing/common/network"
)
@ -17,3 +18,8 @@ type Outbound interface {
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
}
type IPOutbound interface {
Outbound
NewIPConnection(ctx context.Context, conn tun.RouteContext, metadata InboundContext) (tun.DirectDestination, error)
}

View File

@ -21,8 +21,13 @@ type Router interface {
Outbound(tag string) (Outbound, bool)
DefaultOutbound(network string) Outbound
FakeIPStore() FakeIPStore
RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
RouteIPConnection(ctx context.Context, conn tun.RouteContext, metadata InboundContext) tun.RouteAction
NatRequired(outbound string) bool
GeoIPReader() *geoip.Reader
LoadGeosite(code string) (Rule, error)
@ -39,7 +44,9 @@ type Router interface {
NetworkMonitor() tun.NetworkUpdateMonitor
InterfaceMonitor() tun.DefaultInterfaceMonitor
PackageManager() tun.PackageManager
Rules() []Rule
IPRules() []IPRule
TimeService
@ -76,6 +83,12 @@ type Rule interface {
type DNSRule interface {
Rule
DisableCache() bool
RewriteTTL() *uint32
}
type IPRule interface {
Rule
Action() tun.ActionType
}
type InterfaceUpdateListener interface {

View File

@ -21,7 +21,7 @@ func TestMergeJSON(t *testing.T) {
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultRule{
Network: N.NetworkTCP,
Network: []string{N.NetworkTCP},
Outbound: "direct",
},
},
@ -42,7 +42,7 @@ func TestMergeJSON(t *testing.T) {
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultRule{
Network: N.NetworkUDP,
Network: []string{N.NetworkUDP},
Outbound: "direct",
},
},

View File

@ -27,7 +27,12 @@ type slowOpenConn struct {
func DialSlowContext(dialer *tfo.Dialer, ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if dialer.DisableTFO || N.NetworkName(network) != N.NetworkTCP {
return dialer.DialContext(ctx, network, destination.String(), nil)
switch N.NetworkName(network) {
case N.NetworkTCP, N.NetworkUDP:
return dialer.Dialer.DialContext(ctx, network, destination.String())
default:
return dialer.Dialer.DialContext(ctx, network, destination.AddrString())
}
}
return &slowOpenConn{
dialer: dialer,

View File

@ -1,3 +1,22 @@
#### 1.3-beta1
* Add [DNS reverse mapping](/configuration/dns#reverse_mapping) support
* Add [L3 routing](/configuration/route/ip-rule) support **1**
* Add `rewrite_ttl` DNS rule action
* Add [FakeIP](/configuration/dns/fakeip) support **2**
* Add `store_fakeip` Clash API option
* Add multi-peer support for [WireGuard](/configuration/outbound/wireguard#peers) outbound
* Add loopback detect
*1*:
It can currently be used to [route connections directly to WireGuard](/examples/wireguard-direct) or block connections
at the IP layer.
*2*:
See [FAQ](/faq/fakeip) for more information.
#### 1.2.3
* Introducing our [new Android client application](/installation/clients/sfa)

View File

@ -0,0 +1,25 @@
# FakeIP
### Structure
```json
{
"enabled": true,
"inet4_range": "198.18.0.0/15",
"inet6_range": "fc00::/18"
}
```
### Fields
#### enabled
Enable FakeIP service.
#### inet4_range
IPv4 address range for FakeIP.
#### inet6_address
IPv6 address range for FakeIP.

View File

@ -0,0 +1,25 @@
# FakeIP
### 结构
```json
{
"enabled": true,
"inet4_range": "198.18.0.0/15",
"inet6_range": "fc00::/18"
}
```
### 字段
#### enabled
启用 FakeIP 服务。
#### inet4_range
用于 FakeIP 的 IPv4 地址范围。
#### inet6_range
用于 FakeIP 的 IPv6 地址范围。

View File

@ -10,7 +10,9 @@
"final": "",
"strategy": "",
"disable_cache": false,
"disable_expire": false
"disable_expire": false,
"reverse_mapping": false,
"fakeip": {}
}
}
@ -22,6 +24,7 @@
|----------|--------------------------------|
| `server` | List of [DNS Server](./server) |
| `rules` | List of [DNS Rule](./rule) |
| `fakeip` | [FakeIP](./fakeip) |
#### final
@ -43,4 +46,15 @@ Disable dns cache.
#### disable_expire
Disable dns cache expire.
Disable dns cache expire.
#### reverse_mapping
Stores a reverse mapping of IP addresses after responding to a DNS query in order to provide domain names when routing.
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
[FakeIP](./fakeip) settings.

View File

@ -10,7 +10,9 @@
"final": "",
"strategy": "",
"disable_cache": false,
"disable_expire": false
"disable_expire": false,
"reverse_mapping": false,
"fakeip": {}
}
}
@ -43,4 +45,14 @@
#### disable_expire
禁用 DNS 缓存过期。
禁用 DNS 缓存过期。
#### reverse_mapping
在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。
由于此过程依赖于应用程序在发出请求之前解析域名的行为,因此在 macOS 等 DNS 由系统代理和缓存的环境中可能会出现问题。
#### fakeip
[FakeIP](./fakeip) 设置。

View File

@ -84,14 +84,16 @@
"direct"
],
"server": "local",
"disable_cache": false
"disable_cache": false,
"rewrite_ttl": 100
},
{
"type": "logical",
"mode": "and",
"rules": [],
"server": "local",
"disable_cache": false
"disable_cache": false,
"rewrite_ttl": 100
}
]
}
@ -244,6 +246,10 @@ Tag of the target dns server.
Disable cache and save cache in this query.
#### rewrite_ttl
Rewrite TTL in DNS responses.
### Logical Fields
#### type

View File

@ -243,6 +243,10 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
在此查询中禁用缓存。
#### rewrite_ttl
重写 DNS 回应中的 TTL。
### 逻辑字段
#### type

View File

@ -30,17 +30,18 @@ The tag of the dns server.
The address of the dns server.
| Protocol | Format |
|----------|-------------------------------|
| `System` | `local` |
| `TCP` | `tcp://1.0.0.1` |
| `UDP` | `8.8.8.8` `udp://8.8.4.4` |
| `TLS` | `tls://dns.google` |
| `HTTPS` | `https://1.1.1.1/dns-query` |
| `QUIC` | `quic://dns.adguard.com` |
| `HTTP3` | `h3://8.8.8.8/dns-query` |
| `RCode` | `rcode://refused` |
| `DHCP` | `dhcp://auto` or `dhcp://en0` |
| Protocol | Format |
|---------------------|-------------------------------|
| `System` | `local` |
| `TCP` | `tcp://1.0.0.1` |
| `UDP` | `8.8.8.8` `udp://8.8.4.4` |
| `TLS` | `tls://dns.google` |
| `HTTPS` | `https://1.1.1.1/dns-query` |
| `QUIC` | `quic://dns.adguard.com` |
| `HTTP3` | `h3://8.8.8.8/dns-query` |
| `RCode` | `rcode://refused` |
| `DHCP` | `dhcp://auto` or `dhcp://en0` |
| [FakeIP](./fakeip) | `fakeip` |
!!! warning ""

View File

@ -30,17 +30,18 @@ DNS 服务器的标签。
DNS 服务器的地址。
| 协议 | 格式 |
|----------|------------------------------|
| `System` | `local` |
| `TCP` | `tcp://1.0.0.1` |
| `UDP` | `8.8.8.8` `udp://8.8.4.4` |
| `TLS` | `tls://dns.google` |
| `HTTPS` | `https://1.1.1.1/dns-query` |
| `QUIC` | `quic://dns.adguard.com` |
| `HTTP3` | `h3://8.8.8.8/dns-query` |
| `RCode` | `rcode://refused` |
| `DHCP` | `dhcp://auto``dhcp://en0` |
| 协议 | 格式 |
|--------------------|------------------------------|
| `System` | `local` |
| `TCP` | `tcp://1.0.0.1` |
| `UDP` | `8.8.8.8` `udp://8.8.4.4` |
| `TLS` | `tls://dns.google` |
| `HTTPS` | `https://1.1.1.1/dns-query` |
| `QUIC` | `quic://dns.adguard.com` |
| `HTTP3` | `h3://8.8.8.8/dns-query` |
| `RCode` | `rcode://refused` |
| `DHCP` | `dhcp://auto``dhcp://en0` |
| [FakeIP](./fakeip) | `fakeip` |
!!! warning ""

View File

@ -37,4 +37,10 @@
#### tag
The tag of the outbound.
The tag of the outbound.
### Features
#### Outbounds that support IP connection
* `WireGuard`

View File

@ -36,4 +36,10 @@
#### tag
出站的标签。
出站的标签。
### 特性
#### 支持 IP 连接的出站
* `WireGuard`

View File

@ -13,6 +13,18 @@
"10.0.0.2/32"
],
"private_key": "YNXtAzepDqRv9H52osJVDQnznT5AM11eCK3ESpwSt04=",
"peers": [
{
"server": "127.0.0.1",
"server_port": 1080,
"public_key": "Z1XXLsKYkYxuiYjJIkRvtIKFepCYHTgON+GwPq7SOV4=",
"pre_shared_key": "31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM=",
"allowed_ips": [
"0.0.0.0/0"
],
"reserved": [0, 0, 0]
}
],
"peer_public_key": "Z1XXLsKYkYxuiYjJIkRvtIKFepCYHTgON+GwPq7SOV4=",
"pre_shared_key": "31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM=",
"reserved": [0, 0, 0],
@ -36,13 +48,13 @@
#### server
==Required==
==Required if multi-peer disabled==
The server address.
#### server_port
==Required==
==Required if multi-peer disabled==
The server port.
@ -75,9 +87,25 @@ wg genkey
echo "private key" || wg pubkey
```
#### peers
Multi-peer support.
If enabled, `server, server_port, peer_public_key, pre_shared_key` will be ignored.
#### peers.allowed_ips
WireGuard allowed IPs.
#### peers.reserved
WireGuard reserved field bytes.
`$outbound.reserved` will be used if empty.
#### peer_public_key
==Required==
==Required if multi-peer disabled==
WireGuard peer public key.

View File

@ -7,6 +7,7 @@
"route": {
"geoip": {},
"geosite": {},
"ip_rules": [],
"rules": [],
"final": "",
"auto_detect_interface": false,
@ -19,11 +20,12 @@
### Fields
| Key | Format |
|-----------|------------------------------|
| `geoip` | [GeoIP](./geoip) |
| `geosite` | [Geosite](./geosite) |
| `rules` | List of [Route Rule](./rule) |
| Key | Format |
|------------|------------------------------------|
| `geoip` | [GeoIP](./geoip) |
| `geosite` | [Geosite](./geosite) |
| `ip_rules` | List of [IP Route Rule](./ip-rule) |
| `rules` | List of [Route Rule](./rule) |
#### final

View File

@ -7,6 +7,7 @@
"route": {
"geoip": {},
"geosite": {},
"ip_rules": [],
"rules": [],
"final": "",
"auto_detect_interface": false,
@ -19,11 +20,12 @@
### 字段
| 键 | 格式 |
|-----------|----------------------|
| `geoip` | [GeoIP](./geoip) |
| `geosite` | [GeoSite](./geosite) |
| `rules` | 一组 [路由规则](./rule) |
| 键 | 格式 |
|------------|-------------------------|
| `geoip` | [GeoIP](./geoip) |
| `geosite` | [GeoSite](./geosite) |
| `ip_rules` | 一组 [IP 路由规则](./ip-rule) |
| `rules` | 一组 [路由规则](./rule) |
#### final
@ -65,4 +67,4 @@
默认为出站连接设置路由标记。
如果设置了 `outbound.routing_mark` 设置,则不生效。
如果设置了 `outbound.routing_mark` 设置,则不生效。

View File

@ -0,0 +1,205 @@
### Structure
```json
{
"route": {
"ip_rules": [
{
"inbound": [
"mixed-in"
],
"ip_version": 6,
"network": [
"tcp"
],
"domain": [
"test.com"
],
"domain_suffix": [
".cn"
],
"domain_keyword": [
"test"
],
"domain_regex": [
"^stun\\..+"
],
"geosite": [
"cn"
],
"source_geoip": [
"private"
],
"geoip": [
"cn"
],
"source_ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"source_port": [
12345
],
"source_port_range": [
"1000:2000",
":3000",
"4000:"
],
"port": [
80,
443
],
"port_range": [
"1000:2000",
":3000",
"4000:"
],
"invert": false,
"action": "direct",
"outbound": "wireguard"
},
{
"type": "logical",
"mode": "and",
"rules": [],
"invert": false,
"action": "direct",
"outbound": "wireguard"
}
]
}
}
```
!!! note ""
You can ignore the JSON Array [] tag when the content is only one item
### Default Fields
!!! note ""
The default rule uses the following matching logic:
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite` || `geoip` || `ip_cidr`) &&
(`port` || `port_range`) &&
(`source_geoip` || `source_ip_cidr`) &&
(`source_port` || `source_port_range`) &&
`other fields`
#### inbound
Tags of [Inbound](/configuration/inbound).
#### ip_version
4 or 6.
Not limited if empty.
#### network
Match network protocol.
Available values:
* `tcp`
* `udp`
* `icmpv4`
* `icmpv6`
#### domain
Match full domain.
#### domain_suffix
Match domain suffix.
#### domain_keyword
Match domain using keyword.
#### domain_regex
Match domain using regular expression.
#### geosite
Match geosite.
#### source_geoip
Match source geoip.
#### geoip
Match geoip.
#### source_ip_cidr
Match source ip cidr.
#### ip_cidr
Match ip cidr.
#### source_port
Match source port.
#### source_port_range
Match source port range.
#### port
Match port.
#### port_range
Match port range.
#### invert
Invert match result.
#### action
==Required==
| Action | Description |
|--------|--------------------------------------------------------------------|
| return | Stop IP routing and assemble the connection to the transport layer |
| block | Block the connection |
| direct | Directly forward the connection |
#### outbound
==Required if action is direct==
Tag of the target outbound.
Only outbound which supports IP connection can be used, see [Outbounds that support IP connection](/configuration/outbound/#outbounds-that-support-ip-connection).
### Logical Fields
#### type
`logical`
#### mode
==Required==
`and` or `or`
#### rules
==Required==
Included default rules.

View File

@ -0,0 +1,204 @@
### 结构
```json
{
"route": {
"ip_rules": [
{
"inbound": [
"mixed-in"
],
"ip_version": 6,
"network": [
"tcp"
],
"domain": [
"test.com"
],
"domain_suffix": [
".cn"
],
"domain_keyword": [
"test"
],
"domain_regex": [
"^stun\\..+"
],
"geosite": [
"cn"
],
"source_geoip": [
"private"
],
"geoip": [
"cn"
],
"source_ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"source_port": [
12345
],
"source_port_range": [
"1000:2000",
":3000",
"4000:"
],
"port": [
80,
443
],
"port_range": [
"1000:2000",
":3000",
"4000:"
],
"invert": false,
"action": "direct",
"outbound": "wireguard"
},
{
"type": "logical",
"mode": "and",
"rules": [],
"invert": false,
"action": "direct",
"outbound": "wireguard"
}
]
}
}
```
!!! note ""
当内容只有一项时,可以忽略 JSON 数组 [] 标签。
### Default Fields
!!! note ""
默认规则使用以下匹配逻辑:
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite` || `geoip` || `ip_cidr`) &&
(`port` || `port_range`) &&
(`source_geoip` || `source_ip_cidr`) &&
(`source_port` || `source_port_range`) &&
`other fields`
#### inbound
[入站](/zh/configuration/inbound) 标签。
#### ip_version
4 或 6。
默认不限制。
#### network
匹配网络协议。
可用值:
* `tcp`
* `udp`
* `icmpv4`
* `icmpv6`
#### domain
匹配完整域名。
#### domain_suffix
匹配域名后缀。
#### domain_keyword
匹配域名关键字。
#### domain_regex
匹配域名正则表达式。
#### geosite
匹配 GeoSite。
#### source_geoip
匹配源 GeoIP。
#### geoip
匹配 GeoIP。
#### source_ip_cidr
匹配源 IP CIDR。
#### ip_cidr
匹配 IP CIDR。
#### source_port
匹配源端口。
#### source_port_range
匹配源端口范围。
#### port
匹配端口。
#### port_range
匹配端口范围。
#### invert
反选匹配结果。
#### action
==必填==
| Action | 描述 |
|--------|---------------------|
| return | 停止 IP 路由并将该连接组装到传输层 |
| block | 屏蔽该连接 |
| direct | 直接转发该连接 |
#### outbound
==action 为 direct 则必填==
目标出站的标签。
### 逻辑字段
#### type
`logical`
#### mode
==必填==
`and``or`
#### rules
==必填==
包括的默认规则。

View File

@ -9,7 +9,9 @@
"mixed-in"
],
"ip_version": 6,
"network": "tcp",
"network": [
"tcp"
],
"auth_user": [
"usera",
"userb"
@ -244,18 +246,12 @@ Tag of the target outbound.
#### mode
==Required==
`and` or `or`
#### rules
Included default rules.
#### invert
Invert match result.
#### outbound
==Required==
Tag of the target outbound.
Included default rules.

View File

@ -9,7 +9,9 @@
"mixed-in"
],
"ip_version": 6,
"network": "tcp",
"network": [
"tcp"
],
"auth_user": [
"usera",
"userb"
@ -242,18 +244,12 @@
#### mode
==必填==
`and``or`
#### rules
包括的默认规则。
#### invert
反选匹配结果。
#### outbound
==必填==
目标出站的标签
包括的默认规则。

View File

@ -8,3 +8,4 @@ Configuration examples for sing-box.
* [Shadowsocks](./shadowsocks)
* [ShadowTLS](./shadowtls)
* [Clash API](./clash-api)
* [WireGuard Direct](./wireguard-direct)

View File

@ -8,3 +8,4 @@ sing-box 的配置示例。
* [Shadowsocks](./shadowsocks)
* [ShadowTLS](./shadowtls)
* [Clash API](./clash-api)
* [WireGuard Direct](./wireguard-direct)

View File

@ -0,0 +1,90 @@
# WireGuard Direct
```json
{
"dns": {
"servers": [
{
"tag": "google",
"address": "tls://8.8.8.8"
},
{
"tag": "local",
"address": "223.5.5.5",
"detour": "direct"
}
],
"rules": [
{
"geoip": "cn",
"server": "direct"
}
],
"reverse_mapping": true
},
"inbounds": [
{
"type": "tun",
"tag": "tun",
"inet4_address": "172.19.0.1/30",
"auto_route": true,
"sniff": true,
"stack": "system"
}
],
"outbounds": [
{
"type": "wireguard",
"tag": "wg",
"server": "127.0.0.1",
"server_port": 2345,
"local_address": [
"172.19.0.1/128"
],
"private_key": "KLTnpPY03pig/WC3zR8U7VWmpANHPFh2/4pwICGJ5Fk=",
"peer_public_key": "uvNabcamf6Rs0vzmcw99jsjTJbxo6eWGOykSY66zsUk="
},
{
"type": "dns",
"tag": "dns"
},
{
"type": "direct",
"tag": "direct"
},
{
"type": "block",
"tag": "block"
}
],
"route": {
"ip_rules": [
{
"port": 53,
"action": "return"
},
{
"geoip": "cn",
"geosite": "cn",
"action": "return"
},
{
"action": "direct",
"outbound": "wg"
}
],
"rules": [
{
"protocol": "dns",
"outbound": "dns"
},
{
"geoip": "cn",
"geosite": "cn",
"outbound": "direct"
}
],
"auto_detect_interface": true
}
}
```

20
docs/faq/fakeip.md Normal file
View File

@ -0,0 +1,20 @@
# FakeIP
FakeIP refers to a type of behavior in a program that simultaneously hijacks both DNS and connection requests. It
responds to DNS requests with virtual results and restores mapping when accepting connections.
#### Advantage
* Retrieve the requested domain in places like IP routing (L3) where traffic detection is not possible to assist with routing.
* Decrease an RTT on the first TCP request to a domain (the most common reason).
#### Limitation
* Its mechanism breaks applications that depend on returning correct remote addresses.
* Only A and AAAA (IP) requests are supported, which may break applications that rely on other requests.
#### Recommendation
* Do not use if you do not need L3 routing.
* If using tun, make sure FakeIP ranges is included in the tun's routes.
* Enable `experimental.clash_api.store_fakeip` to persist FakeIP records, or use `dns.rules.rewrite_ttl` to avoid losing records after program restart in DNS cached environments.

19
docs/faq/fakeip.zh.md Normal file
View File

@ -0,0 +1,19 @@
# FakeIP
FakeIP 是指同时劫持 DNS 和连接请求的程序中的一种行为。它通过虚拟结果响应 DNS 请求,在接受连接时恢复映射。
#### 优点
* 在像 L3 路由这样无法进行流量探测的地方检索所请求的域名,以协助路由。
* 减少对一个域的第一个 TCP 请求的 RTT这是最常见的原因
#### 限制
* 它的机制会破坏依赖于返回正确远程地址的应用程序。
* 仅支持 A 和 AAAAIP请求这可能会破坏依赖于其他请求的应用程序。
#### 建议
* 如果不需要 L3 路由,请勿使用。
* 如果使用 tun请确保 tun 路由中包含 FakeIP 地址范围。
* 启用 `experimental.clash_api.store_fakeip` 以持久化 FakeIP 记录,或者使用 `dns.rules.rewrite_ttl` 避免程序重启后在 DNS 被缓存的环境中丢失记录。

View File

@ -11,12 +11,6 @@ it doesn't fit, because it compromises performance or design clarity, or because
If it bothers you that sing-box is missing feature X, please forgive us and investigate the features that sing-box does
have. You might find that they compensate in interesting ways for the lack of X.
#### Fake IP
Fake IP (also called Fake DNS) is an invasive and imperfect DNS solution that breaks expected behavior, causes DNS leaks
and makes many software unusable. It is recommended by some software that lacks DNS processing and caching, but sing-box
does not need this.
#### Naive outbound
NaïveProxy's main function is chromium's network stack, and it makes no sense to implement only its transport protocol.

View File

@ -9,11 +9,6 @@
如果 sing-box 缺少功能 X 让您感到困扰,请原谅我们并调查 sing-box 确实有的功能。 您可能会发现它们以有趣的方式弥补了 X 的缺失。
#### Fake IP
Fake IP也称 Fake DNS是一种侵入性和不完善的 DNS 解决方案,它打破了预期的行为,导致 DNS 泄漏并使许多软件无法使用。
一些缺乏 DNS 处理和缓存的软件推荐使用它,但 sing-box 不需要。
#### Naive 出站
NaïveProxy 的主要功能是 chromium 的网络栈,仅实现它的传输协议是舍本逐末的。

View File

@ -3,21 +3,28 @@ package clashapi
import (
"net/http"
"github.com/sagernet/sing-box/adapter"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
func cacheRouter() http.Handler {
func cacheRouter(router adapter.Router) http.Handler {
r := chi.NewRouter()
r.Post("/fakeip/flush", flushFakeip)
r.Post("/fakeip/flush", flushFakeip(router))
return r
}
func flushFakeip(w http.ResponseWriter, r *http.Request) {
/*if err := cachefile.Cache().FlushFakeip(); err != nil {
render.Status(r, http.StatusInternalServerError)
render.JSON(w, r, newError(err.Error()))
return
}*/
render.NoContent(w, r)
func flushFakeip(router adapter.Router) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if cacheFile := router.ClashServer().CacheFile(); cacheFile != nil {
err := cacheFile.FakeIPReset()
if err != nil {
render.Status(r, http.StatusInternalServerError)
render.JSON(w, r, newError(err.Error()))
return
}
}
render.NoContent(w, r)
}
}

View File

@ -0,0 +1,77 @@
package cachefile
import (
"net/netip"
"os"
"github.com/sagernet/sing-box/adapter"
"go.etcd.io/bbolt"
)
var (
bucketFakeIP = []byte("fakeip")
keyMetadata = []byte("metadata")
)
func (c *CacheFile) FakeIPMetadata() *adapter.FakeIPMetadata {
var metadata adapter.FakeIPMetadata
err := c.DB.View(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(bucketFakeIP)
if bucket == nil {
return nil
}
metadataBinary := bucket.Get(keyMetadata)
if len(metadataBinary) == 0 {
return os.ErrInvalid
}
return metadata.UnmarshalBinary(metadataBinary)
})
if err != nil {
return nil
}
return &metadata
}
func (c *CacheFile) FakeIPSaveMetadata(metadata *adapter.FakeIPMetadata) error {
return c.DB.Batch(func(tx *bbolt.Tx) error {
bucket, err := tx.CreateBucketIfNotExists(bucketFakeIP)
if err != nil {
return err
}
metadataBinary, err := metadata.MarshalBinary()
if err != nil {
return err
}
return bucket.Put(keyMetadata, metadataBinary)
})
}
func (c *CacheFile) FakeIPStore(address netip.Addr, domain string) error {
return c.DB.Batch(func(tx *bbolt.Tx) error {
bucket, err := tx.CreateBucketIfNotExists(bucketFakeIP)
if err != nil {
return err
}
return bucket.Put(address.AsSlice(), []byte(domain))
})
}
func (c *CacheFile) FakeIPLoad(address netip.Addr) (string, bool) {
var domain string
_ = c.DB.View(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(bucketFakeIP)
if bucket == nil {
return nil
}
domain = string(bucket.Get(address.AsSlice()))
return nil
})
return domain, domain != ""
}
func (c *CacheFile) FakeIPReset() error {
return c.DB.Batch(func(tx *bbolt.Tx) error {
return tx.DeleteBucket(bucketFakeIP)
})
}

View File

@ -44,6 +44,7 @@ type Server struct {
urlTestHistory *urltest.HistoryStorage
mode string
storeSelected bool
storeFakeIP bool
cacheFilePath string
cacheFile adapter.ClashCacheFile
}
@ -61,12 +62,13 @@ func NewServer(router adapter.Router, logFactory log.ObservableFactory, options
trafficManager: trafficManager,
urlTestHistory: urltest.NewHistoryStorage(),
mode: strings.ToLower(options.DefaultMode),
storeSelected: options.StoreSelected,
storeFakeIP: options.StoreFakeIP,
}
if server.mode == "" {
server.mode = "rule"
}
if options.StoreSelected {
server.storeSelected = true
if options.StoreSelected || options.StoreFakeIP {
cachePath := os.ExpandEnv(options.CacheFile)
if cachePath == "" {
cachePath = "cache.db"
@ -99,7 +101,7 @@ func NewServer(router adapter.Router, logFactory log.ObservableFactory, options
r.Mount("/providers/rules", ruleProviderRouter())
r.Mount("/script", scriptRouter())
r.Mount("/profile", profileRouter())
r.Mount("/cache", cacheRouter())
r.Mount("/cache", cacheRouter(router))
r.Mount("/dns", dnsRouter(router))
})
if options.ExternalUI != "" {
@ -156,6 +158,10 @@ func (s *Server) StoreSelected() bool {
return s.storeSelected
}
func (s *Server) StoreFakeIP() bool {
return s.storeFakeIP
}
func (s *Server) CacheFile() adapter.ClashCacheFile {
return s.cacheFile
}

View File

@ -19,7 +19,10 @@ import (
"github.com/sagernet/sing/common/ranges"
)
var _ adapter.Inbound = (*Tun)(nil)
var (
_ adapter.Inbound = (*Tun)(nil)
_ tun.Router = (*Tun)(nil)
)
type Tun struct {
tag string
@ -38,10 +41,6 @@ type Tun struct {
}
func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunInboundOptions, platformInterface platform.Interface) (*Tun, error) {
tunName := options.InterfaceName
if tunName == "" {
tunName = tun.CalculateInterfaceName("")
}
tunMTU := options.MTU
if tunMTU == 0 {
tunMTU = 9000
@ -75,7 +74,7 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger
logger: logger,
inboundOptions: options.InboundOptions,
tunOptions: tun.Options{
Name: tunName,
Name: options.InterfaceName,
MTU: tunMTU,
Inet4Address: common.Map(options.Inet4Address, option.ListenPrefix.Build),
Inet6Address: common.Map(options.Inet6Address, option.ListenPrefix.Build),
@ -141,12 +140,17 @@ func (t *Tun) Tag() string {
func (t *Tun) Start() error {
if C.IsAndroid && t.platformInterface == nil {
t.logger.Trace("building android rules")
t.tunOptions.BuildAndroidRules(t.router.PackageManager(), t)
}
if t.tunOptions.Name == "" {
t.tunOptions.Name = tun.CalculateInterfaceName("")
}
var (
tunInterface tun.Tun
err error
)
t.logger.Trace("opening interface")
if t.platformInterface != nil {
tunInterface, err = t.platformInterface.OpenTun(t.tunOptions, t.platformOptions)
} else {
@ -155,7 +159,12 @@ func (t *Tun) Start() error {
if err != nil {
return E.Cause(err, "configure tun interface")
}
t.logger.Trace("creating stack")
t.tunIf = tunInterface
var tunRouter tun.Router
if len(t.router.IPRules()) > 0 {
tunRouter = t
}
t.tunStack, err = tun.NewStack(t.stack, tun.StackOptions{
Context: t.ctx,
Tun: tunInterface,
@ -165,6 +174,7 @@ func (t *Tun) Start() error {
Inet6Address: t.tunOptions.Inet6Address,
EndpointIndependentNat: t.endpointIndependentNat,
UDPTimeout: t.udpTimeout,
Router: tunRouter,
Handler: t,
Logger: t.logger,
UnderPlatform: t.platformInterface != nil,
@ -172,6 +182,7 @@ func (t *Tun) Start() error {
if err != nil {
return err
}
t.logger.Trace("starting stack")
err = t.tunStack.Start()
if err != nil {
return err
@ -187,6 +198,21 @@ func (t *Tun) Close() error {
)
}
func (t *Tun) RouteConnection(session tun.RouteSession, conn tun.RouteContext) tun.RouteAction {
ctx := log.ContextWithNewID(t.ctx)
var metadata adapter.InboundContext
metadata.Inbound = t.tag
metadata.InboundType = C.TypeTun
metadata.IPVersion = session.IPVersion
metadata.Network = tun.NetworkName(session.Network)
metadata.Source = M.SocksaddrFromNetIP(session.Source)
metadata.Destination = M.SocksaddrFromNetIP(session.Destination)
metadata.InboundOptions = t.inboundOptions
t.logger.DebugContext(ctx, "incoming connection from ", metadata.Source)
t.logger.DebugContext(ctx, "incoming connection to ", metadata.Destination)
return t.router.RouteIPConnection(ctx, conn, metadata)
}
func (t *Tun) NewConnection(ctx context.Context, conn net.Conn, upstreamMetadata M.Metadata) error {
ctx = log.ContextWithNewID(ctx)
var metadata adapter.InboundContext

View File

@ -48,12 +48,14 @@ nav:
- configuration/dns/index.md
- DNS Server: configuration/dns/server.md
- DNS Rule: configuration/dns/rule.md
- FakeIP: configuration/dns/fakeip.md
- NTP:
- configuration/ntp/index.md
- Route:
- configuration/route/index.md
- GeoIP: configuration/route/geoip.md
- Geosite: configuration/route/geosite.md
- IP Route Rule: configuration/route/ip-rule.md
- Route Rule: configuration/route/rule.md
- Protocol Sniff: configuration/route/sniff.md
- Experimental:
@ -102,6 +104,7 @@ nav:
- URLTest: configuration/outbound/urltest.md
- FAQ:
- faq/index.md
- FakeIP: faq/fakeip.md
- Known Issues: faq/known-issues.md
- Examples:
- examples/index.md
@ -111,6 +114,7 @@ nav:
- Shadowsocks: examples/shadowsocks.md
- ShadowTLS: examples/shadowtls.md
- Clash API: examples/clash-api.md
- WireGuard Direct: examples/wireguard-direct.md
- Contributing:
- contributing/index.md
- Developing:
@ -169,6 +173,7 @@ plugins:
DNS Rule: DNS 规则
Route: 路由
IP Route Rule: IP 路由规则
Route Rule: 路由规则
Protocol Sniff: 协议探测

View File

@ -6,6 +6,7 @@ type ClashAPIOptions struct {
Secret string `json:"secret,omitempty"`
DefaultMode string `json:"default_mode,omitempty"`
StoreSelected bool `json:"store_selected,omitempty"`
StoreFakeIP bool `json:"store_fakeip,omitempty"`
CacheFile string `json:"cache_file,omitempty"`
}

View File

@ -1,27 +1,14 @@
package option
import (
"reflect"
"github.com/sagernet/sing-box/common/json"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
type DNSOptions struct {
Servers []DNSServerOptions `json:"servers,omitempty"`
Rules []DNSRule `json:"rules,omitempty"`
Final string `json:"final,omitempty"`
Servers []DNSServerOptions `json:"servers,omitempty"`
Rules []DNSRule `json:"rules,omitempty"`
Final string `json:"final,omitempty"`
ReverseMapping bool `json:"reverse_mapping,omitempty"`
FakeIP *DNSFakeIPOptions `json:"fakeip,omitempty"`
DNSClientOptions
}
type DNSClientOptions struct {
Strategy DomainStrategy `json:"strategy,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
DisableExpire bool `json:"disable_expire,omitempty"`
}
type DNSServerOptions struct {
Tag string `json:"tag,omitempty"`
Address string `json:"address"`
@ -32,96 +19,14 @@ type DNSServerOptions struct {
Detour string `json:"detour,omitempty"`
}
type _DNSRule struct {
Type string `json:"type,omitempty"`
DefaultOptions DefaultDNSRule `json:"-"`
LogicalOptions LogicalDNSRule `json:"-"`
type DNSClientOptions struct {
Strategy DomainStrategy `json:"strategy,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
DisableExpire bool `json:"disable_expire,omitempty"`
}
type DNSRule _DNSRule
func (r DNSRule) MarshalJSON() ([]byte, error) {
var v any
switch r.Type {
case C.RuleTypeDefault:
r.Type = ""
v = r.DefaultOptions
case C.RuleTypeLogical:
v = r.LogicalOptions
default:
return nil, E.New("unknown rule type: " + r.Type)
}
return MarshallObjects((_DNSRule)(r), v)
}
func (r *DNSRule) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_DNSRule)(r))
if err != nil {
return err
}
var v any
switch r.Type {
case "", C.RuleTypeDefault:
r.Type = C.RuleTypeDefault
v = &r.DefaultOptions
case C.RuleTypeLogical:
v = &r.LogicalOptions
default:
return E.New("unknown rule type: " + r.Type)
}
err = UnmarshallExcluded(bytes, (*_DNSRule)(r), v)
if err != nil {
return E.Cause(err, "dns route rule")
}
return nil
}
type DefaultDNSRule struct {
Inbound Listable[string] `json:"inbound,omitempty"`
IPVersion int `json:"ip_version,omitempty"`
QueryType Listable[DNSQueryType] `json:"query_type,omitempty"`
Network string `json:"network,omitempty"`
AuthUser Listable[string] `json:"auth_user,omitempty"`
Protocol Listable[string] `json:"protocol,omitempty"`
Domain Listable[string] `json:"domain,omitempty"`
DomainSuffix Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword Listable[string] `json:"domain_keyword,omitempty"`
DomainRegex Listable[string] `json:"domain_regex,omitempty"`
Geosite Listable[string] `json:"geosite,omitempty"`
SourceGeoIP Listable[string] `json:"source_geoip,omitempty"`
SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"`
SourcePort Listable[uint16] `json:"source_port,omitempty"`
SourcePortRange Listable[string] `json:"source_port_range,omitempty"`
Port Listable[uint16] `json:"port,omitempty"`
PortRange Listable[string] `json:"port_range,omitempty"`
ProcessName Listable[string] `json:"process_name,omitempty"`
ProcessPath Listable[string] `json:"process_path,omitempty"`
PackageName Listable[string] `json:"package_name,omitempty"`
User Listable[string] `json:"user,omitempty"`
UserID Listable[int32] `json:"user_id,omitempty"`
Outbound Listable[string] `json:"outbound,omitempty"`
ClashMode string `json:"clash_mode,omitempty"`
Invert bool `json:"invert,omitempty"`
Server string `json:"server,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
}
func (r DefaultDNSRule) IsValid() bool {
var defaultValue DefaultDNSRule
defaultValue.Invert = r.Invert
defaultValue.Server = r.Server
defaultValue.DisableCache = r.DisableCache
return !reflect.DeepEqual(r, defaultValue)
}
type LogicalDNSRule struct {
Mode string `json:"mode"`
Rules []DefaultDNSRule `json:"rules,omitempty"`
Invert bool `json:"invert,omitempty"`
Server string `json:"server,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
}
func (r LogicalDNSRule) IsValid() bool {
return len(r.Rules) > 0 && common.All(r.Rules, DefaultDNSRule.IsValid)
type DNSFakeIPOptions struct {
Enabled bool `json:"enabled,omitempty"`
Inet4Range *ListenPrefix `json:"inet4_range,omitempty"`
Inet6Range *ListenPrefix `json:"inet6_range,omitempty"`
}

View File

@ -1,17 +1,9 @@
package option
import (
"reflect"
"github.com/sagernet/sing-box/common/json"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
type RouteOptions struct {
GeoIP *GeoIPOptions `json:"geoip,omitempty"`
Geosite *GeositeOptions `json:"geosite,omitempty"`
IPRules []IPRule `json:"ip_rules,omitempty"`
Rules []Rule `json:"rules,omitempty"`
Final string `json:"final,omitempty"`
FindProcess bool `json:"find_process,omitempty"`
@ -32,94 +24,3 @@ type GeositeOptions struct {
DownloadURL string `json:"download_url,omitempty"`
DownloadDetour string `json:"download_detour,omitempty"`
}
type _Rule struct {
Type string `json:"type,omitempty"`
DefaultOptions DefaultRule `json:"-"`
LogicalOptions LogicalRule `json:"-"`
}
type Rule _Rule
func (r Rule) MarshalJSON() ([]byte, error) {
var v any
switch r.Type {
case C.RuleTypeDefault:
r.Type = ""
v = r.DefaultOptions
case C.RuleTypeLogical:
v = r.LogicalOptions
default:
return nil, E.New("unknown rule type: " + r.Type)
}
return MarshallObjects((_Rule)(r), v)
}
func (r *Rule) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_Rule)(r))
if err != nil {
return err
}
var v any
switch r.Type {
case "", C.RuleTypeDefault:
r.Type = C.RuleTypeDefault
v = &r.DefaultOptions
case C.RuleTypeLogical:
v = &r.LogicalOptions
default:
return E.New("unknown rule type: " + r.Type)
}
err = UnmarshallExcluded(bytes, (*_Rule)(r), v)
if err != nil {
return E.Cause(err, "route rule")
}
return nil
}
type DefaultRule struct {
Inbound Listable[string] `json:"inbound,omitempty"`
IPVersion int `json:"ip_version,omitempty"`
Network string `json:"network,omitempty"`
AuthUser Listable[string] `json:"auth_user,omitempty"`
Protocol Listable[string] `json:"protocol,omitempty"`
Domain Listable[string] `json:"domain,omitempty"`
DomainSuffix Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword Listable[string] `json:"domain_keyword,omitempty"`
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"`
SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"`
IPCIDR Listable[string] `json:"ip_cidr,omitempty"`
SourcePort Listable[uint16] `json:"source_port,omitempty"`
SourcePortRange Listable[string] `json:"source_port_range,omitempty"`
Port Listable[uint16] `json:"port,omitempty"`
PortRange Listable[string] `json:"port_range,omitempty"`
ProcessName Listable[string] `json:"process_name,omitempty"`
ProcessPath Listable[string] `json:"process_path,omitempty"`
PackageName Listable[string] `json:"package_name,omitempty"`
User Listable[string] `json:"user,omitempty"`
UserID Listable[int32] `json:"user_id,omitempty"`
ClashMode string `json:"clash_mode,omitempty"`
Invert bool `json:"invert,omitempty"`
Outbound string `json:"outbound,omitempty"`
}
func (r DefaultRule) IsValid() bool {
var defaultValue DefaultRule
defaultValue.Invert = r.Invert
defaultValue.Outbound = r.Outbound
return !reflect.DeepEqual(r, defaultValue)
}
type LogicalRule struct {
Mode string `json:"mode"`
Rules []DefaultRule `json:"rules,omitempty"`
Invert bool `json:"invert,omitempty"`
Outbound string `json:"outbound,omitempty"`
}
func (r LogicalRule) IsValid() bool {
return len(r.Rules) > 0 && common.All(r.Rules, DefaultRule.IsValid)
}

101
option/rule.go Normal file
View File

@ -0,0 +1,101 @@
package option
import (
"reflect"
"github.com/sagernet/sing-box/common/json"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
type _Rule struct {
Type string `json:"type,omitempty"`
DefaultOptions DefaultRule `json:"-"`
LogicalOptions LogicalRule `json:"-"`
}
type Rule _Rule
func (r Rule) MarshalJSON() ([]byte, error) {
var v any
switch r.Type {
case C.RuleTypeDefault:
r.Type = ""
v = r.DefaultOptions
case C.RuleTypeLogical:
v = r.LogicalOptions
default:
return nil, E.New("unknown rule type: " + r.Type)
}
return MarshallObjects((_Rule)(r), v)
}
func (r *Rule) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_Rule)(r))
if err != nil {
return err
}
var v any
switch r.Type {
case "", C.RuleTypeDefault:
r.Type = C.RuleTypeDefault
v = &r.DefaultOptions
case C.RuleTypeLogical:
v = &r.LogicalOptions
default:
return E.New("unknown rule type: " + r.Type)
}
err = UnmarshallExcluded(bytes, (*_Rule)(r), v)
if err != nil {
return E.Cause(err, "route rule")
}
return nil
}
type DefaultRule struct {
Inbound Listable[string] `json:"inbound,omitempty"`
IPVersion int `json:"ip_version,omitempty"`
Network Listable[string] `json:"network,omitempty"`
AuthUser Listable[string] `json:"auth_user,omitempty"`
Protocol Listable[string] `json:"protocol,omitempty"`
Domain Listable[string] `json:"domain,omitempty"`
DomainSuffix Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword Listable[string] `json:"domain_keyword,omitempty"`
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"`
SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"`
IPCIDR Listable[string] `json:"ip_cidr,omitempty"`
SourcePort Listable[uint16] `json:"source_port,omitempty"`
SourcePortRange Listable[string] `json:"source_port_range,omitempty"`
Port Listable[uint16] `json:"port,omitempty"`
PortRange Listable[string] `json:"port_range,omitempty"`
ProcessName Listable[string] `json:"process_name,omitempty"`
ProcessPath Listable[string] `json:"process_path,omitempty"`
PackageName Listable[string] `json:"package_name,omitempty"`
User Listable[string] `json:"user,omitempty"`
UserID Listable[int32] `json:"user_id,omitempty"`
ClashMode string `json:"clash_mode,omitempty"`
Invert bool `json:"invert,omitempty"`
Outbound string `json:"outbound,omitempty"`
}
func (r DefaultRule) IsValid() bool {
var defaultValue DefaultRule
defaultValue.Invert = r.Invert
defaultValue.Outbound = r.Outbound
return !reflect.DeepEqual(r, defaultValue)
}
type LogicalRule struct {
Mode string `json:"mode"`
Rules []DefaultRule `json:"rules,omitempty"`
Invert bool `json:"invert,omitempty"`
Outbound string `json:"outbound,omitempty"`
}
func (r LogicalRule) IsValid() bool {
return len(r.Rules) > 0 && common.All(r.Rules, DefaultRule.IsValid)
}

107
option/rule_dns.go Normal file
View File

@ -0,0 +1,107 @@
package option
import (
"reflect"
"github.com/sagernet/sing-box/common/json"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
type _DNSRule struct {
Type string `json:"type,omitempty"`
DefaultOptions DefaultDNSRule `json:"-"`
LogicalOptions LogicalDNSRule `json:"-"`
}
type DNSRule _DNSRule
func (r DNSRule) MarshalJSON() ([]byte, error) {
var v any
switch r.Type {
case C.RuleTypeDefault:
r.Type = ""
v = r.DefaultOptions
case C.RuleTypeLogical:
v = r.LogicalOptions
default:
return nil, E.New("unknown rule type: " + r.Type)
}
return MarshallObjects((_DNSRule)(r), v)
}
func (r *DNSRule) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_DNSRule)(r))
if err != nil {
return err
}
var v any
switch r.Type {
case "", C.RuleTypeDefault:
r.Type = C.RuleTypeDefault
v = &r.DefaultOptions
case C.RuleTypeLogical:
v = &r.LogicalOptions
default:
return E.New("unknown rule type: " + r.Type)
}
err = UnmarshallExcluded(bytes, (*_DNSRule)(r), v)
if err != nil {
return E.Cause(err, "dns route rule")
}
return nil
}
type DefaultDNSRule struct {
Inbound Listable[string] `json:"inbound,omitempty"`
IPVersion int `json:"ip_version,omitempty"`
QueryType Listable[DNSQueryType] `json:"query_type,omitempty"`
Network Listable[string] `json:"network,omitempty"`
AuthUser Listable[string] `json:"auth_user,omitempty"`
Protocol Listable[string] `json:"protocol,omitempty"`
Domain Listable[string] `json:"domain,omitempty"`
DomainSuffix Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword Listable[string] `json:"domain_keyword,omitempty"`
DomainRegex Listable[string] `json:"domain_regex,omitempty"`
Geosite Listable[string] `json:"geosite,omitempty"`
SourceGeoIP Listable[string] `json:"source_geoip,omitempty"`
SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"`
SourcePort Listable[uint16] `json:"source_port,omitempty"`
SourcePortRange Listable[string] `json:"source_port_range,omitempty"`
Port Listable[uint16] `json:"port,omitempty"`
PortRange Listable[string] `json:"port_range,omitempty"`
ProcessName Listable[string] `json:"process_name,omitempty"`
ProcessPath Listable[string] `json:"process_path,omitempty"`
PackageName Listable[string] `json:"package_name,omitempty"`
User Listable[string] `json:"user,omitempty"`
UserID Listable[int32] `json:"user_id,omitempty"`
Outbound Listable[string] `json:"outbound,omitempty"`
ClashMode string `json:"clash_mode,omitempty"`
Invert bool `json:"invert,omitempty"`
Server string `json:"server,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
}
func (r DefaultDNSRule) IsValid() bool {
var defaultValue DefaultDNSRule
defaultValue.Invert = r.Invert
defaultValue.Server = r.Server
defaultValue.DisableCache = r.DisableCache
defaultValue.RewriteTTL = r.RewriteTTL
return !reflect.DeepEqual(r, defaultValue)
}
type LogicalDNSRule struct {
Mode string `json:"mode"`
Rules []DefaultDNSRule `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"`
}
func (r LogicalDNSRule) IsValid() bool {
return len(r.Rules) > 0 && common.All(r.Rules, DefaultDNSRule.IsValid)
}

120
option/rule_ip.go Normal file
View File

@ -0,0 +1,120 @@
package option
import (
"reflect"
"github.com/sagernet/sing-box/common/json"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
type _IPRule struct {
Type string `json:"type,omitempty"`
DefaultOptions DefaultIPRule `json:"-"`
LogicalOptions LogicalIPRule `json:"-"`
}
type IPRule _IPRule
func (r IPRule) MarshalJSON() ([]byte, error) {
var v any
switch r.Type {
case C.RuleTypeDefault:
r.Type = ""
v = r.DefaultOptions
case C.RuleTypeLogical:
v = r.LogicalOptions
default:
return nil, E.New("unknown rule type: " + r.Type)
}
return MarshallObjects((_IPRule)(r), v)
}
func (r *IPRule) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_IPRule)(r))
if err != nil {
return err
}
var v any
switch r.Type {
case "", C.RuleTypeDefault:
r.Type = C.RuleTypeDefault
v = &r.DefaultOptions
case C.RuleTypeLogical:
v = &r.LogicalOptions
default:
return E.New("unknown rule type: " + r.Type)
}
err = UnmarshallExcluded(bytes, (*_IPRule)(r), v)
if err != nil {
return E.Cause(err, "ip route rule")
}
return nil
}
type DefaultIPRule struct {
Inbound Listable[string] `json:"inbound,omitempty"`
IPVersion int `json:"ip_version,omitempty"`
Network Listable[string] `json:"network,omitempty"`
Domain Listable[string] `json:"domain,omitempty"`
DomainSuffix Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword Listable[string] `json:"domain_keyword,omitempty"`
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"`
SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"`
IPCIDR Listable[string] `json:"ip_cidr,omitempty"`
SourcePort Listable[uint16] `json:"source_port,omitempty"`
SourcePortRange Listable[string] `json:"source_port_range,omitempty"`
Port Listable[uint16] `json:"port,omitempty"`
PortRange Listable[string] `json:"port_range,omitempty"`
Invert bool `json:"invert,omitempty"`
Action RouteAction `json:"action,omitempty"`
Outbound string `json:"outbound,omitempty"`
}
type RouteAction tun.ActionType
func (a RouteAction) MarshalJSON() ([]byte, error) {
typeName, err := tun.ActionTypeName(tun.ActionType(a))
if err != nil {
return nil, err
}
return json.Marshal(typeName)
}
func (a *RouteAction) UnmarshalJSON(bytes []byte) error {
var value string
err := json.Unmarshal(bytes, &value)
if err != nil {
return err
}
actionType, err := tun.ParseActionType(value)
if err != nil {
return err
}
*a = RouteAction(actionType)
return nil
}
func (r DefaultIPRule) IsValid() bool {
var defaultValue DefaultIPRule
defaultValue.Invert = r.Invert
defaultValue.Outbound = r.Outbound
return !reflect.DeepEqual(r, defaultValue)
}
type LogicalIPRule struct {
Mode string `json:"mode"`
Rules []DefaultIPRule `json:"rules,omitempty"`
Invert bool `json:"invert,omitempty"`
Action RouteAction `json:"action,omitempty"`
Outbound string `json:"outbound,omitempty"`
}
func (r LogicalIPRule) IsValid() bool {
return len(r.Rules) > 0 && common.All(r.Rules, DefaultIPRule.IsValid)
}

View File

@ -2,15 +2,25 @@ package option
type WireGuardOutboundOptions struct {
DialerOptions
ServerOptions
SystemInterface bool `json:"system_interface,omitempty"`
InterfaceName string `json:"interface_name,omitempty"`
LocalAddress Listable[ListenPrefix] `json:"local_address"`
PrivateKey string `json:"private_key"`
PeerPublicKey string `json:"peer_public_key"`
PreSharedKey string `json:"pre_shared_key,omitempty"`
Reserved []uint8 `json:"reserved,omitempty"`
Workers int `json:"workers,omitempty"`
MTU uint32 `json:"mtu,omitempty"`
Network NetworkList `json:"network,omitempty"`
Peers []WireGuardPeer `json:"peers,omitempty"`
ServerOptions
PeerPublicKey string `json:"peer_public_key"`
PreSharedKey string `json:"pre_shared_key,omitempty"`
Reserved []uint8 `json:"reserved,omitempty"`
Workers int `json:"workers,omitempty"`
MTU uint32 `json:"mtu,omitempty"`
Network NetworkList `json:"network,omitempty"`
IPRewrite bool `json:"ip_rewrite,omitempty"`
}
type WireGuardPeer struct {
ServerOptions
PublicKey string `json:"public_key,omitempty"`
PreSharedKey string `json:"pre_shared_key,omitempty"`
AllowedIPs Listable[string] `json:"allowed_ips,omitempty"`
Reserved []uint8 `json:"reserved,omitempty"`
}

View File

@ -8,7 +8,9 @@ import (
"encoding/hex"
"fmt"
"net"
"os"
"strings"
"syscall"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
@ -26,7 +28,7 @@ import (
)
var (
_ adapter.Outbound = (*WireGuard)(nil)
_ adapter.IPOutbound = (*WireGuard)(nil)
_ adapter.InterfaceUpdateListener = (*WireGuard)(nil)
)
@ -34,6 +36,7 @@ type WireGuard struct {
myOutboundAdapter
bind *wireguard.ClientBind
device *device.Device
natDevice wireguard.NatDevice
tunDevice wireguard.Device
}
@ -54,13 +57,22 @@ func NewWireGuard(ctx context.Context, router adapter.Router, logger log.Context
}
copy(reserved[:], options.Reserved)
}
peerAddr := options.ServerOptions.Build()
outbound.bind = wireguard.NewClientBind(ctx, dialer.New(router, options.DialerOptions), peerAddr, reserved)
var isConnect bool
var connectAddr M.Socksaddr
if len(options.Peers) < 2 {
isConnect = true
if len(options.Peers) == 1 {
connectAddr = options.Peers[0].ServerOptions.Build()
} else {
connectAddr = options.ServerOptions.Build()
}
}
outbound.bind = wireguard.NewClientBind(ctx, dialer.New(router, options.DialerOptions), isConnect, connectAddr, reserved)
localPrefixes := common.Map(options.LocalAddress, option.ListenPrefix.Build)
if len(localPrefixes) == 0 {
return nil, E.New("missing local address")
}
var privateKey, peerPublicKey, preSharedKey string
var privateKey string
{
bytes, err := base64.StdEncoding.DecodeString(options.PrivateKey)
if err != nil {
@ -68,55 +80,103 @@ func NewWireGuard(ctx context.Context, router adapter.Router, logger log.Context
}
privateKey = hex.EncodeToString(bytes)
}
{
bytes, err := base64.StdEncoding.DecodeString(options.PeerPublicKey)
if err != nil {
return nil, E.Cause(err, "decode peer public key")
}
peerPublicKey = hex.EncodeToString(bytes)
}
if options.PreSharedKey != "" {
bytes, err := base64.StdEncoding.DecodeString(options.PreSharedKey)
if err != nil {
return nil, E.Cause(err, "decode pre shared key")
}
preSharedKey = hex.EncodeToString(bytes)
}
ipcConf := "private_key=" + privateKey
ipcConf += "\npublic_key=" + peerPublicKey
ipcConf += "\nendpoint=" + peerAddr.String()
if preSharedKey != "" {
ipcConf += "\npreshared_key=" + preSharedKey
}
var has4, has6 bool
for _, address := range localPrefixes {
if address.Addr().Is4() {
has4 = true
} else {
has6 = true
if len(options.Peers) > 0 {
for i, peer := range options.Peers {
var peerPublicKey, preSharedKey string
{
bytes, err := base64.StdEncoding.DecodeString(peer.PublicKey)
if err != nil {
return nil, E.Cause(err, "decode public key for peer ", i)
}
peerPublicKey = hex.EncodeToString(bytes)
}
if peer.PreSharedKey != "" {
bytes, err := base64.StdEncoding.DecodeString(peer.PreSharedKey)
if err != nil {
return nil, E.Cause(err, "decode pre shared key for peer ", i)
}
preSharedKey = hex.EncodeToString(bytes)
}
destination := peer.ServerOptions.Build()
ipcConf += "\npublic_key=" + peerPublicKey
ipcConf += "\nendpoint=" + destination.String()
if preSharedKey != "" {
ipcConf += "\npreshared_key=" + preSharedKey
}
if len(peer.AllowedIPs) == 0 {
return nil, E.New("missing allowed_ips for peer ", i)
}
for _, allowedIP := range peer.AllowedIPs {
ipcConf += "\nallowed_ip=" + allowedIP
}
if len(peer.Reserved) > 0 {
if len(peer.Reserved) != 3 {
return nil, E.New("invalid reserved value for peer ", i, ", required 3 bytes, got ", len(peer.Reserved))
}
copy(reserved[:], options.Reserved)
outbound.bind.SetReservedForEndpoint(destination, reserved)
}
}
} else {
var peerPublicKey, preSharedKey string
{
bytes, err := base64.StdEncoding.DecodeString(options.PeerPublicKey)
if err != nil {
return nil, E.Cause(err, "decode peer public key")
}
peerPublicKey = hex.EncodeToString(bytes)
}
if options.PreSharedKey != "" {
bytes, err := base64.StdEncoding.DecodeString(options.PreSharedKey)
if err != nil {
return nil, E.Cause(err, "decode pre shared key")
}
preSharedKey = hex.EncodeToString(bytes)
}
ipcConf += "\npublic_key=" + peerPublicKey
ipcConf += "\nendpoint=" + options.ServerOptions.Build().String()
if preSharedKey != "" {
ipcConf += "\npreshared_key=" + preSharedKey
}
var has4, has6 bool
for _, address := range localPrefixes {
if address.Addr().Is4() {
has4 = true
} else {
has6 = true
}
}
if has4 {
ipcConf += "\nallowed_ip=0.0.0.0/0"
}
if has6 {
ipcConf += "\nallowed_ip=::/0"
}
}
if has4 {
ipcConf += "\nallowed_ip=0.0.0.0/0"
}
if has6 {
ipcConf += "\nallowed_ip=::/0"
}
mtu := options.MTU
if mtu == 0 {
mtu = 1408
}
var wireTunDevice wireguard.Device
var tunDevice wireguard.Device
var err error
if !options.SystemInterface && tun.WithGVisor {
wireTunDevice, err = wireguard.NewStackDevice(localPrefixes, mtu)
tunDevice, err = wireguard.NewStackDevice(localPrefixes, mtu, options.IPRewrite)
} else {
wireTunDevice, err = wireguard.NewSystemDevice(router, options.InterfaceName, localPrefixes, mtu)
tunDevice, err = wireguard.NewSystemDevice(router, options.InterfaceName, localPrefixes, mtu)
}
if err != nil {
return nil, E.Cause(err, "create WireGuard device")
}
wgDevice := device.NewDevice(wireTunDevice, outbound.bind, &device.Logger{
natDevice, isNatDevice := tunDevice.(wireguard.NatDevice)
if !isNatDevice && router.NatRequired(tag) {
natDevice = wireguard.NewNATDevice(tunDevice, options.IPRewrite)
}
deviceInput := tunDevice
if natDevice != nil {
deviceInput = natDevice
}
wgDevice := device.NewDevice(deviceInput, outbound.bind, &device.Logger{
Verbosef: func(format string, args ...interface{}) {
logger.Debug(fmt.Sprintf(strings.ToLower(format), args...))
},
@ -132,7 +192,8 @@ func NewWireGuard(ctx context.Context, router adapter.Router, logger log.Context
return nil, E.Cause(err, "setup wireguard")
}
outbound.device = wgDevice
outbound.tunDevice = wireTunDevice
outbound.natDevice = natDevice
outbound.tunDevice = tunDevice
return outbound, nil
}
@ -171,6 +232,27 @@ func (w *WireGuard) NewPacketConnection(ctx context.Context, conn N.PacketConn,
return NewPacketConnection(ctx, w, conn, metadata)
}
func (w *WireGuard) NewIPConnection(ctx context.Context, conn tun.RouteContext, metadata adapter.InboundContext) (tun.DirectDestination, error) {
if w.natDevice == nil {
return nil, os.ErrInvalid
}
session := tun.RouteSession{
IPVersion: metadata.IPVersion,
Network: tun.NetworkFromName(metadata.Network),
Source: metadata.Source.AddrPort(),
Destination: metadata.Destination.AddrPort(),
}
switch session.Network {
case syscall.IPPROTO_TCP:
w.logger.InfoContext(ctx, "linked connection to ", metadata.Destination)
case syscall.IPPROTO_UDP:
w.logger.InfoContext(ctx, "linked packet connection to ", metadata.Destination)
default:
w.logger.InfoContext(ctx, "linked ", metadata.Network, " connection to ", metadata.Destination.AddrString())
}
return w.natDevice.CreateDestination(session, conn), nil
}
func (w *WireGuard) Start() error {
return w.tunDevice.Start()
}

View File

@ -2,14 +2,11 @@ package route
import (
"context"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"os"
"os/user"
"path/filepath"
"strings"
"time"
@ -27,6 +24,7 @@ import (
"github.com/sagernet/sing-box/ntp"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/outbound"
"github.com/sagernet/sing-box/transport/fakeip"
"github.com/sagernet/sing-dns"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-vmess"
@ -38,7 +36,6 @@ import (
F "github.com/sagernet/sing/common/format"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/common/uot"
)
@ -73,6 +70,7 @@ type Router struct {
outbounds []adapter.Outbound
outboundByTag map[string]adapter.Outbound
rules []adapter.Rule
ipRules []adapter.IPRule
defaultDetour string
defaultOutboundForConnection adapter.Outbound
defaultOutboundForPacketConnection adapter.Outbound
@ -90,6 +88,8 @@ type Router struct {
transports []dns.Transport
transportMap map[string]dns.Transport
transportDomainStrategy map[dns.Transport]dns.DomainStrategy
dnsReverseMapping *DNSReverseMapping
fakeIPStore adapter.FakeIPStore
interfaceFinder myInterfaceFinder
autoDetectInterface bool
defaultInterface string
@ -129,6 +129,7 @@ func NewRouter(
dnsLogger: logFactory.NewLogger("dns"),
outboundByTag: make(map[string]adapter.Outbound),
rules: make([]adapter.Rule, 0, len(options.Rules)),
ipRules: make([]adapter.IPRule, 0, len(options.IPRules)),
dnsRules: make([]adapter.DNSRule, 0, len(dnsOptions.Rules)),
needGeoIPDatabase: hasRule(options.Rules, isGeoIPRule) || hasDNSRule(dnsOptions.Rules, isGeoIPDNSRule),
needGeositeDatabase: hasRule(options.Rules, isGeositeRule) || hasDNSRule(dnsOptions.Rules, isGeositeDNSRule),
@ -150,6 +151,13 @@ func NewRouter(
}
router.rules = append(router.rules, routeRule)
}
for i, ipRuleOptions := range options.IPRules {
ipRule, err := NewIPRule(router, router.logger, ipRuleOptions)
if err != nil {
return nil, E.Cause(err, "parse ip rule[", i, "]")
}
router.ipRules = append(router.ipRules, ipRule)
}
for i, dnsRuleOptions := range dnsOptions.Rules {
dnsRule, err := NewDNSRule(router, router.logger, dnsRuleOptions)
if err != nil {
@ -157,6 +165,7 @@ func NewRouter(
}
router.dnsRules = append(router.dnsRules, dnsRule)
}
transports := make([]dns.Transport, len(dnsOptions.Servers))
dummyTransportMap := make(map[string]dns.Transport)
transportMap := make(map[string]dns.Transport)
@ -211,12 +220,8 @@ func NewRouter(
} else {
continue
}
} else if notIpAddress != nil {
switch serverURL.Scheme {
case "rcode", "dhcp":
default:
return nil, E.New("parse dns server[", tag, "]: missing address_resolver")
}
} else if notIpAddress != nil && strings.Contains(server.Address, ".") {
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)
@ -268,6 +273,22 @@ func NewRouter(
router.transportMap = transportMap
router.transportDomainStrategy = transportDomainStrategy
if dnsOptions.ReverseMapping {
router.dnsReverseMapping = NewDNSReverseMapping()
}
if fakeIPOptions := dnsOptions.FakeIP; fakeIPOptions != nil && dnsOptions.FakeIP.Enabled {
var inet4Range netip.Prefix
var inet6Range netip.Prefix
if fakeIPOptions.Inet4Range != nil {
inet4Range = fakeIPOptions.Inet4Range.Build()
}
if fakeIPOptions.Inet6Range != nil {
inet6Range = fakeIPOptions.Inet6Range.Build()
}
router.fakeIPStore = fakeip.NewStore(router, inet4Range, inet6Range)
}
needInterfaceMonitor := platformInterface == nil && (options.AutoDetectInterface || common.Any(inbounds, func(inbound option.Inbound) bool {
return inbound.HTTPOptions.SetSystemProxy || inbound.MixedOptions.SetSystemProxy || inbound.TunOptions.AutoRoute
}))
@ -474,6 +495,12 @@ func (r *Router) Start() error {
return E.Cause(err, "initialize DNS rule[", i, "]")
}
}
if r.fakeIPStore != nil {
err := r.fakeIPStore.Start()
if err != nil {
return err
}
}
for i, transport := range r.transports {
err := transport.Start()
if err != nil {
@ -539,30 +566,15 @@ func (r *Router) Close() error {
return E.Cause(err, "close time service")
})
}
if r.fakeIPStore != nil {
r.logger.Trace("closing fakeip store")
err = E.Append(err, r.fakeIPStore.Close(), func(err error) error {
return E.Cause(err, "close fakeip store")
})
}
return err
}
func (r *Router) GeoIPReader() *geoip.Reader {
return r.geoIPReader
}
func (r *Router) LoadGeosite(code string) (adapter.Rule, error) {
rule, cached := r.geositeCache[code]
if cached {
return rule, nil
}
items, err := r.geositeReader.Read(code)
if err != nil {
return nil, err
}
rule, err = NewDefaultRule(r, nil, geosite.Compile(items))
if err != nil {
return nil, err
}
r.geositeCache[code] = rule
return rule, nil
}
func (r *Router) Outbound(tag string) (adapter.Outbound, bool) {
outbound, loaded := r.outboundByTag[tag]
return outbound, loaded
@ -576,6 +588,10 @@ func (r *Router) DefaultOutbound(network string) adapter.Outbound {
}
}
func (r *Router) FakeIPStore() adapter.FakeIPStore {
return r.fakeIPStore
}
func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
if metadata.InboundDetour != "" {
if metadata.LastInbound == metadata.InboundDetour {
@ -628,6 +644,19 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
metadata.Destination = M.Socksaddr{Addr: netip.IPv4Unspecified()}
return r.RoutePacketConnection(ctx, uot.NewConn(conn, uot.Request{}), metadata)
}
if r.fakeIPStore != nil && r.fakeIPStore.Contains(metadata.Destination.Addr) {
domain, loaded := r.fakeIPStore.Lookup(metadata.Destination.Addr)
if !loaded {
return E.New("missing fakeip context")
}
metadata.Destination = M.Socksaddr{
Fqdn: domain,
Port: metadata.Destination.Port,
}
r.logger.DebugContext(ctx, "found fakeip domain: ", domain)
}
if metadata.InboundOptions.SniffEnabled {
buffer := buf.NewPacket()
buffer.FullReset()
@ -653,6 +682,15 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
buffer.Release()
}
}
if r.dnsReverseMapping != nil && metadata.Domain == "" {
domain, loaded := r.dnsReverseMapping.Query(metadata.Destination.Addr)
if loaded {
metadata.Domain = domain
r.logger.DebugContext(ctx, "found reserve mapped domain: ", metadata.Domain)
}
}
if metadata.Destination.IsFqdn() && dns.DomainStrategy(metadata.InboundOptions.DomainStrategy) != dns.DomainStrategyAsIS {
addresses, err := r.Lookup(adapter.WithContext(ctx, &metadata), metadata.Destination.Fqdn, dns.DomainStrategy(metadata.InboundOptions.DomainStrategy))
if err != nil {
@ -707,6 +745,21 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
return nil
}
metadata.Network = N.NetworkUDP
var originAddress M.Socksaddr
if r.fakeIPStore != nil && r.fakeIPStore.Contains(metadata.Destination.Addr) {
domain, loaded := r.fakeIPStore.Lookup(metadata.Destination.Addr)
if !loaded {
return E.New("missing fakeip context")
}
originAddress = metadata.Destination
metadata.Destination = M.Socksaddr{
Fqdn: domain,
Port: metadata.Destination.Port,
}
r.logger.DebugContext(ctx, "found fakeip domain: ", domain)
}
if metadata.InboundOptions.SniffEnabled {
buffer := buf.NewPacket()
buffer.FullReset()
@ -733,6 +786,13 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
}
conn = bufio.NewCachedPacketConn(conn, buffer, destination)
}
if r.dnsReverseMapping != nil && metadata.Domain == "" {
domain, loaded := r.dnsReverseMapping.Query(metadata.Destination.Addr)
if loaded {
metadata.Domain = domain
r.logger.DebugContext(ctx, "found reserve mapped domain: ", metadata.Domain)
}
}
if metadata.Destination.IsFqdn() && dns.DomainStrategy(metadata.InboundOptions.DomainStrategy) != dns.DomainStrategyAsIS {
addresses, err := r.Lookup(adapter.WithContext(ctx, &metadata), metadata.Destination.Fqdn, dns.DomainStrategy(metadata.InboundOptions.DomainStrategy))
if err != nil {
@ -758,6 +818,9 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
conn = statsService.RoutedPacketConnection(metadata.Inbound, detour.Tag(), metadata.User, conn)
}
}
if originAddress.IsValid() {
conn = fakeip.NewNATPacketConn(conn, originAddress, metadata.Destination)
}
return detour.NewPacketConnection(ctx, conn, metadata)
}
@ -852,6 +915,10 @@ func (r *Router) Rules() []adapter.Rule {
return r.rules
}
func (r *Router) IPRules() []adapter.IPRule {
return r.ipRules
}
func (r *Router) NetworkMonitor() tun.NetworkUpdateMonitor {
return r.networkMonitor
}
@ -887,239 +954,6 @@ func (r *Router) SetV2RayServer(server adapter.V2RayServer) {
r.v2rayServer = server
}
func hasRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool {
for _, rule := range rules {
switch rule.Type {
case C.RuleTypeDefault:
if cond(rule.DefaultOptions) {
return true
}
case C.RuleTypeLogical:
for _, subRule := range rule.LogicalOptions.Rules {
if cond(subRule) {
return true
}
}
}
}
return false
}
func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bool) bool {
for _, rule := range rules {
switch rule.Type {
case C.RuleTypeDefault:
if cond(rule.DefaultOptions) {
return true
}
case C.RuleTypeLogical:
for _, subRule := range rule.LogicalOptions.Rules {
if cond(subRule) {
return true
}
}
}
}
return false
}
func isGeoIPRule(rule option.DefaultRule) bool {
return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode) || len(rule.GeoIP) > 0 && common.Any(rule.GeoIP, notPrivateNode)
}
func isGeoIPDNSRule(rule option.DefaultDNSRule) bool {
return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode)
}
func isGeositeRule(rule option.DefaultRule) bool {
return len(rule.Geosite) > 0
}
func isGeositeDNSRule(rule option.DefaultDNSRule) bool {
return len(rule.Geosite) > 0
}
func isProcessRule(rule option.DefaultRule) bool {
return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0
}
func isProcessDNSRule(rule option.DefaultDNSRule) bool {
return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0
}
func notPrivateNode(code string) bool {
return code != "private"
}
func (r *Router) prepareGeoIPDatabase() error {
var geoPath string
if r.geoIPOptions.Path != "" {
geoPath = r.geoIPOptions.Path
} else {
geoPath = "geoip.db"
if foundPath, loaded := C.FindPath(geoPath); loaded {
geoPath = foundPath
}
}
geoPath = C.BasePath(geoPath)
if !rw.FileExists(geoPath) {
r.logger.Warn("geoip database not exists: ", geoPath)
var err error
for attempts := 0; attempts < 3; attempts++ {
err = r.downloadGeoIPDatabase(geoPath)
if err == nil {
break
}
r.logger.Error("download geoip database: ", err)
os.Remove(geoPath)
// time.Sleep(10 * time.Second)
}
if err != nil {
return err
}
}
geoReader, codes, err := geoip.Open(geoPath)
if err != nil {
return E.Cause(err, "open geoip database")
}
r.logger.Info("loaded geoip database: ", len(codes), " codes")
r.geoIPReader = geoReader
return nil
}
func (r *Router) prepareGeositeDatabase() error {
var geoPath string
if r.geositeOptions.Path != "" {
geoPath = r.geositeOptions.Path
} else {
geoPath = "geosite.db"
if foundPath, loaded := C.FindPath(geoPath); loaded {
geoPath = foundPath
}
}
geoPath = C.BasePath(geoPath)
if !rw.FileExists(geoPath) {
r.logger.Warn("geosite database not exists: ", geoPath)
var err error
for attempts := 0; attempts < 3; attempts++ {
err = r.downloadGeositeDatabase(geoPath)
if err == nil {
break
}
r.logger.Error("download geosite database: ", err)
os.Remove(geoPath)
// time.Sleep(10 * time.Second)
}
if err != nil {
return err
}
}
geoReader, codes, err := geosite.Open(geoPath)
if err == nil {
r.logger.Info("loaded geosite database: ", len(codes), " codes")
r.geositeReader = geoReader
} else {
return E.Cause(err, "open geosite database")
}
return nil
}
func (r *Router) downloadGeoIPDatabase(savePath string) error {
var downloadURL string
if r.geoIPOptions.DownloadURL != "" {
downloadURL = r.geoIPOptions.DownloadURL
} else {
downloadURL = "https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db"
}
r.logger.Info("downloading geoip database")
var detour adapter.Outbound
if r.geoIPOptions.DownloadDetour != "" {
outbound, loaded := r.Outbound(r.geoIPOptions.DownloadDetour)
if !loaded {
return E.New("detour outbound not found: ", r.geoIPOptions.DownloadDetour)
}
detour = outbound
} else {
detour = r.defaultOutboundForConnection
}
if parentDir := filepath.Dir(savePath); parentDir != "" {
os.MkdirAll(parentDir, 0o755)
}
saveFile, err := os.OpenFile(savePath, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return E.Cause(err, "open output file: ", downloadURL)
}
defer saveFile.Close()
httpClient := &http.Client{
Transport: &http.Transport{
ForceAttemptHTTP2: true,
TLSHandshakeTimeout: 5 * time.Second,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return detour.DialContext(ctx, network, M.ParseSocksaddr(addr))
},
},
}
defer httpClient.CloseIdleConnections()
response, err := httpClient.Get(downloadURL)
if err != nil {
return err
}
defer response.Body.Close()
_, err = io.Copy(saveFile, response.Body)
return err
}
func (r *Router) downloadGeositeDatabase(savePath string) error {
var downloadURL string
if r.geositeOptions.DownloadURL != "" {
downloadURL = r.geositeOptions.DownloadURL
} else {
downloadURL = "https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db"
}
r.logger.Info("downloading geosite database")
var detour adapter.Outbound
if r.geositeOptions.DownloadDetour != "" {
outbound, loaded := r.Outbound(r.geositeOptions.DownloadDetour)
if !loaded {
return E.New("detour outbound not found: ", r.geositeOptions.DownloadDetour)
}
detour = outbound
} else {
detour = r.defaultOutboundForConnection
}
if parentDir := filepath.Dir(savePath); parentDir != "" {
os.MkdirAll(parentDir, 0o755)
}
saveFile, err := os.OpenFile(savePath, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return E.Cause(err, "open output file: ", downloadURL)
}
defer saveFile.Close()
httpClient := &http.Client{
Transport: &http.Transport{
ForceAttemptHTTP2: true,
TLSHandshakeTimeout: 5 * time.Second,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return detour.DialContext(ctx, network, M.ParseSocksaddr(addr))
},
},
}
defer httpClient.CloseIdleConnections()
response, err := httpClient.Get(downloadURL)
if err != nil {
return err
}
defer response.Body.Close()
_, err = io.Copy(saveFile, response.Body)
return err
}
func (r *Router) OnPackagesUpdated(packages int, sharedUsers int) {
r.logger.Info("updated packages list: ", packages, " packages, ", sharedUsers, " shared users")
}

View File

@ -4,17 +4,39 @@ import (
"context"
"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"
F "github.com/sagernet/sing/common/format"
M "github.com/sagernet/sing/common/metadata"
mDNS "github.com/miekg/dns"
)
type DNSReverseMapping struct {
cache *cache.LruCache[netip.Addr, string]
}
func NewDNSReverseMapping() *DNSReverseMapping {
return &DNSReverseMapping{
cache: cache.New[netip.Addr, string](),
}
}
func (m *DNSReverseMapping) Save(address netip.Addr, domain string, ttl int) {
m.cache.StoreWithExpire(address, domain, time.Now().Add(time.Duration(ttl)*time.Second))
}
func (m *DNSReverseMapping) Query(address netip.Addr) (string, bool) {
domain, loaded := m.cache.Load(address)
return domain, loaded
}
func (r *Router) matchDNS(ctx context.Context) (context.Context, dns.Transport, dns.DomainStrategy) {
metadata := adapter.ContextFrom(ctx)
if metadata == nil {
@ -25,6 +47,9 @@ func (r *Router) matchDNS(ctx context.Context) (context.Context, dns.Transport,
if rule.DisableCache() {
ctx = dns.ContextWithDisableCache(ctx, true)
}
if rewriteTTL := rule.RewriteTTL(); rewriteTTL != nil {
ctx = dns.ContextWithRewriteTTL(ctx, *rewriteTTL)
}
detour := rule.Outbound()
r.dnsLogger.DebugContext(ctx, "match[", i, "] ", rule.String(), " => ", detour)
if transport, loaded := r.transportMap[detour]; loaded {
@ -69,6 +94,16 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er
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) {
case *mDNS.A:
r.dnsReverseMapping.Save(M.AddrFromIP(record.A), fqdnToDomain(record.Hdr.Name), int(record.Hdr.Ttl))
case *mDNS.AAAA:
r.dnsReverseMapping.Save(M.AddrFromIP(record.AAAA), fqdnToDomain(record.Hdr.Name), int(record.Hdr.Ttl))
}
}
}
return response, err
}

View File

@ -0,0 +1,283 @@
package route
import (
"context"
"io"
"net"
"net/http"
"os"
"path/filepath"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/geoip"
"github.com/sagernet/sing-box/common/geosite"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/rw"
)
func (r *Router) GeoIPReader() *geoip.Reader {
return r.geoIPReader
}
func (r *Router) LoadGeosite(code string) (adapter.Rule, error) {
rule, cached := r.geositeCache[code]
if cached {
return rule, nil
}
items, err := r.geositeReader.Read(code)
if err != nil {
return nil, err
}
rule, err = NewDefaultRule(r, nil, geosite.Compile(items))
if err != nil {
return nil, err
}
r.geositeCache[code] = rule
return rule, nil
}
func (r *Router) prepareGeoIPDatabase() error {
var geoPath string
if r.geoIPOptions.Path != "" {
geoPath = r.geoIPOptions.Path
} else {
geoPath = "geoip.db"
if foundPath, loaded := C.FindPath(geoPath); loaded {
geoPath = foundPath
}
}
geoPath = C.BasePath(geoPath)
if rw.FileExists(geoPath) {
geoReader, codes, err := geoip.Open(geoPath)
if err == nil {
r.logger.Info("loaded geoip database: ", len(codes), " codes")
r.geoIPReader = geoReader
return nil
}
}
if !rw.FileExists(geoPath) {
r.logger.Warn("geoip database not exists: ", geoPath)
var err error
for attempts := 0; attempts < 3; attempts++ {
err = r.downloadGeoIPDatabase(geoPath)
if err == nil {
break
}
r.logger.Error("download geoip database: ", err)
os.Remove(geoPath)
// time.Sleep(10 * time.Second)
}
if err != nil {
return err
}
}
geoReader, codes, err := geoip.Open(geoPath)
if err != nil {
return E.Cause(err, "open geoip database")
}
r.logger.Info("loaded geoip database: ", len(codes), " codes")
r.geoIPReader = geoReader
return nil
}
func (r *Router) prepareGeositeDatabase() error {
var geoPath string
if r.geositeOptions.Path != "" {
geoPath = r.geositeOptions.Path
} else {
geoPath = "geosite.db"
if foundPath, loaded := C.FindPath(geoPath); loaded {
geoPath = foundPath
}
}
geoPath = C.BasePath(geoPath)
if !rw.FileExists(geoPath) {
r.logger.Warn("geosite database not exists: ", geoPath)
var err error
for attempts := 0; attempts < 3; attempts++ {
err = r.downloadGeositeDatabase(geoPath)
if err == nil {
break
}
r.logger.Error("download geosite database: ", err)
os.Remove(geoPath)
// time.Sleep(10 * time.Second)
}
if err != nil {
return err
}
}
geoReader, codes, err := geosite.Open(geoPath)
if err == nil {
r.logger.Info("loaded geosite database: ", len(codes), " codes")
r.geositeReader = geoReader
} else {
return E.Cause(err, "open geosite database")
}
return nil
}
func (r *Router) downloadGeoIPDatabase(savePath string) error {
var downloadURL string
if r.geoIPOptions.DownloadURL != "" {
downloadURL = r.geoIPOptions.DownloadURL
} else {
downloadURL = "https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db"
}
r.logger.Info("downloading geoip database")
var detour adapter.Outbound
if r.geoIPOptions.DownloadDetour != "" {
outbound, loaded := r.Outbound(r.geoIPOptions.DownloadDetour)
if !loaded {
return E.New("detour outbound not found: ", r.geoIPOptions.DownloadDetour)
}
detour = outbound
} else {
detour = r.defaultOutboundForConnection
}
if parentDir := filepath.Dir(savePath); parentDir != "" {
os.MkdirAll(parentDir, 0o755)
}
saveFile, err := os.OpenFile(savePath, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return E.Cause(err, "open output file: ", downloadURL)
}
defer saveFile.Close()
httpClient := &http.Client{
Transport: &http.Transport{
ForceAttemptHTTP2: true,
TLSHandshakeTimeout: 5 * time.Second,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return detour.DialContext(ctx, network, M.ParseSocksaddr(addr))
},
},
}
defer httpClient.CloseIdleConnections()
response, err := httpClient.Get(downloadURL)
if err != nil {
return err
}
defer response.Body.Close()
_, err = io.Copy(saveFile, response.Body)
return err
}
func (r *Router) downloadGeositeDatabase(savePath string) error {
var downloadURL string
if r.geositeOptions.DownloadURL != "" {
downloadURL = r.geositeOptions.DownloadURL
} else {
downloadURL = "https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db"
}
r.logger.Info("downloading geosite database")
var detour adapter.Outbound
if r.geositeOptions.DownloadDetour != "" {
outbound, loaded := r.Outbound(r.geositeOptions.DownloadDetour)
if !loaded {
return E.New("detour outbound not found: ", r.geositeOptions.DownloadDetour)
}
detour = outbound
} else {
detour = r.defaultOutboundForConnection
}
if parentDir := filepath.Dir(savePath); parentDir != "" {
os.MkdirAll(parentDir, 0o755)
}
saveFile, err := os.OpenFile(savePath, os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return E.Cause(err, "open output file: ", downloadURL)
}
defer saveFile.Close()
httpClient := &http.Client{
Transport: &http.Transport{
ForceAttemptHTTP2: true,
TLSHandshakeTimeout: 5 * time.Second,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return detour.DialContext(ctx, network, M.ParseSocksaddr(addr))
},
},
}
defer httpClient.CloseIdleConnections()
response, err := httpClient.Get(downloadURL)
if err != nil {
return err
}
defer response.Body.Close()
_, err = io.Copy(saveFile, response.Body)
return err
}
func hasRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool {
for _, rule := range rules {
switch rule.Type {
case C.RuleTypeDefault:
if cond(rule.DefaultOptions) {
return true
}
case C.RuleTypeLogical:
for _, subRule := range rule.LogicalOptions.Rules {
if cond(subRule) {
return true
}
}
}
}
return false
}
func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bool) bool {
for _, rule := range rules {
switch rule.Type {
case C.RuleTypeDefault:
if cond(rule.DefaultOptions) {
return true
}
case C.RuleTypeLogical:
for _, subRule := range rule.LogicalOptions.Rules {
if cond(subRule) {
return true
}
}
}
}
return false
}
func isGeoIPRule(rule option.DefaultRule) bool {
return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode) || len(rule.GeoIP) > 0 && common.Any(rule.GeoIP, notPrivateNode)
}
func isGeoIPDNSRule(rule option.DefaultDNSRule) bool {
return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode)
}
func isGeositeRule(rule option.DefaultRule) bool {
return len(rule.Geosite) > 0
}
func isGeositeDNSRule(rule option.DefaultDNSRule) bool {
return len(rule.Geosite) > 0
}
func isProcessRule(rule option.DefaultRule) bool {
return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0
}
func isProcessDNSRule(rule option.DefaultDNSRule) bool {
return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0
}
func notPrivateNode(code string) bool {
return code != "private"
}

79
route/router_ip.go Normal file
View File

@ -0,0 +1,79 @@
package route
import (
"context"
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-dns"
"github.com/sagernet/sing-tun"
F "github.com/sagernet/sing/common/format"
M "github.com/sagernet/sing/common/metadata"
)
func (r *Router) RouteIPConnection(ctx context.Context, conn tun.RouteContext, metadata adapter.InboundContext) tun.RouteAction {
if r.fakeIPStore != nil && r.fakeIPStore.Contains(metadata.Destination.Addr) {
domain, loaded := r.fakeIPStore.Lookup(metadata.Destination.Addr)
if !loaded {
r.logger.ErrorContext(ctx, "missing fakeip context")
return (*tun.ActionReturn)(nil)
}
metadata.Destination = M.Socksaddr{
Fqdn: domain,
Port: metadata.Destination.Port,
}
r.logger.DebugContext(ctx, "found fakeip domain: ", domain)
}
if r.dnsReverseMapping != nil && metadata.Domain == "" {
domain, loaded := r.dnsReverseMapping.Query(metadata.Destination.Addr)
if loaded {
metadata.Domain = domain
r.logger.DebugContext(ctx, "found reserve mapped domain: ", metadata.Domain)
}
}
if metadata.Destination.IsFqdn() && dns.DomainStrategy(metadata.InboundOptions.DomainStrategy) != dns.DomainStrategyAsIS {
addresses, err := r.Lookup(adapter.WithContext(ctx, &metadata), metadata.Destination.Fqdn, dns.DomainStrategy(metadata.InboundOptions.DomainStrategy))
if err != nil {
r.logger.ErrorContext(ctx, err)
return (*tun.ActionReturn)(nil)
}
metadata.DestinationAddresses = addresses
r.dnsLogger.DebugContext(ctx, "resolved [", strings.Join(F.MapToString(metadata.DestinationAddresses), " "), "]")
}
for i, rule := range r.ipRules {
if rule.Match(&metadata) {
if rule.Action() == tun.ActionTypeBlock {
r.logger.InfoContext(ctx, "match[", i, "] ", rule.String(), " => block")
return (*tun.ActionBlock)(nil)
}
detour := rule.Outbound()
r.logger.InfoContext(ctx, "match[", i, "] ", rule.String(), " => ", detour)
outbound, loaded := r.Outbound(detour)
if !loaded {
r.logger.ErrorContext(ctx, "outbound not found: ", detour)
break
}
ipOutbound, loaded := outbound.(adapter.IPOutbound)
if !loaded {
r.logger.ErrorContext(ctx, "outbound have no ip connection support: ", detour)
break
}
destination, err := ipOutbound.NewIPConnection(ctx, conn, metadata)
if err != nil {
r.logger.ErrorContext(ctx, err)
break
}
return &tun.ActionDirect{DirectDestination: destination}
}
}
return (*tun.ActionReturn)(nil)
}
func (r *Router) NatRequired(outbound string) bool {
for _, ipRule := range r.ipRules {
if ipRule.Outbound() == outbound {
return true
}
}
return false
}

207
route/rule_abstract.go Normal file
View File

@ -0,0 +1,207 @@
package route
import (
"strings"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
F "github.com/sagernet/sing/common/format"
)
type abstractDefaultRule struct {
items []RuleItem
sourceAddressItems []RuleItem
sourcePortItems []RuleItem
destinationAddressItems []RuleItem
destinationPortItems []RuleItem
allItems []RuleItem
invert bool
outbound string
}
func (r *abstractDefaultRule) Type() string {
return C.RuleTypeDefault
}
func (r *abstractDefaultRule) Start() error {
for _, item := range r.allItems {
err := common.Start(item)
if err != nil {
return err
}
}
return nil
}
func (r *abstractDefaultRule) Close() error {
for _, item := range r.allItems {
err := common.Close(item)
if err != nil {
return err
}
}
return nil
}
func (r *abstractDefaultRule) UpdateGeosite() error {
for _, item := range r.allItems {
if geositeItem, isSite := item.(*GeositeItem); isSite {
err := geositeItem.Update()
if err != nil {
return err
}
}
}
return nil
}
func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
if len(r.allItems) == 0 {
return true
}
for _, item := range r.items {
if !item.Match(metadata) {
return r.invert
}
}
if len(r.sourceAddressItems) > 0 {
var sourceAddressMatch bool
for _, item := range r.sourceAddressItems {
if item.Match(metadata) {
sourceAddressMatch = true
break
}
}
if !sourceAddressMatch {
return r.invert
}
}
if len(r.sourcePortItems) > 0 {
var sourcePortMatch bool
for _, item := range r.sourcePortItems {
if item.Match(metadata) {
sourcePortMatch = true
break
}
}
if !sourcePortMatch {
return r.invert
}
}
if len(r.destinationAddressItems) > 0 {
var destinationAddressMatch bool
for _, item := range r.destinationAddressItems {
if item.Match(metadata) {
destinationAddressMatch = true
break
}
}
if !destinationAddressMatch {
return r.invert
}
}
if len(r.destinationPortItems) > 0 {
var destinationPortMatch bool
for _, item := range r.destinationPortItems {
if item.Match(metadata) {
destinationPortMatch = true
break
}
}
if !destinationPortMatch {
return r.invert
}
}
return !r.invert
}
func (r *abstractDefaultRule) Outbound() string {
return r.outbound
}
func (r *abstractDefaultRule) String() string {
if !r.invert {
return strings.Join(F.MapToString(r.allItems), " ")
} else {
return "!(" + strings.Join(F.MapToString(r.allItems), " ") + ")"
}
}
type abstractLogicalRule struct {
rules []adapter.Rule
mode string
invert bool
outbound string
}
func (r *abstractLogicalRule) Type() string {
return C.RuleTypeLogical
}
func (r *abstractLogicalRule) UpdateGeosite() error {
for _, rule := range r.rules {
err := rule.UpdateGeosite()
if err != nil {
return err
}
}
return nil
}
func (r *abstractLogicalRule) Start() error {
for _, rule := range r.rules {
err := rule.Start()
if err != nil {
return err
}
}
return nil
}
func (r *abstractLogicalRule) Close() error {
for _, rule := range r.rules {
err := rule.Close()
if err != nil {
return err
}
}
return nil
}
func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool {
if r.mode == C.LogicalTypeAnd {
return common.All(r.rules, func(it adapter.Rule) bool {
return it.Match(metadata)
}) != r.invert
} else {
return common.Any(r.rules, func(it adapter.Rule) bool {
return it.Match(metadata)
}) != r.invert
}
}
func (r *abstractLogicalRule) Outbound() string {
return r.outbound
}
func (r *abstractLogicalRule) String() string {
var op string
switch r.mode {
case C.LogicalTypeAnd:
op = "&&"
case C.LogicalTypeOr:
op = "||"
}
if !r.invert {
return strings.Join(F.MapToString(r.rules), " "+op+" ")
} else {
return "!(" + strings.Join(F.MapToString(r.rules), " "+op+" ") + ")"
}
}

View File

@ -1,16 +1,11 @@
package route
import (
"strings"
"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"
F "github.com/sagernet/sing/common/format"
N "github.com/sagernet/sing/common/network"
)
func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rule) (adapter.Rule, error) {
@ -39,14 +34,7 @@ func NewRule(router adapter.Router, logger log.ContextLogger, options option.Rul
var _ adapter.Rule = (*DefaultRule)(nil)
type DefaultRule struct {
items []RuleItem
sourceAddressItems []RuleItem
sourcePortItems []RuleItem
destinationAddressItems []RuleItem
destinationPortItems []RuleItem
allItems []RuleItem
invert bool
outbound string
abstractDefaultRule
}
type RuleItem interface {
@ -56,8 +44,10 @@ type RuleItem interface {
func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options option.DefaultRule) (*DefaultRule, error) {
rule := &DefaultRule{
invert: options.Invert,
outbound: options.Outbound,
abstractDefaultRule{
invert: options.Invert,
outbound: options.Outbound,
},
}
if len(options.Inbound) > 0 {
item := NewInboundRule(options.Inbound)
@ -74,15 +64,10 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt
return nil, E.New("invalid ip version: ", options.IPVersion)
}
}
if options.Network != "" {
switch options.Network {
case N.NetworkTCP, N.NetworkUDP:
item := NewNetworkItem(options.Network)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
default:
return nil, E.New("invalid network: ", options.Network)
}
if len(options.Network) > 0 {
item := NewNetworkItem(options.Network)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.AuthUser) > 0 {
item := NewAuthUserItem(options.AuthUser)
@ -202,130 +187,19 @@ func NewDefaultRule(router adapter.Router, logger log.ContextLogger, options opt
return rule, nil
}
func (r *DefaultRule) Type() string {
return C.RuleTypeDefault
}
func (r *DefaultRule) Start() error {
for _, item := range r.allItems {
err := common.Start(item)
if err != nil {
return err
}
}
return nil
}
func (r *DefaultRule) Close() error {
for _, item := range r.allItems {
err := common.Close(item)
if err != nil {
return err
}
}
return nil
}
func (r *DefaultRule) UpdateGeosite() error {
for _, item := range r.allItems {
if geositeItem, isSite := item.(*GeositeItem); isSite {
err := geositeItem.Update()
if err != nil {
return err
}
}
}
return nil
}
func (r *DefaultRule) Match(metadata *adapter.InboundContext) bool {
for _, item := range r.items {
if !item.Match(metadata) {
return r.invert
}
}
if len(r.sourceAddressItems) > 0 {
var sourceAddressMatch bool
for _, item := range r.sourceAddressItems {
if item.Match(metadata) {
sourceAddressMatch = true
break
}
}
if !sourceAddressMatch {
return r.invert
}
}
if len(r.sourcePortItems) > 0 {
var sourcePortMatch bool
for _, item := range r.sourcePortItems {
if item.Match(metadata) {
sourcePortMatch = true
break
}
}
if !sourcePortMatch {
return r.invert
}
}
if len(r.destinationAddressItems) > 0 {
var destinationAddressMatch bool
for _, item := range r.destinationAddressItems {
if item.Match(metadata) {
destinationAddressMatch = true
break
}
}
if !destinationAddressMatch {
return r.invert
}
}
if len(r.destinationPortItems) > 0 {
var destinationPortMatch bool
for _, item := range r.destinationPortItems {
if item.Match(metadata) {
destinationPortMatch = true
break
}
}
if !destinationPortMatch {
return r.invert
}
}
return !r.invert
}
func (r *DefaultRule) Outbound() string {
return r.outbound
}
func (r *DefaultRule) String() string {
if !r.invert {
return strings.Join(F.MapToString(r.allItems), " ")
} else {
return "!(" + strings.Join(F.MapToString(r.allItems), " ") + ")"
}
}
var _ adapter.Rule = (*LogicalRule)(nil)
type LogicalRule struct {
mode string
rules []*DefaultRule
invert bool
outbound string
abstractLogicalRule
}
func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) {
r := &LogicalRule{
rules: make([]*DefaultRule, len(options.Rules)),
invert: options.Invert,
outbound: options.Outbound,
abstractLogicalRule{
rules: make([]adapter.Rule, len(options.Rules)),
invert: options.Invert,
outbound: options.Outbound,
},
}
switch options.Mode {
case C.LogicalTypeAnd:
@ -344,68 +218,3 @@ func NewLogicalRule(router adapter.Router, logger log.ContextLogger, options opt
}
return r, nil
}
func (r *LogicalRule) Type() string {
return C.RuleTypeLogical
}
func (r *LogicalRule) UpdateGeosite() error {
for _, rule := range r.rules {
err := rule.UpdateGeosite()
if err != nil {
return err
}
}
return nil
}
func (r *LogicalRule) Start() error {
for _, rule := range r.rules {
err := rule.Start()
if err != nil {
return err
}
}
return nil
}
func (r *LogicalRule) Close() error {
for _, rule := range r.rules {
err := rule.Close()
if err != nil {
return err
}
}
return nil
}
func (r *LogicalRule) Match(metadata *adapter.InboundContext) bool {
if r.mode == C.LogicalTypeAnd {
return common.All(r.rules, func(it *DefaultRule) bool {
return it.Match(metadata)
}) != r.invert
} else {
return common.Any(r.rules, func(it *DefaultRule) bool {
return it.Match(metadata)
}) != r.invert
}
}
func (r *LogicalRule) Outbound() string {
return r.outbound
}
func (r *LogicalRule) String() string {
var op string
switch r.mode {
case C.LogicalTypeAnd:
op = "&&"
case C.LogicalTypeOr:
op = "||"
}
if !r.invert {
return strings.Join(F.MapToString(r.rules), " "+op+" ")
} else {
return "!(" + strings.Join(F.MapToString(r.rules), " "+op+" ") + ")"
}
}

View File

@ -1,16 +1,11 @@
package route
import (
"strings"
"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"
F "github.com/sagernet/sing/common/format"
N "github.com/sagernet/sing/common/network"
)
func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option.DNSRule) (adapter.DNSRule, error) {
@ -39,22 +34,19 @@ func NewDNSRule(router adapter.Router, logger log.ContextLogger, options option.
var _ adapter.DNSRule = (*DefaultDNSRule)(nil)
type DefaultDNSRule struct {
items []RuleItem
sourceAddressItems []RuleItem
sourcePortItems []RuleItem
destinationAddressItems []RuleItem
destinationPortItems []RuleItem
allItems []RuleItem
invert bool
outbound string
disableCache bool
abstractDefaultRule
disableCache bool
rewriteTTL *uint32
}
func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options option.DefaultDNSRule) (*DefaultDNSRule, error) {
rule := &DefaultDNSRule{
invert: options.Invert,
outbound: options.Server,
abstractDefaultRule: abstractDefaultRule{
invert: options.Invert,
outbound: options.Server,
},
disableCache: options.DisableCache,
rewriteTTL: options.RewriteTTL,
}
if len(options.Inbound) > 0 {
item := NewInboundRule(options.Inbound)
@ -76,15 +68,10 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if options.Network != "" {
switch options.Network {
case N.NetworkTCP, N.NetworkUDP:
item := NewNetworkItem(options.Network)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
default:
return nil, E.New("invalid network: ", options.Network)
}
if len(options.Network) > 0 {
item := NewNetworkItem(options.Network)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.AuthUser) > 0 {
item := NewAuthUserItem(options.AuthUser)
@ -196,132 +183,31 @@ func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options
return rule, nil
}
func (r *DefaultDNSRule) Type() string {
return C.RuleTypeDefault
}
func (r *DefaultDNSRule) Start() error {
for _, item := range r.allItems {
err := common.Start(item)
if err != nil {
return err
}
}
return nil
}
func (r *DefaultDNSRule) Close() error {
for _, item := range r.allItems {
err := common.Close(item)
if err != nil {
return err
}
}
return nil
}
func (r *DefaultDNSRule) UpdateGeosite() error {
for _, item := range r.allItems {
if geositeItem, isSite := item.(*GeositeItem); isSite {
err := geositeItem.Update()
if err != nil {
return err
}
}
}
return nil
}
func (r *DefaultDNSRule) Match(metadata *adapter.InboundContext) bool {
for _, item := range r.items {
if !item.Match(metadata) {
return r.invert
}
}
if len(r.sourceAddressItems) > 0 {
var sourceAddressMatch bool
for _, item := range r.sourceAddressItems {
if item.Match(metadata) {
sourceAddressMatch = true
break
}
}
if !sourceAddressMatch {
return r.invert
}
}
if len(r.sourcePortItems) > 0 {
var sourcePortMatch bool
for _, item := range r.sourcePortItems {
if item.Match(metadata) {
sourcePortMatch = true
break
}
}
if !sourcePortMatch {
return r.invert
}
}
if len(r.destinationAddressItems) > 0 {
var destinationAddressMatch bool
for _, item := range r.destinationAddressItems {
if item.Match(metadata) {
destinationAddressMatch = true
break
}
}
if !destinationAddressMatch {
return r.invert
}
}
if len(r.destinationPortItems) > 0 {
var destinationPortMatch bool
for _, item := range r.destinationPortItems {
if item.Match(metadata) {
destinationPortMatch = true
break
}
}
if !destinationPortMatch {
return r.invert
}
}
return !r.invert
}
func (r *DefaultDNSRule) Outbound() string {
return r.outbound
}
func (r *DefaultDNSRule) DisableCache() bool {
return r.disableCache
}
func (r *DefaultDNSRule) String() string {
return strings.Join(F.MapToString(r.allItems), " ")
func (r *DefaultDNSRule) RewriteTTL() *uint32 {
return r.rewriteTTL
}
var _ adapter.DNSRule = (*LogicalDNSRule)(nil)
type LogicalDNSRule struct {
mode string
rules []*DefaultDNSRule
invert bool
outbound string
abstractLogicalRule
disableCache bool
rewriteTTL *uint32
}
func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) {
r := &LogicalDNSRule{
rules: make([]*DefaultDNSRule, len(options.Rules)),
invert: options.Invert,
outbound: options.Server,
abstractLogicalRule: abstractLogicalRule{
rules: make([]adapter.Rule, len(options.Rules)),
invert: options.Invert,
outbound: options.Server,
},
disableCache: options.DisableCache,
rewriteTTL: options.RewriteTTL,
}
switch options.Mode {
case C.LogicalTypeAnd:
@ -341,71 +227,10 @@ func NewLogicalDNSRule(router adapter.Router, logger log.ContextLogger, options
return r, nil
}
func (r *LogicalDNSRule) Type() string {
return C.RuleTypeLogical
}
func (r *LogicalDNSRule) UpdateGeosite() error {
for _, rule := range r.rules {
err := rule.UpdateGeosite()
if err != nil {
return err
}
}
return nil
}
func (r *LogicalDNSRule) Start() error {
for _, rule := range r.rules {
err := rule.Start()
if err != nil {
return err
}
}
return nil
}
func (r *LogicalDNSRule) Close() error {
for _, rule := range r.rules {
err := rule.Close()
if err != nil {
return err
}
}
return nil
}
func (r *LogicalDNSRule) Match(metadata *adapter.InboundContext) bool {
if r.mode == C.LogicalTypeAnd {
return common.All(r.rules, func(it *DefaultDNSRule) bool {
return it.Match(metadata)
}) != r.invert
} else {
return common.Any(r.rules, func(it *DefaultDNSRule) bool {
return it.Match(metadata)
}) != r.invert
}
}
func (r *LogicalDNSRule) Outbound() string {
return r.outbound
}
func (r *LogicalDNSRule) DisableCache() bool {
return r.disableCache
}
func (r *LogicalDNSRule) String() string {
var op string
switch r.mode {
case C.LogicalTypeAnd:
op = "&&"
case C.LogicalTypeOr:
op = "||"
}
if !r.invert {
return strings.Join(F.MapToString(r.rules), " "+op+" ")
} else {
return "!(" + strings.Join(F.MapToString(r.rules), " "+op+" ") + ")"
}
func (r *LogicalDNSRule) RewriteTTL() *uint32 {
return r.rewriteTTL
}

189
route/rule_ip.go Normal file
View File

@ -0,0 +1,189 @@
package route
import (
"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-tun"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
func NewIPRule(router adapter.Router, logger log.ContextLogger, options option.IPRule) (adapter.IPRule, error) {
switch options.Type {
case "", C.RuleTypeDefault:
if !options.DefaultOptions.IsValid() {
return nil, E.New("missing conditions")
}
if common.IsEmpty(options.DefaultOptions.Action) {
return nil, E.New("missing action")
}
return NewDefaultIPRule(router, logger, options.DefaultOptions)
case C.RuleTypeLogical:
if !options.LogicalOptions.IsValid() {
return nil, E.New("missing conditions")
}
if common.IsEmpty(options.DefaultOptions.Action) {
return nil, E.New("missing action")
}
return NewLogicalIPRule(router, logger, options.LogicalOptions)
default:
return nil, E.New("unknown rule type: ", options.Type)
}
}
var _ adapter.IPRule = (*DefaultIPRule)(nil)
type DefaultIPRule struct {
abstractDefaultRule
action tun.ActionType
}
func NewDefaultIPRule(router adapter.Router, logger log.ContextLogger, options option.DefaultIPRule) (*DefaultIPRule, error) {
rule := &DefaultIPRule{
abstractDefaultRule: abstractDefaultRule{
invert: options.Invert,
outbound: options.Outbound,
},
action: tun.ActionType(options.Action),
}
if len(options.Inbound) > 0 {
item := NewInboundRule(options.Inbound)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if options.IPVersion > 0 {
switch options.IPVersion {
case 4, 6:
item := NewIPVersionItem(options.IPVersion == 6)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
default:
return nil, E.New("invalid ip version: ", options.IPVersion)
}
}
if len(options.Network) > 0 {
item := NewNetworkItem(options.Network)
rule.items = append(rule.items, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 {
item := NewDomainItem(options.Domain, options.DomainSuffix)
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.DomainKeyword) > 0 {
item := NewDomainKeywordItem(options.DomainKeyword)
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.DomainRegex) > 0 {
item, err := NewDomainRegexItem(options.DomainRegex)
if err != nil {
return nil, E.Cause(err, "domain_regex")
}
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.Geosite) > 0 {
item := NewGeositeItem(router, logger, options.Geosite)
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.SourceGeoIP) > 0 {
item := NewGeoIPItem(router, logger, true, options.SourceGeoIP)
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.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.SourceIPCIDR) > 0 {
item, err := NewIPCIDRItem(true, options.SourceIPCIDR)
if err != nil {
return nil, E.Cause(err, "source_ipcidr")
}
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, "ipcidr")
}
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.SourcePort) > 0 {
item := NewPortItem(true, options.SourcePort)
rule.sourcePortItems = append(rule.sourcePortItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.SourcePortRange) > 0 {
item, err := NewPortRangeItem(true, options.SourcePortRange)
if err != nil {
return nil, E.Cause(err, "source_port_range")
}
rule.sourcePortItems = append(rule.sourcePortItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.Port) > 0 {
item := NewPortItem(false, options.Port)
rule.destinationPortItems = append(rule.destinationPortItems, item)
rule.allItems = append(rule.allItems, item)
}
if len(options.PortRange) > 0 {
item, err := NewPortRangeItem(false, options.PortRange)
if err != nil {
return nil, E.Cause(err, "port_range")
}
rule.destinationPortItems = append(rule.destinationPortItems, item)
rule.allItems = append(rule.allItems, item)
}
return rule, nil
}
func (r *DefaultIPRule) Action() tun.ActionType {
return r.action
}
var _ adapter.IPRule = (*LogicalIPRule)(nil)
type LogicalIPRule struct {
abstractLogicalRule
action tun.ActionType
}
func NewLogicalIPRule(router adapter.Router, logger log.ContextLogger, options option.LogicalIPRule) (*LogicalIPRule, error) {
r := &LogicalIPRule{
abstractLogicalRule: abstractLogicalRule{
rules: make([]adapter.Rule, len(options.Rules)),
invert: options.Invert,
outbound: options.Outbound,
},
action: tun.ActionType(options.Action),
}
switch options.Mode {
case C.LogicalTypeAnd:
r.mode = C.LogicalTypeAnd
case C.LogicalTypeOr:
r.mode = C.LogicalTypeOr
default:
return nil, E.New("unknown logical mode: ", options.Mode)
}
for i, subRule := range options.Rules {
rule, err := NewDefaultIPRule(router, logger, subRule)
if err != nil {
return nil, E.Cause(err, "sub rule[", i, "]")
}
r.rules[i] = rule
}
return r, nil
}
func (r *LogicalIPRule) Action() tun.ActionType {
return r.action
}

View File

@ -0,0 +1,42 @@
package route
import (
"strings"
"github.com/sagernet/sing-box/adapter"
F "github.com/sagernet/sing/common/format"
)
var _ RuleItem = (*NetworkItem)(nil)
type NetworkItem struct {
networks []string
networkMap map[string]bool
}
func NewNetworkItem(networks []string) *NetworkItem {
networkMap := make(map[string]bool)
for _, network := range networks {
networkMap[network] = true
}
return &NetworkItem{
networks: networks,
networkMap: networkMap,
}
}
func (r *NetworkItem) Match(metadata *adapter.InboundContext) bool {
return r.networkMap[metadata.Network]
}
func (r *NetworkItem) String() string {
description := "network="
pLen := len(r.networks)
if pLen == 1 {
description += F.ToString(r.networks[0])
} else {
description += "[" + strings.Join(F.MapToString(r.networks), " ") + "]"
}
return description
}

View File

@ -1,23 +0,0 @@
package route
import (
"github.com/sagernet/sing-box/adapter"
)
var _ RuleItem = (*NetworkItem)(nil)
type NetworkItem struct {
network string
}
func NewNetworkItem(network string) *NetworkItem {
return &NetworkItem{network}
}
func (r *NetworkItem) Match(metadata *adapter.InboundContext) bool {
return r.network == metadata.Network
}
func (r *NetworkItem) String() string {
return "network=" + r.network
}

View File

@ -0,0 +1,44 @@
package fakeip
import (
"net/netip"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common/cache"
)
var _ adapter.FakeIPStorage = (*MemoryStorage)(nil)
type MemoryStorage struct {
metadata *adapter.FakeIPMetadata
domainCache *cache.LruCache[netip.Addr, string]
}
func NewMemoryStorage() *MemoryStorage {
return &MemoryStorage{
domainCache: cache.New[netip.Addr, string](),
}
}
func (s *MemoryStorage) FakeIPMetadata() *adapter.FakeIPMetadata {
return s.metadata
}
func (s *MemoryStorage) FakeIPSaveMetadata(metadata *adapter.FakeIPMetadata) error {
s.metadata = metadata
return nil
}
func (s *MemoryStorage) FakeIPStore(address netip.Addr, domain string) error {
s.domainCache.Store(address, domain)
return nil
}
func (s *MemoryStorage) FakeIPLoad(address netip.Addr) (string, bool) {
return s.domainCache.Load(address)
}
func (s *MemoryStorage) FakeIPReset() error {
s.domainCache = cache.New[netip.Addr, string]()
return nil
}

View File

@ -0,0 +1,42 @@
package fakeip
import (
"github.com/sagernet/sing/common/buf"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
var _ N.PacketConn = (*NATPacketConn)(nil)
type NATPacketConn struct {
N.PacketConn
origin M.Socksaddr
destination M.Socksaddr
}
func NewNATPacketConn(conn N.PacketConn, origin M.Socksaddr, destination M.Socksaddr) *NATPacketConn {
return &NATPacketConn{
PacketConn: conn,
origin: origin,
destination: destination,
}
}
func (c *NATPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
destination, err = c.PacketConn.ReadPacket(buffer)
if destination == c.origin {
destination = c.destination
}
return
}
func (c *NATPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
if destination == c.destination {
destination = c.origin
}
return c.PacketConn.WritePacket(buffer, destination)
}
func (c *NATPacketConn) Upstream() any {
return c.PacketConn
}

View File

@ -0,0 +1,83 @@
package fakeip
import (
"context"
"net/netip"
"os"
"github.com/sagernet/sing-box/adapter"
"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"
)
var _ dns.Transport = (*Server)(nil)
func init() {
dns.RegisterTransport([]string{"fakeip"}, NewTransport)
}
type Server struct {
name string
router adapter.Router
store adapter.FakeIPStore
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)
if router == nil {
return nil, E.New("missing router in context")
}
return &Server{
name: name,
router: router,
logger: logger,
}, nil
}
func (s *Server) Name() string {
return s.name
}
func (s *Server) Start() error {
s.store = s.router.FakeIPStore()
if s.store == nil {
return E.New("fakeip not enabled")
}
return nil
}
func (s *Server) Close() error {
return nil
}
func (s *Server) Raw() bool {
return false
}
func (s *Server) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
return nil, os.ErrInvalid
}
func (s *Server) Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error) {
var addresses []netip.Addr
if strategy != dns.DomainStrategyUseIPv6 {
inet4Address, err := s.store.Create(domain, dns.DomainStrategyUseIPv4)
if err != nil {
return nil, err
}
addresses = append(addresses, inet4Address)
}
if strategy != dns.DomainStrategyUseIPv4 {
inet6Address, err := s.store.Create(domain, dns.DomainStrategyUseIPv6)
if err != nil {
return nil, err
}
addresses = append(addresses, inet6Address)
}
return addresses, nil
}

108
transport/fakeip/store.go Normal file
View File

@ -0,0 +1,108 @@
package fakeip
import (
"net/netip"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-dns"
E "github.com/sagernet/sing/common/exceptions"
)
var _ adapter.FakeIPStore = (*Store)(nil)
type Store struct {
router adapter.Router
inet4Range netip.Prefix
inet6Range netip.Prefix
storage adapter.FakeIPStorage
inet4Current netip.Addr
inet6Current netip.Addr
}
func NewStore(router adapter.Router, inet4Range netip.Prefix, inet6Range netip.Prefix) *Store {
return &Store{
router: router,
inet4Range: inet4Range,
inet6Range: inet6Range,
}
}
func (s *Store) Start() error {
var storage adapter.FakeIPStorage
if clashServer := s.router.ClashServer(); clashServer != nil && clashServer.StoreFakeIP() {
if cacheFile := clashServer.CacheFile(); cacheFile != nil {
storage = cacheFile
}
}
if storage == nil {
storage = NewMemoryStorage()
}
metadata := storage.FakeIPMetadata()
if metadata != nil && metadata.Inet4Range == s.inet4Range && metadata.Inet6Range == s.inet6Range {
s.inet4Current = metadata.Inet4Current
s.inet6Current = metadata.Inet6Current
} else {
if s.inet4Range.IsValid() {
s.inet4Current = s.inet4Range.Addr().Next().Next()
}
if s.inet6Range.IsValid() {
s.inet6Current = s.inet6Range.Addr().Next().Next()
}
}
s.storage = storage
return nil
}
func (s *Store) Contains(address netip.Addr) bool {
return s.inet4Range.Contains(address) || s.inet6Range.Contains(address)
}
func (s *Store) Close() error {
if s.storage == nil {
return nil
}
return s.storage.FakeIPSaveMetadata(&adapter.FakeIPMetadata{
Inet4Range: s.inet4Range,
Inet6Range: s.inet6Range,
Inet4Current: s.inet4Current,
Inet6Current: s.inet6Current,
})
}
func (s *Store) Create(domain string, strategy dns.DomainStrategy) (netip.Addr, error) {
var address netip.Addr
if strategy == dns.DomainStrategyUseIPv4 {
if !s.inet4Current.IsValid() {
return netip.Addr{}, E.New("missing IPv4 fakeip address range")
}
nextAddress := s.inet4Current.Next()
if !s.inet4Range.Contains(nextAddress) {
nextAddress = s.inet4Range.Addr().Next().Next()
}
s.inet4Current = nextAddress
address = nextAddress
} else {
if !s.inet6Current.IsValid() {
return netip.Addr{}, E.New("missing IPv6 fakeip address range")
}
nextAddress := s.inet6Current.Next()
if !s.inet6Range.Contains(nextAddress) {
nextAddress = s.inet6Range.Addr().Next().Next()
}
s.inet6Current = nextAddress
address = nextAddress
}
err := s.storage.FakeIPStore(address, domain)
if err != nil {
return netip.Addr{}, err
}
return address, nil
}
func (s *Store) Lookup(address netip.Addr) (string, bool) {
return s.storage.FakeIPLoad(address)
}
func (s *Store) Reset() error {
return s.storage.FakeIPReset()
}

View File

@ -3,9 +3,12 @@ package wireguard
import (
"context"
"net"
"net/netip"
"sync"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/wireguard-go/conn"
@ -14,24 +17,32 @@ import (
var _ conn.Bind = (*ClientBind)(nil)
type ClientBind struct {
ctx context.Context
dialer N.Dialer
peerAddr M.Socksaddr
reserved [3]uint8
connAccess sync.Mutex
conn *wireConn
done chan struct{}
ctx context.Context
dialer N.Dialer
reservedForEndpoint map[M.Socksaddr][3]uint8
connAccess sync.Mutex
conn *wireConn
done chan struct{}
isConnect bool
connectAddr M.Socksaddr
reserved [3]uint8
}
func NewClientBind(ctx context.Context, dialer N.Dialer, peerAddr M.Socksaddr, reserved [3]uint8) *ClientBind {
func NewClientBind(ctx context.Context, dialer N.Dialer, isConnect bool, connectAddr M.Socksaddr, reserved [3]uint8) *ClientBind {
return &ClientBind{
ctx: ctx,
dialer: dialer,
peerAddr: peerAddr,
reserved: reserved,
ctx: ctx,
dialer: dialer,
reservedForEndpoint: make(map[M.Socksaddr][3]uint8),
isConnect: isConnect,
connectAddr: connectAddr,
reserved: reserved,
}
}
func (c *ClientBind) SetReservedForEndpoint(destination M.Socksaddr, reserved [3]byte) {
c.reservedForEndpoint[destination] = reserved
}
func (c *ClientBind) connect() (*wireConn, error) {
serverConn := c.conn
if serverConn != nil {
@ -53,13 +64,27 @@ func (c *ClientBind) connect() (*wireConn, error) {
return serverConn, nil
}
}
udpConn, err := c.dialer.DialContext(c.ctx, "udp", c.peerAddr)
if err != nil {
return nil, &wireError{err}
}
c.conn = &wireConn{
Conn: udpConn,
done: make(chan struct{}),
if c.isConnect {
udpConn, err := c.dialer.DialContext(c.ctx, N.NetworkUDP, c.connectAddr)
if err != nil {
return nil, &wireError{err}
}
c.conn = &wireConn{
NetPacketConn: &bufio.UnbindPacketConn{
ExtendedConn: bufio.NewExtendedConn(udpConn),
Addr: c.connectAddr,
},
done: make(chan struct{}),
}
} else {
udpConn, err := c.dialer.ListenPacket(c.ctx, M.Socksaddr{Addr: netip.IPv4Unspecified()})
if err != nil {
return nil, &wireError{err}
}
c.conn = &wireConn{
NetPacketConn: bufio.NewPacketConn(udpConn),
done: make(chan struct{}),
}
}
return c.conn, nil
}
@ -80,7 +105,8 @@ func (c *ClientBind) receive(b []byte) (n int, ep conn.Endpoint, err error) {
err = &wireError{err}
return
}
n, err = udpConn.Read(b)
buffer := buf.With(b)
destination, err := udpConn.ReadPacket(buffer)
if err != nil {
udpConn.Close()
select {
@ -90,12 +116,16 @@ func (c *ClientBind) receive(b []byte) (n int, ep conn.Endpoint, err error) {
}
return
}
n = buffer.Len()
if buffer.Start() > 0 {
copy(b, buffer.Bytes())
}
if n > 3 {
b[1] = 0
b[2] = 0
b[3] = 0
}
ep = Endpoint(c.peerAddr)
ep = Endpoint(destination)
return
}
@ -127,12 +157,17 @@ func (c *ClientBind) Send(b []byte, ep conn.Endpoint) error {
if err != nil {
return err
}
destination := M.Socksaddr(ep.(Endpoint))
if len(b) > 3 {
b[1] = c.reserved[0]
b[2] = c.reserved[1]
b[3] = c.reserved[2]
reserved, loaded := c.reservedForEndpoint[destination]
if !loaded {
reserved = c.reserved
}
b[1] = reserved[0]
b[2] = reserved[1]
b[3] = reserved[2]
}
_, err = udpConn.Write(b)
err = udpConn.WritePacket(buf.As(b), destination)
if err != nil {
udpConn.Close()
}
@ -140,15 +175,11 @@ func (c *ClientBind) Send(b []byte, ep conn.Endpoint) error {
}
func (c *ClientBind) ParseEndpoint(s string) (conn.Endpoint, error) {
return Endpoint(c.peerAddr), nil
}
func (c *ClientBind) Endpoint() conn.Endpoint {
return Endpoint(c.peerAddr)
return Endpoint(M.ParseSocksaddr(s)), nil
}
type wireConn struct {
net.Conn
N.NetPacketConn
access sync.Mutex
done chan struct{}
}
@ -161,7 +192,7 @@ func (w *wireConn) Close() error {
return net.ErrClosed
default:
}
w.Conn.Close()
w.NetPacketConn.Close()
close(w.done)
return nil
}

View File

@ -1,13 +1,23 @@
package wireguard
import (
"net/netip"
"github.com/sagernet/sing-tun"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/wireguard-go/tun"
wgTun "github.com/sagernet/wireguard-go/tun"
)
type Device interface {
tun.Device
wgTun.Device
N.Dialer
Start() error
Inet4Address() netip.Addr
Inet6Address() netip.Addr
// NewEndpoint() (stack.LinkEndpoint, error)
}
type NatDevice interface {
Device
CreateDestination(session tun.RouteSession, conn tun.RouteContext) tun.DirectDestination
}

View File

@ -0,0 +1,75 @@
package wireguard
import (
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/buf"
)
var _ Device = (*natDeviceWrapper)(nil)
type natDeviceWrapper struct {
Device
outbound chan *buf.Buffer
mapping *tun.NatMapping
writer *tun.NatWriter
}
func NewNATDevice(upstream Device, ipRewrite bool) NatDevice {
wrapper := &natDeviceWrapper{
Device: upstream,
outbound: make(chan *buf.Buffer, 256),
mapping: tun.NewNatMapping(ipRewrite),
}
if ipRewrite {
wrapper.writer = tun.NewNatWriter(upstream.Inet4Address(), upstream.Inet6Address())
}
return wrapper
}
func (d *natDeviceWrapper) Read(p []byte, offset int) (int, error) {
select {
case packet := <-d.outbound:
defer packet.Release()
return copy(p[offset:], packet.Bytes()), nil
default:
}
return d.Device.Read(p, offset)
}
func (d *natDeviceWrapper) Write(p []byte, offset int) (int, error) {
packet := p[offset:]
handled, err := d.mapping.WritePacket(packet)
if handled {
return len(packet), err
}
return d.Device.Write(p, offset)
}
func (d *natDeviceWrapper) CreateDestination(session tun.RouteSession, conn tun.RouteContext) tun.DirectDestination {
d.mapping.CreateSession(session, conn)
return &natDestinationWrapper{d, session}
}
var _ tun.DirectDestination = (*natDestinationWrapper)(nil)
type natDestinationWrapper struct {
device *natDeviceWrapper
session tun.RouteSession
}
func (d *natDestinationWrapper) WritePacket(buffer *buf.Buffer) error {
if d.device.writer != nil {
d.device.writer.RewritePacket(buffer.Bytes())
}
d.device.outbound <- buffer
return nil
}
func (d *natDestinationWrapper) Close() error {
d.device.mapping.DeleteSession(d.session)
return nil
}
func (d *natDestinationWrapper) Timeout() bool {
return false
}

View File

@ -0,0 +1,27 @@
//go:build with_gvisor
package wireguard
import (
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
"gvisor.dev/gvisor/pkg/tcpip/stack"
)
func (d *natDestinationWrapper) WritePacketBuffer(buffer *stack.PacketBuffer) error {
defer buffer.DecRef()
if d.device.writer != nil {
d.device.writer.RewritePacketBuffer(buffer)
}
var packetLen int
for _, slice := range buffer.AsSlices() {
packetLen += len(slice)
}
packet := buf.NewSize(packetLen)
for _, slice := range buffer.AsSlices() {
common.Must1(packet.Write(slice))
}
d.device.outbound <- packet
return nil
}

View File

@ -8,10 +8,12 @@ import (
"net/netip"
"os"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/wireguard-go/tun"
wgTun "github.com/sagernet/wireguard-go/tun"
"gvisor.dev/gvisor/pkg/bufferv2"
"gvisor.dev/gvisor/pkg/tcpip"
@ -25,33 +27,38 @@ import (
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
)
var _ Device = (*StackDevice)(nil)
var _ NatDevice = (*StackDevice)(nil)
const defaultNIC tcpip.NICID = 1
type StackDevice struct {
stack *stack.Stack
mtu uint32
events chan tun.Event
outbound chan *stack.PacketBuffer
done chan struct{}
dispatcher stack.NetworkDispatcher
addr4 tcpip.Address
addr6 tcpip.Address
stack *stack.Stack
mtu uint32
events chan wgTun.Event
outbound chan *stack.PacketBuffer
packetOutbound chan *buf.Buffer
done chan struct{}
dispatcher stack.NetworkDispatcher
addr4 tcpip.Address
addr6 tcpip.Address
mapping *tun.NatMapping
writer *tun.NatWriter
}
func NewStackDevice(localAddresses []netip.Prefix, mtu uint32) (*StackDevice, error) {
func NewStackDevice(localAddresses []netip.Prefix, mtu uint32, ipRewrite bool) (*StackDevice, error) {
ipStack := stack.New(stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol},
TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol4, icmp.NewProtocol6},
HandleLocal: true,
})
tunDevice := &StackDevice{
stack: ipStack,
mtu: mtu,
events: make(chan tun.Event, 1),
outbound: make(chan *stack.PacketBuffer, 256),
done: make(chan struct{}),
stack: ipStack,
mtu: mtu,
events: make(chan wgTun.Event, 1),
outbound: make(chan *stack.PacketBuffer, 256),
packetOutbound: make(chan *buf.Buffer, 256),
done: make(chan struct{}),
mapping: tun.NewNatMapping(ipRewrite),
}
err := ipStack.CreateNIC(defaultNIC, (*wireEndpoint)(tunDevice))
if err != nil {
@ -77,6 +84,9 @@ func NewStackDevice(localAddresses []netip.Prefix, mtu uint32) (*StackDevice, er
return nil, E.New("parse local address ", protoAddr.AddressWithPrefix, ": ", err.String())
}
}
if ipRewrite {
tunDevice.writer = tun.NewNatWriter(tunDevice.Inet4Address(), tunDevice.Inet6Address())
}
sOpt := tcpip.TCPSACKEnabled(true)
ipStack.SetTransportProtocolOption(tcp.ProtocolNumber, &sOpt)
cOpt := tcpip.CongestionControlOption("cubic")
@ -144,8 +154,16 @@ func (w *StackDevice) ListenPacket(ctx context.Context, destination M.Socksaddr)
return udpConn, nil
}
func (w *StackDevice) Inet4Address() netip.Addr {
return M.AddrFromIP(net.IP(w.addr4))
}
func (w *StackDevice) Inet6Address() netip.Addr {
return M.AddrFromIP(net.IP(w.addr6))
}
func (w *StackDevice) Start() error {
w.events <- tun.EventUp
w.events <- wgTun.EventUp
return nil
}
@ -165,6 +183,10 @@ func (w *StackDevice) Read(p []byte, offset int) (n int, err error) {
n += copy(p[n:], slice)
}
return
case packet := <-w.packetOutbound:
defer packet.Release()
n = copy(p[offset:], packet.Bytes())
return
case <-w.done:
return 0, os.ErrClosed
}
@ -175,6 +197,10 @@ func (w *StackDevice) Write(p []byte, offset int) (n int, err error) {
if len(p) == 0 {
return
}
handled, err := w.mapping.WritePacket(p)
if handled {
return len(p), err
}
var networkProtocol tcpip.NetworkProtocolNumber
switch header.IPVersion(p) {
case header.IPv4Version:
@ -203,7 +229,7 @@ func (w *StackDevice) Name() (string, error) {
return "sing-box", nil
}
func (w *StackDevice) Events() chan tun.Event {
func (w *StackDevice) Events() chan wgTun.Event {
return w.events
}
@ -222,6 +248,44 @@ func (w *StackDevice) Close() error {
return nil
}
func (w *StackDevice) CreateDestination(session tun.RouteSession, conn tun.RouteContext) tun.DirectDestination {
w.mapping.CreateSession(session, conn)
return &stackNatDestination{
device: w,
session: session,
}
}
type stackNatDestination struct {
device *StackDevice
session tun.RouteSession
}
func (d *stackNatDestination) WritePacket(buffer *buf.Buffer) error {
if d.device.writer != nil {
d.device.writer.RewritePacket(buffer.Bytes())
}
d.device.packetOutbound <- buffer
return nil
}
func (d *stackNatDestination) WritePacketBuffer(buffer *stack.PacketBuffer) error {
if d.device.writer != nil {
d.device.writer.RewritePacketBuffer(buffer)
}
d.device.outbound <- buffer
return nil
}
func (d *stackNatDestination) Close() error {
d.device.mapping.DeleteSession(d.session)
return nil
}
func (d *stackNatDestination) Timeout() bool {
return false
}
var _ stack.LinkEndpoint = (*wireEndpoint)(nil)
type wireEndpoint StackDevice

View File

@ -8,6 +8,6 @@ import (
"github.com/sagernet/sing-tun"
)
func NewStackDevice(localAddresses []netip.Prefix, mtu uint32) (Device, error) {
func NewStackDevice(localAddresses []netip.Prefix, mtu uint32, ipRewrite bool) (Device, error) {
return nil, tun.ErrGVisorNotIncluded
}

View File

@ -23,6 +23,8 @@ type SystemDevice struct {
name string
mtu int
events chan wgTun.Event
addr4 netip.Addr
addr6 netip.Addr
}
/*func (w *SystemDevice) NewEndpoint() (stack.LinkEndpoint, error) {
@ -55,11 +57,24 @@ func NewSystemDevice(router adapter.Router, interfaceName string, localPrefixes
if err != nil {
return nil, err
}
var inet4Address netip.Addr
var inet6Address netip.Addr
if len(inet4Addresses) > 0 {
inet4Address = inet4Addresses[0].Addr()
}
if len(inet6Addresses) > 0 {
inet6Address = inet6Addresses[0].Addr()
}
return &SystemDevice{
dialer.NewDefault(router, option.DialerOptions{
dialer: dialer.NewDefault(router, option.DialerOptions{
BindInterface: interfaceName,
}),
tunInterface, interfaceName, int(mtu), make(chan wgTun.Event),
device: tunInterface,
name: interfaceName,
mtu: int(mtu),
events: make(chan wgTun.Event),
addr4: inet4Address,
addr6: inet6Address,
}, nil
}
@ -71,6 +86,14 @@ func (w *SystemDevice) ListenPacket(ctx context.Context, destination M.Socksaddr
return w.dialer.ListenPacket(ctx, destination)
}
func (w *SystemDevice) Inet4Address() netip.Addr {
return w.addr4
}
func (w *SystemDevice) Inet6Address() netip.Addr {
return w.addr6
}
func (w *SystemDevice) Start() error {
w.events <- wgTun.EventUp
return nil
@ -80,12 +103,12 @@ func (w *SystemDevice) File() *os.File {
return nil
}
func (w *SystemDevice) Read(bytes []byte, index int) (int, error) {
return w.device.Read(bytes[index-tun.PacketOffset:])
func (w *SystemDevice) Read(p []byte, offset int) (int, error) {
return w.device.Read(p[offset-tun.PacketOffset:])
}
func (w *SystemDevice) Write(bytes []byte, index int) (int, error) {
return w.device.Write(bytes[index:])
func (w *SystemDevice) Write(p []byte, offset int) (int, error) {
return w.device.Write(p[offset:])
}
func (w *SystemDevice) Flush() error {