mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-09-04 16:28:49 +08:00
Compare commits
5 Commits
dev-next
...
v1.3-beta1
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6d63f9255f | ||
![]() |
6f2cc9761d | ||
![]() |
b484d9bca6 | ||
![]() |
58c4fd745a | ||
![]() |
7d1e6affb3 |
@ -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
23
adapter/fakeip.go
Normal 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
|
||||
}
|
50
adapter/fakeip_metadata.go
Normal file
50
adapter/fakeip_metadata.go
Normal 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
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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",
|
||||
},
|
||||
},
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
25
docs/configuration/dns/fakeip.md
Normal file
25
docs/configuration/dns/fakeip.md
Normal 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.
|
25
docs/configuration/dns/fakeip.zh.md
Normal file
25
docs/configuration/dns/fakeip.zh.md
Normal 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 地址范围。
|
@ -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.
|
||||
|
@ -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) 设置。
|
||||
|
@ -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
|
||||
|
@ -243,6 +243,10 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
||||
|
||||
在此查询中禁用缓存。
|
||||
|
||||
#### rewrite_ttl
|
||||
|
||||
重写 DNS 回应中的 TTL。
|
||||
|
||||
### 逻辑字段
|
||||
|
||||
#### type
|
||||
|
@ -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 ""
|
||||
|
||||
|
@ -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 ""
|
||||
|
||||
|
@ -37,4 +37,10 @@
|
||||
|
||||
#### tag
|
||||
|
||||
The tag of the outbound.
|
||||
The tag of the outbound.
|
||||
|
||||
### Features
|
||||
|
||||
#### Outbounds that support IP connection
|
||||
|
||||
* `WireGuard`
|
||||
|
@ -36,4 +36,10 @@
|
||||
|
||||
#### tag
|
||||
|
||||
出站的标签。
|
||||
出站的标签。
|
||||
|
||||
### 特性
|
||||
|
||||
#### 支持 IP 连接的出站
|
||||
|
||||
* `WireGuard`
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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` 设置,则不生效。
|
205
docs/configuration/route/ip-rule.md
Normal file
205
docs/configuration/route/ip-rule.md
Normal 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.
|
204
docs/configuration/route/ip-rule.zh.md
Normal file
204
docs/configuration/route/ip-rule.zh.md
Normal 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
|
||||
|
||||
==必填==
|
||||
|
||||
包括的默认规则。
|
@ -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.
|
||||
|
@ -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
|
||||
|
||||
==必填==
|
||||
|
||||
目标出站的标签。
|
||||
包括的默认规则。
|
@ -8,3 +8,4 @@ Configuration examples for sing-box.
|
||||
* [Shadowsocks](./shadowsocks)
|
||||
* [ShadowTLS](./shadowtls)
|
||||
* [Clash API](./clash-api)
|
||||
* [WireGuard Direct](./wireguard-direct)
|
||||
|
@ -8,3 +8,4 @@ sing-box 的配置示例。
|
||||
* [Shadowsocks](./shadowsocks)
|
||||
* [ShadowTLS](./shadowtls)
|
||||
* [Clash API](./clash-api)
|
||||
* [WireGuard Direct](./wireguard-direct)
|
||||
|
90
docs/examples/wireguard-direct.md
Normal file
90
docs/examples/wireguard-direct.md
Normal 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
20
docs/faq/fakeip.md
Normal 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
19
docs/faq/fakeip.zh.md
Normal file
@ -0,0 +1,19 @@
|
||||
# FakeIP
|
||||
|
||||
FakeIP 是指同时劫持 DNS 和连接请求的程序中的一种行为。它通过虚拟结果响应 DNS 请求,在接受连接时恢复映射。
|
||||
|
||||
#### 优点
|
||||
|
||||
* 在像 L3 路由这样无法进行流量探测的地方检索所请求的域名,以协助路由。
|
||||
* 减少对一个域的第一个 TCP 请求的 RTT(这是最常见的原因)。
|
||||
|
||||
#### 限制
|
||||
|
||||
* 它的机制会破坏依赖于返回正确远程地址的应用程序。
|
||||
* 仅支持 A 和 AAAA(IP)请求,这可能会破坏依赖于其他请求的应用程序。
|
||||
|
||||
#### 建议
|
||||
|
||||
* 如果不需要 L3 路由,请勿使用。
|
||||
* 如果使用 tun,请确保 tun 路由中包含 FakeIP 地址范围。
|
||||
* 启用 `experimental.clash_api.store_fakeip` 以持久化 FakeIP 记录,或者使用 `dns.rules.rewrite_ttl` 避免程序重启后在 DNS 被缓存的环境中丢失记录。
|
@ -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.
|
||||
|
@ -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 的网络栈,仅实现它的传输协议是舍本逐末的。
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
77
experimental/clashapi/cachefile/fakeip.go
Normal file
77
experimental/clashapi/cachefile/fakeip.go
Normal 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)
|
||||
})
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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: 协议探测
|
||||
|
||||
|
@ -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"`
|
||||
}
|
||||
|
||||
|
121
option/dns.go
121
option/dns.go
@ -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"`
|
||||
}
|
||||
|
101
option/route.go
101
option/route.go
@ -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
101
option/rule.go
Normal 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
107
option/rule_dns.go
Normal 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
120
option/rule_ip.go
Normal 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)
|
||||
}
|
@ -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"`
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
362
route/router.go
362
route/router.go
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
283
route/router_geo_resources.go
Normal file
283
route/router_geo_resources.go
Normal 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
79
route/router_ip.go
Normal 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
207
route/rule_abstract.go
Normal 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+" ") + ")"
|
||||
}
|
||||
}
|
@ -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+" ") + ")"
|
||||
}
|
||||
}
|
@ -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
189
route/rule_ip.go
Normal 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
|
||||
}
|
42
route/rule_item_network.go
Normal file
42
route/rule_item_network.go
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
44
transport/fakeip/memory.go
Normal file
44
transport/fakeip/memory.go
Normal 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
|
||||
}
|
42
transport/fakeip/packet.go
Normal file
42
transport/fakeip/packet.go
Normal 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
|
||||
}
|
83
transport/fakeip/server.go
Normal file
83
transport/fakeip/server.go
Normal 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
108
transport/fakeip/store.go
Normal 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()
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
75
transport/wireguard/device_nat.go
Normal file
75
transport/wireguard/device_nat.go
Normal 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
|
||||
}
|
27
transport/wireguard/device_nat_gvisor.go
Normal file
27
transport/wireguard/device_nat_gvisor.go
Normal 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
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user