Implement route rules

This commit is contained in:
世界 2022-07-02 22:55:10 +08:00
parent 7c57eb70e8
commit 6eae8e361f
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
40 changed files with 1220 additions and 145 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
/.idea/ /.idea/
/vendor/ /vendor/
/*.json /*.json
/Country.mmdb

View file

@ -17,4 +17,10 @@ type InboundContext struct {
Destination M.Socksaddr Destination M.Socksaddr
Domain string Domain string
Protocol string Protocol string
// cache
SourceGeoIPCode string
GeoIPCode string
ProcessPath string
} }

View file

@ -7,6 +7,7 @@ import (
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
F "github.com/sagernet/sing/common/format" F "github.com/sagernet/sing/common/format"
) )
@ -20,15 +21,15 @@ func New(ctx context.Context, router adapter.Router, logger log.Logger, index in
inboundLogger := logger.WithPrefix(F.ToString("inbound/", options.Type, "[", tag, "]: ")) inboundLogger := logger.WithPrefix(F.ToString("inbound/", options.Type, "[", tag, "]: "))
switch options.Type { switch options.Type {
case C.TypeDirect: case C.TypeDirect:
return NewDirect(ctx, router, inboundLogger, options.Tag, options.DirectOptions), nil return NewDirect(ctx, router, inboundLogger, options.Tag, common.PtrValueOrDefault(options.DirectOptions)), nil
case C.TypeSocks: case C.TypeSocks:
return NewSocks(ctx, router, inboundLogger, options.Tag, options.SocksOptions), nil return NewSocks(ctx, router, inboundLogger, options.Tag, common.PtrValueOrDefault(options.SocksOptions)), nil
case C.TypeHTTP: case C.TypeHTTP:
return NewHTTP(ctx, router, inboundLogger, options.Tag, options.HTTPOptions), nil return NewHTTP(ctx, router, inboundLogger, options.Tag, common.PtrValueOrDefault(options.HTTPOptions)), nil
case C.TypeMixed: case C.TypeMixed:
return NewMixed(ctx, router, inboundLogger, options.Tag, options.MixedOptions), nil return NewMixed(ctx, router, inboundLogger, options.Tag, common.PtrValueOrDefault(options.MixedOptions)), nil
case C.TypeShadowsocks: case C.TypeShadowsocks:
return NewShadowsocks(ctx, router, inboundLogger, options.Tag, options.ShadowsocksOptions) return NewShadowsocks(ctx, router, inboundLogger, options.Tag, common.PtrValueOrDefault(options.ShadowsocksOptions))
default: default:
panic(F.ToString("unknown inbound type: ", options.Type)) panic(F.ToString("unknown inbound type: ", options.Type))
} }

View file

@ -233,7 +233,7 @@ func (a *myInboundAdapter) NewError(ctx context.Context, err error) {
func (a *myInboundAdapter) writePacket(buffer *buf.Buffer, destination M.Socksaddr) error { func (a *myInboundAdapter) writePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
defer buffer.Release() defer buffer.Release()
if destination.Family().IsFqdn() { if destination.IsFqdn() {
udpAddr, err := net.ResolveUDPAddr("udp", destination.String()) udpAddr, err := net.ResolveUDPAddr("udp", destination.String())
if err != nil { if err != nil {
return err return err

View file

@ -24,7 +24,7 @@ type Direct struct {
overrideDestination M.Socksaddr overrideDestination M.Socksaddr
} }
func NewDirect(ctx context.Context, router adapter.Router, logger log.Logger, tag string, options *option.DirectInboundOptions) *Direct { func NewDirect(ctx context.Context, router adapter.Router, logger log.Logger, tag string, options option.DirectInboundOptions) *Direct {
inbound := &Direct{ inbound := &Direct{
myInboundAdapter: myInboundAdapter{ myInboundAdapter: myInboundAdapter{
protocol: C.TypeDirect, protocol: C.TypeDirect,

View file

@ -21,7 +21,7 @@ type HTTP struct {
authenticator auth.Authenticator authenticator auth.Authenticator
} }
func NewHTTP(ctx context.Context, router adapter.Router, logger log.Logger, tag string, options *option.SimpleInboundOptions) *HTTP { func NewHTTP(ctx context.Context, router adapter.Router, logger log.Logger, tag string, options option.SimpleInboundOptions) *HTTP {
inbound := &HTTP{ inbound := &HTTP{
myInboundAdapter{ myInboundAdapter{
protocol: C.TypeHTTP, protocol: C.TypeHTTP,

View file

@ -27,7 +27,7 @@ type Mixed struct {
authenticator auth.Authenticator authenticator auth.Authenticator
} }
func NewMixed(ctx context.Context, router adapter.Router, logger log.Logger, tag string, options *option.SimpleInboundOptions) *Mixed { func NewMixed(ctx context.Context, router adapter.Router, logger log.Logger, tag string, options option.SimpleInboundOptions) *Mixed {
inbound := &Mixed{ inbound := &Mixed{
myInboundAdapter{ myInboundAdapter{
protocol: C.TypeMixed, protocol: C.TypeMixed,

View file

@ -25,7 +25,7 @@ type Shadowsocks struct {
service shadowsocks.Service service shadowsocks.Service
} }
func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Logger, tag string, options *option.ShadowsocksInboundOptions) (*Shadowsocks, error) { func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Logger, tag string, options option.ShadowsocksInboundOptions) (*Shadowsocks, error) {
inbound := &Shadowsocks{ inbound := &Shadowsocks{
myInboundAdapter: myInboundAdapter{ myInboundAdapter: myInboundAdapter{
protocol: C.TypeShadowsocks, protocol: C.TypeShadowsocks,

View file

@ -20,7 +20,7 @@ type Socks struct {
authenticator auth.Authenticator authenticator auth.Authenticator
} }
func NewSocks(ctx context.Context, router adapter.Router, logger log.Logger, tag string, options *option.SimpleInboundOptions) *Socks { func NewSocks(ctx context.Context, router adapter.Router, logger log.Logger, tag string, options option.SimpleInboundOptions) *Socks {
inbound := &Socks{ inbound := &Socks{
myInboundAdapter{ myInboundAdapter{
protocol: C.TypeSocks, protocol: C.TypeSocks,

View file

@ -5,6 +5,7 @@ import (
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
F "github.com/sagernet/sing/common/format" F "github.com/sagernet/sing/common/format"
) )
@ -18,9 +19,9 @@ func New(router adapter.Router, logger log.Logger, index int, options option.Out
outboundLogger := logger.WithPrefix(F.ToString("outbound/", options.Type, "[", tag, "]: ")) outboundLogger := logger.WithPrefix(F.ToString("outbound/", options.Type, "[", tag, "]: "))
switch options.Type { switch options.Type {
case C.TypeDirect: case C.TypeDirect:
return NewDirect(router, outboundLogger, options.Tag, options.DirectOptions), nil return NewDirect(router, outboundLogger, options.Tag, common.PtrValueOrDefault(options.DirectOptions)), nil
case C.TypeShadowsocks: case C.TypeShadowsocks:
return NewShadowsocks(router, outboundLogger, options.Tag, options.ShadowsocksOptions) return NewShadowsocks(router, outboundLogger, options.Tag, common.PtrValueOrDefault(options.ShadowsocksOptions))
default: default:
panic(F.ToString("unknown outbound type: ", options.Type)) panic(F.ToString("unknown outbound type: ", options.Type))
} }

View file

@ -21,7 +21,7 @@ type Direct struct {
overrideDestination M.Socksaddr overrideDestination M.Socksaddr
} }
func NewDirect(router adapter.Router, logger log.Logger, tag string, options *option.DirectOutboundOptions) *Direct { func NewDirect(router adapter.Router, logger log.Logger, tag string, options option.DirectOutboundOptions) *Direct {
outbound := &Direct{ outbound := &Direct{
myOutboundAdapter: myOutboundAdapter{ myOutboundAdapter: myOutboundAdapter{
protocol: C.TypeDirect, protocol: C.TypeDirect,

View file

@ -24,7 +24,7 @@ type Shadowsocks struct {
serverAddr M.Socksaddr serverAddr M.Socksaddr
} }
func NewShadowsocks(router adapter.Router, logger log.Logger, tag string, options *option.ShadowsocksOutboundOptions) (*Shadowsocks, error) { func NewShadowsocks(router adapter.Router, logger log.Logger, tag string, options option.ShadowsocksOutboundOptions) (*Shadowsocks, error) {
outbound := &Shadowsocks{ outbound := &Shadowsocks{
myOutboundAdapter: myOutboundAdapter{ myOutboundAdapter: myOutboundAdapter{
protocol: C.TypeDirect, protocol: C.TypeDirect,

View file

@ -0,0 +1,47 @@
package domain
import "unicode/utf8"
type Matcher struct {
set *succinctSet
}
func NewMatcher(domains []string, domainSuffix []string) *Matcher {
var domainList []string
for _, domain := range domains {
domainList = append(domainList, reverseDomain(domain))
}
for _, domain := range domainSuffix {
domainList = append(domainList, reverseDomainSuffix(domain))
}
return &Matcher{
newSuccinctSet(domainList),
}
}
func (m *Matcher) Match(domain string) bool {
return m.set.Has(reverseDomain(domain))
}
func reverseDomain(domain string) string {
l := len(domain)
b := make([]byte, l)
for i := 0; i < l; {
r, n := utf8.DecodeRuneInString(domain[i:])
i += n
utf8.EncodeRune(b[l-i:], r)
}
return string(b)
}
func reverseDomainSuffix(domain string) string {
l := len(domain)
b := make([]byte, l+1)
for i := 0; i < l; {
r, n := utf8.DecodeRuneInString(domain[i:])
i += n
utf8.EncodeRune(b[l-i:], r)
}
b[l] = prefixLabel
return string(b)
}

View file

@ -0,0 +1,18 @@
package domain
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestMatch(t *testing.T) {
r := require.New(t)
matcher := NewMatcher([]string{"domain.com"}, []string{"suffix.com", ".suffix.org"})
r.True(matcher.Match("domain.com"))
r.False(matcher.Match("my.domain.com"))
r.True(matcher.Match("suffix.com"))
r.True(matcher.Match("my.suffix.com"))
r.False(matcher.Match("suffix.org"))
r.True(matcher.Match("my.suffix.org"))
}

232
adapter/route/domain/set.go Normal file
View file

@ -0,0 +1,232 @@
package domain
import (
"math/bits"
)
const prefixLabel = '\r'
// mod from https://github.com/openacid/succinct
type succinctSet struct {
leaves, labelBitmap []uint64
labels []byte
ranks, selects []int32
}
func newSuccinctSet(keys []string) *succinctSet {
ss := &succinctSet{}
lIdx := 0
type qElt struct{ s, e, col int }
queue := []qElt{{0, len(keys), 0}}
for i := 0; i < len(queue); i++ {
elt := queue[i]
if elt.col == len(keys[elt.s]) {
// a leaf node
elt.s++
setBit(&ss.leaves, i, 1)
}
for j := elt.s; j < elt.e; {
frm := j
for ; j < elt.e && keys[j][elt.col] == keys[frm][elt.col]; j++ {
}
queue = append(queue, qElt{frm, j, elt.col + 1})
ss.labels = append(ss.labels, keys[frm][elt.col])
setBit(&ss.labelBitmap, lIdx, 0)
lIdx++
}
setBit(&ss.labelBitmap, lIdx, 1)
lIdx++
}
ss.init()
return ss
}
func (ss *succinctSet) Has(key string) bool {
var nodeId, bmIdx int
for i := 0; i < len(key); i++ {
currentChar := key[i]
for ; ; bmIdx++ {
if getBit(ss.labelBitmap, bmIdx) != 0 {
return false
}
nextLabel := ss.labels[bmIdx-nodeId]
if nextLabel == prefixLabel {
return true
}
if nextLabel == currentChar {
break
}
}
nodeId = countZeros(ss.labelBitmap, ss.ranks, bmIdx+1)
bmIdx = selectIthOne(ss.labelBitmap, ss.ranks, ss.selects, nodeId-1) + 1
}
if getBit(ss.leaves, nodeId) != 0 {
return true
}
for ; ; bmIdx++ {
if getBit(ss.labelBitmap, bmIdx) != 0 {
return false
}
if ss.labels[bmIdx-nodeId] == prefixLabel {
return true
}
}
}
func setBit(bm *[]uint64, i int, v int) {
for i>>6 >= len(*bm) {
*bm = append(*bm, 0)
}
(*bm)[i>>6] |= uint64(v) << uint(i&63)
}
func getBit(bm []uint64, i int) uint64 {
return bm[i>>6] & (1 << uint(i&63))
}
func (ss *succinctSet) init() {
ss.selects, ss.ranks = indexSelect32R64(ss.labelBitmap)
}
func countZeros(bm []uint64, ranks []int32, i int) int {
a, _ := rank64(bm, ranks, int32(i))
return i - int(a)
}
func selectIthOne(bm []uint64, ranks, selects []int32, i int) int {
a, _ := select32R64(bm, selects, ranks, int32(i))
return int(a)
}
func rank64(words []uint64, rindex []int32, i int32) (int32, int32) {
wordI := i >> 6
j := uint32(i & 63)
n := rindex[wordI]
w := words[wordI]
c1 := n + int32(bits.OnesCount64(w&mask[j]))
return c1, int32(w>>uint(j)) & 1
}
func indexRank64(words []uint64, opts ...bool) []int32 {
trailing := false
if len(opts) > 0 {
trailing = opts[0]
}
l := len(words)
if trailing {
l++
}
idx := make([]int32, l)
n := int32(0)
for i := 0; i < len(words); i++ {
idx[i] = n
n += int32(bits.OnesCount64(words[i]))
}
if trailing {
idx[len(words)] = n
}
return idx
}
func select32R64(words []uint64, selectIndex, rankIndex []int32, i int32) (int32, int32) {
a := int32(0)
l := int32(len(words))
wordI := selectIndex[i>>5] >> 6
for ; rankIndex[wordI+1] <= i; wordI++ {
}
w := words[wordI]
ww := w
base := wordI << 6
findIth := int(i - rankIndex[wordI])
offset := int32(0)
ones := bits.OnesCount32(uint32(ww))
if ones <= findIth {
findIth -= ones
offset |= 32
ww >>= 32
}
ones = bits.OnesCount16(uint16(ww))
if ones <= findIth {
findIth -= ones
offset |= 16
ww >>= 16
}
ones = bits.OnesCount8(uint8(ww))
if ones <= findIth {
a = int32(select8Lookup[(ww>>5)&(0x7f8)|uint64(findIth-ones)]) + offset + 8
} else {
a = int32(select8Lookup[(ww&0xff)<<3|uint64(findIth)]) + offset
}
a += base
w &= rMaskUpto[a&63]
if w != 0 {
return a, base + int32(bits.TrailingZeros64(w))
}
wordI++
for ; wordI < l; wordI++ {
w = words[wordI]
if w != 0 {
return a, wordI<<6 + int32(bits.TrailingZeros64(w))
}
}
return a, l << 6
}
func indexSelect32R64(words []uint64) ([]int32, []int32) {
l := len(words) << 6
sidx := make([]int32, 0, len(words))
ith := -1
for i := 0; i < l; i++ {
if words[i>>6]&(1<<uint(i&63)) != 0 {
ith++
if ith&31 == 0 {
sidx = append(sidx, int32(i))
}
}
}
// clone to reduce cap to len
sidx = append(sidx[:0:0], sidx...)
return sidx, indexRank64(words, true)
}
func init() {
initMasks()
initSelectLookup()
}
var (
mask [65]uint64
rMaskUpto [64]uint64
)
func initMasks() {
for i := 0; i < 65; i++ {
mask[i] = (1 << uint(i)) - 1
}
var maskUpto [64]uint64
for i := 0; i < 64; i++ {
maskUpto[i] = (1 << uint(i+1)) - 1
rMaskUpto[i] = ^maskUpto[i]
}
}
var select8Lookup [256 * 8]uint8
func initSelectLookup() {
for i := 0; i < 256; i++ {
w := uint8(i)
for j := 0; j < 8; j++ {
// x-th 1 in w
// if x-th 1 is not found, it is 8
x := bits.TrailingZeros8(w)
w &= w - 1
select8Lookup[i*8+j] = uint8(x)
}
}
}

View file

@ -2,7 +2,12 @@ package route
import ( import (
"context" "context"
"io"
"net" "net"
"net/http"
"os"
"path/filepath"
"time"
"github.com/oschwald/geoip2-golang" "github.com/oschwald/geoip2-golang"
"github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter"
@ -10,71 +15,63 @@ import (
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
) )
var _ adapter.Router = (*Router)(nil) var _ adapter.Router = (*Router)(nil)
type Router struct { type Router struct {
ctx context.Context
logger log.Logger logger log.Logger
defaultOutbound adapter.Outbound defaultOutbound adapter.Outbound
outboundByTag map[string]adapter.Outbound outboundByTag map[string]adapter.Outbound
rules []adapter.Rule
rules []adapter.Rule needGeoDatabase bool
geoReader *geoip2.Reader geoOptions option.GeoIPOptions
geoReader *geoip2.Reader
} }
func NewRouter(logger log.Logger) *Router { func NewRouter(ctx context.Context, logger log.Logger, options option.RouteOptions) (*Router, error) {
return &Router{ router := &Router{
logger: logger.WithPrefix("router: "), ctx: ctx,
outboundByTag: make(map[string]adapter.Outbound), logger: logger.WithPrefix("router: "),
outboundByTag: make(map[string]adapter.Outbound),
rules: make([]adapter.Rule, 0, len(options.Rules)),
needGeoDatabase: hasGeoRule(options.Rules),
geoOptions: common.PtrValueOrDefault(options.GeoIP),
} }
} for i, ruleOptions := range options.Rules {
rule, err := NewRule(router, logger, ruleOptions)
func (r *Router) DefaultOutbound() adapter.Outbound { if err != nil {
if r.defaultOutbound == nil { return nil, E.Cause(err, "parse rule[", i, "]")
panic("missing default outbound") }
router.rules = append(router.rules, rule)
} }
return r.defaultOutbound return router, nil
} }
func (r *Router) Outbound(tag string) (adapter.Outbound, bool) { func hasGeoRule(rules []option.Rule) bool {
outbound, loaded := r.outboundByTag[tag] for _, rule := range rules {
return outbound, loaded if rule.DefaultOptions != nil {
} if isGeoRule(common.PtrValueOrDefault(rule.DefaultOptions)) {
return true
func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { }
for _, rule := range r.rules { } else if rule.LogicalOptions != nil {
if rule.Match(metadata) { for _, subRule := range rule.LogicalOptions.Rules {
r.logger.WithContext(ctx).Info("match ", rule.String()) if isGeoRule(subRule) {
if outbound, loaded := r.Outbound(rule.Outbound()); loaded { return true
return outbound.NewConnection(ctx, conn, metadata.Destination) }
} }
r.logger.WithContext(ctx).Error("outbound ", rule.Outbound(), " not found")
} }
} }
r.logger.WithContext(ctx).Info("no match => ", r.defaultOutbound.Tag()) return false
return r.defaultOutbound.NewConnection(ctx, conn, metadata.Destination)
} }
func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { func isGeoRule(rule option.DefaultRule) bool {
for _, rule := range r.rules { return len(rule.SourceGeoIP) > 0 || len(rule.GeoIP) > 0
if rule.Match(metadata) {
r.logger.WithContext(ctx).Info("match ", rule.String())
if outbound, loaded := r.Outbound(rule.Outbound()); loaded {
return outbound.NewPacketConnection(ctx, conn, metadata.Destination)
}
r.logger.WithContext(ctx).Error("outbound ", rule.Outbound(), " not found")
}
}
r.logger.WithContext(ctx).Info("no match => ", r.defaultOutbound.Tag())
return r.defaultOutbound.NewPacketConnection(ctx, conn, metadata.Destination)
}
func (r *Router) Close() error {
return common.Close(
common.PtrOrNil(r.geoReader),
)
} }
func (r *Router) UpdateOutbounds(outbounds []adapter.Outbound) { func (r *Router) UpdateOutbounds(outbounds []adapter.Outbound) {
@ -90,14 +87,136 @@ func (r *Router) UpdateOutbounds(outbounds []adapter.Outbound) {
r.outboundByTag = outboundByTag r.outboundByTag = outboundByTag
} }
func (r *Router) UpdateRules(options []option.Rule) error { func (r *Router) Start() error {
rules := make([]adapter.Rule, 0, len(options)) if r.needGeoDatabase {
for i, rule := range options { go r.prepareGeoIPDatabase()
switch rule.Type {
case "", C.RuleTypeDefault:
rules = append(rules, NewDefaultRule(i, rule.DefaultOptions))
}
} }
r.rules = rules
return nil return nil
} }
func (r *Router) Close() error {
return common.Close(
common.PtrOrNil(r.geoReader),
)
}
func (r *Router) GeoIPReader() *geoip2.Reader {
return r.geoReader
}
func (r *Router) prepareGeoIPDatabase() {
var geoPath string
if r.geoOptions.Path != "" {
geoPath = r.geoOptions.Path
} else {
geoPath = "Country.mmdb"
}
geoPath, loaded := C.Find(geoPath)
if !loaded {
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
}
}
geoReader, err := geoip2.Open(geoPath)
if err == nil {
r.logger.Info("loaded geoip database")
r.geoReader = geoReader
} else {
r.logger.Error("open geoip database: ", err)
return
}
}
func (r *Router) downloadGeoIPDatabase(savePath string) error {
var downloadURL string
if r.geoOptions.DownloadURL != "" {
downloadURL = r.geoOptions.DownloadURL
} else {
downloadURL = "https://cdn.jsdelivr.net/gh/Dreamacro/maxmind-geoip@release/Country.mmdb"
}
r.logger.Info("downloading geoip database")
var detour adapter.Outbound
if r.geoOptions.DownloadDetour != "" {
outbound, loaded := r.Outbound(r.geoOptions.DownloadDetour)
if !loaded {
return E.New("detour outbound not found: ", r.geoOptions.DownloadDetour)
}
detour = outbound
} else {
detour = r.defaultOutbound
}
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{
Timeout: 5 * time.Second,
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))
},
},
}
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) DefaultOutbound() adapter.Outbound {
if r.defaultOutbound == nil {
panic("missing default outbound")
}
return r.defaultOutbound
}
func (r *Router) Outbound(tag string) (adapter.Outbound, bool) {
outbound, loaded := r.outboundByTag[tag]
return outbound, loaded
}
func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
return r.match(ctx, metadata).NewConnection(ctx, conn, metadata.Destination)
}
func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
return r.match(ctx, metadata).NewPacketConnection(ctx, conn, metadata.Destination)
}
func (r *Router) match(ctx context.Context, metadata adapter.InboundContext) adapter.Outbound {
for i, rule := range r.rules {
if rule.Match(&metadata) {
detour := rule.Outbound()
r.logger.WithContext(ctx).Info("match [", i, "]", rule.String(), " => ", detour)
if outbound, loaded := r.Outbound(detour); loaded {
return outbound
}
r.logger.WithContext(ctx).Error("outbound not found: ", detour)
}
}
r.logger.WithContext(ctx).Info("no match")
return r.defaultOutbound
}

View file

@ -1,11 +1,28 @@
package route package route
import ( import (
"strings"
"github.com/sagernet/sing-box/adapter" "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-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format" F "github.com/sagernet/sing/common/format"
) )
func NewRule(router adapter.Router, logger log.Logger, options option.Rule) (adapter.Rule, error) {
switch options.Type {
case "", C.RuleTypeDefault:
return NewDefaultRule(router, logger, common.PtrValueOrDefault(options.DefaultOptions))
case C.RuleTypeLogical:
return NewLogicalRule(router, logger, common.PtrValueOrDefault(options.LogicalOptions))
default:
return nil, E.New("unknown rule type: ", options.Type)
}
}
var _ adapter.Rule = (*DefaultRule)(nil) var _ adapter.Rule = (*DefaultRule)(nil)
type DefaultRule struct { type DefaultRule struct {
@ -15,22 +32,72 @@ type DefaultRule struct {
} }
type RuleItem interface { type RuleItem interface {
Match(metadata adapter.InboundContext) bool Match(metadata *adapter.InboundContext) bool
String() string String() string
} }
func NewDefaultRule(index int, options option.DefaultRule) *DefaultRule { func NewDefaultRule(router adapter.Router, logger log.Logger, options option.DefaultRule) (*DefaultRule, error) {
rule := &DefaultRule{ rule := &DefaultRule{
index: index,
outbound: options.Outbound, outbound: options.Outbound,
} }
if len(options.Inbound) > 0 { if len(options.Inbound) > 0 {
rule.items = append(rule.items, NewInboundRule(options.Inbound)) rule.items = append(rule.items, NewInboundRule(options.Inbound))
} }
return rule if options.IPVersion > 0 {
switch options.IPVersion {
case 4, 6:
rule.items = append(rule.items, NewIPVersionItem(options.IPVersion == 6))
default:
return nil, E.New("invalid ip version: ", options.IPVersion)
}
}
if options.Network != "" {
switch options.Network {
case C.NetworkTCP, C.NetworkUDP:
rule.items = append(rule.items, NewNetworkItem(options.Network))
default:
return nil, E.New("invalid network: ", options.Network)
}
}
if len(options.Protocol) > 0 {
rule.items = append(rule.items, NewProtocolItem(options.Protocol))
}
if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 {
rule.items = append(rule.items, NewDomainItem(options.Domain, options.DomainSuffix))
}
if len(options.DomainKeyword) > 0 {
rule.items = append(rule.items, NewDomainKeywordItem(options.DomainKeyword))
}
if len(options.SourceGeoIP) > 0 {
rule.items = append(rule.items, NewGeoIPItem(router, logger, true, options.SourceGeoIP))
}
if len(options.GeoIP) > 0 {
rule.items = append(rule.items, NewGeoIPItem(router, logger, false, options.GeoIP))
}
if len(options.SourceIPCIDR) > 0 {
item, err := NewIPCIDRItem(true, options.SourceIPCIDR)
if err != nil {
return nil, err
}
rule.items = append(rule.items, item)
}
if len(options.IPCIDR) > 0 {
item, err := NewIPCIDRItem(false, options.IPCIDR)
if err != nil {
return nil, err
}
rule.items = append(rule.items, item)
}
if len(options.SourcePort) > 0 {
rule.items = append(rule.items, NewPortItem(true, options.SourcePort))
}
if len(options.Port) > 0 {
rule.items = append(rule.items, NewPortItem(false, options.Port))
}
return rule, nil
} }
func (r *DefaultRule) Match(metadata adapter.InboundContext) bool { func (r *DefaultRule) Match(metadata *adapter.InboundContext) bool {
for _, item := range r.items { for _, item := range r.items {
if item.Match(metadata) { if item.Match(metadata) {
return true return true
@ -44,12 +111,5 @@ func (r *DefaultRule) Outbound() string {
} }
func (r *DefaultRule) String() string { func (r *DefaultRule) String() string {
var description string return strings.Join(common.Map(r.items, F.ToString0[RuleItem]), " ")
description = F.ToString("[", r.index, "]")
for _, item := range r.items {
description += " "
description += item.String()
}
description += " => " + r.outbound
return description
} }

View file

@ -0,0 +1,68 @@
package route
import (
"net/netip"
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common"
F "github.com/sagernet/sing/common/format"
)
var _ RuleItem = (*IPCIDRItem)(nil)
type IPCIDRItem struct {
prefixes []netip.Prefix
isSource bool
}
func NewIPCIDRItem(isSource bool, prefixStrings []string) (*IPCIDRItem, error) {
prefixes := make([]netip.Prefix, 0, len(prefixStrings))
for _, prefixString := range prefixStrings {
prefix, err := netip.ParsePrefix(prefixString)
if err != nil {
return nil, err
}
prefixes = append(prefixes, prefix)
}
return &IPCIDRItem{
prefixes: prefixes,
isSource: isSource,
}, nil
}
func (r *IPCIDRItem) Match(metadata *adapter.InboundContext) bool {
if r.isSource {
for _, prefix := range r.prefixes {
if prefix.Contains(metadata.Source.Addr) {
return true
}
}
} else {
if metadata.Destination.IsFqdn() {
return false
}
for _, prefix := range r.prefixes {
if prefix.Contains(metadata.Destination.Addr) {
return true
}
}
}
return false
}
func (r *IPCIDRItem) String() string {
var description string
if r.isSource {
description = "source_ipcidr="
} else {
description = "ipcidr="
}
pLen := len(r.prefixes)
if pLen == 1 {
description += r.prefixes[0].String()
} else {
description += "[" + strings.Join(common.Map(r.prefixes, F.ToString0[netip.Prefix]), " ") + "]"
}
return description
}

View file

@ -0,0 +1,64 @@
package route
import (
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/route/domain"
"github.com/sagernet/sing/common"
)
var _ RuleItem = (*DomainItem)(nil)
type DomainItem struct {
description string
matcher *domain.Matcher
}
func NewDomainItem(domains []string, domainSuffixes []string) *DomainItem {
domains = common.Uniq(domains)
domainSuffixes = common.Uniq(domainSuffixes)
var description string
if dLen := len(domains); dLen > 0 {
if dLen == 1 {
description = "domain=" + domains[0]
} else if dLen > 3 {
description = "domain=[" + strings.Join(domains[:3], " ") + "...]"
} else {
description = "domain=[" + strings.Join(domains, " ") + "]"
}
}
if dsLen := len(domainSuffixes); dsLen > 0 {
if len(description) > 0 {
description += " "
}
if dsLen == 1 {
description += "domainSuffix=" + domainSuffixes[0]
} else if dsLen > 3 {
description += "domainSuffix=[" + strings.Join(domainSuffixes[:3], " ") + "...]"
} else {
description += "domainSuffix=[" + strings.Join(domainSuffixes, " ") + "]"
}
}
return &DomainItem{
description,
domain.NewMatcher(domains, domainSuffixes),
}
}
func (r *DomainItem) Match(metadata *adapter.InboundContext) bool {
var domainHost string
if metadata.Domain != "" {
domainHost = metadata.Domain
} else {
domainHost = metadata.Destination.Fqdn
}
if domainHost == "" {
return false
}
return r.matcher.Match(domainHost)
}
func (r *DomainItem) String() string {
return r.description
}

View file

@ -0,0 +1,46 @@
package route
import (
"strings"
"github.com/sagernet/sing-box/adapter"
)
var _ RuleItem = (*DomainKeywordItem)(nil)
type DomainKeywordItem struct {
keywords []string
}
func NewDomainKeywordItem(keywords []string) *DomainKeywordItem {
return &DomainKeywordItem{keywords}
}
func (r *DomainKeywordItem) Match(metadata *adapter.InboundContext) bool {
var domainHost string
if metadata.Domain != "" {
domainHost = metadata.Domain
} else {
domainHost = metadata.Destination.Fqdn
}
if domainHost == "" {
return false
}
for _, keyword := range r.keywords {
if strings.Contains(domainHost, keyword) {
return true
}
}
return false
}
func (r *DomainKeywordItem) String() string {
kLen := len(r.keywords)
if kLen == 1 {
return "domain_keyword=" + r.keywords[0]
} else if kLen > 3 {
return "domain_keyword=[" + strings.Join(r.keywords[:3], " ") + "...]"
} else {
return "domain_keyword=[" + strings.Join(r.keywords, " ") + "]"
}
}

View file

@ -0,0 +1,81 @@
package route
import (
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
)
var _ RuleItem = (*GeoIPItem)(nil)
type GeoIPItem struct {
router adapter.Router
logger log.Logger
isSource bool
codes []string
codeMap map[string]bool
}
func NewGeoIPItem(router adapter.Router, logger log.Logger, isSource bool, codes []string) *GeoIPItem {
codeMap := make(map[string]bool)
for _, code := range codes {
codeMap[code] = true
}
return &GeoIPItem{
router: router,
logger: logger,
codes: codes,
isSource: isSource,
codeMap: codeMap,
}
}
func (r *GeoIPItem) Match(metadata *adapter.InboundContext) bool {
geoReader := r.router.GeoIPReader()
if geoReader == nil {
return false
}
if r.isSource {
if metadata.SourceGeoIPCode == "" {
country, err := geoReader.Country(metadata.Source.Addr.AsSlice())
if err != nil {
r.logger.Error("query geoip for ", metadata.Source.Addr, ": ", err)
return false
}
metadata.SourceGeoIPCode = country.Country.IsoCode
}
return r.codeMap[metadata.SourceGeoIPCode]
} else {
if metadata.Destination.IsFqdn() {
return false
}
if metadata.GeoIPCode == "" {
country, err := geoReader.Country(metadata.Destination.Addr.AsSlice())
if err != nil {
r.logger.Error("query geoip for ", metadata.Destination.Addr, ": ", err)
return false
}
metadata.GeoIPCode = country.Country.IsoCode
}
return r.codeMap[metadata.GeoIPCode]
}
}
func (r *GeoIPItem) String() string {
var description string
if r.isSource {
description = "source_geoip="
} else {
description = "geoip="
}
cLen := len(r.codes)
if cLen == 1 {
description += r.codes[0]
} else if cLen > 3 {
description += "[" + strings.Join(r.codes[:3], " ") + "...]"
} else {
description += "[" + strings.Join(r.codes, " ") + "]"
}
return description
}

View file

@ -7,26 +7,26 @@ import (
F "github.com/sagernet/sing/common/format" F "github.com/sagernet/sing/common/format"
) )
var _ RuleItem = (*InboundRule)(nil) var _ RuleItem = (*InboundItem)(nil)
type InboundRule struct { type InboundItem struct {
inbounds []string inbounds []string
inboundMap map[string]bool inboundMap map[string]bool
} }
func NewInboundRule(inbounds []string) RuleItem { func NewInboundRule(inbounds []string) *InboundItem {
rule := &InboundRule{inbounds, make(map[string]bool)} rule := &InboundItem{inbounds, make(map[string]bool)}
for _, inbound := range inbounds { for _, inbound := range inbounds {
rule.inboundMap[inbound] = true rule.inboundMap[inbound] = true
} }
return rule return rule
} }
func (r *InboundRule) Match(metadata adapter.InboundContext) bool { func (r *InboundItem) Match(metadata *adapter.InboundContext) bool {
return r.inboundMap[metadata.Inbound] return r.inboundMap[metadata.Inbound]
} }
func (r *InboundRule) String() string { func (r *InboundItem) String() string {
if len(r.inbounds) == 1 { if len(r.inbounds) == 1 {
return F.ToString("inbound=", r.inbounds[0]) return F.ToString("inbound=", r.inbounds[0])
} else { } else {

View file

@ -0,0 +1,29 @@
package route
import (
"github.com/sagernet/sing-box/adapter"
)
var _ RuleItem = (*IPVersionItem)(nil)
type IPVersionItem struct {
isIPv6 bool
}
func NewIPVersionItem(isIPv6 bool) *IPVersionItem {
return &IPVersionItem{isIPv6}
}
func (r *IPVersionItem) Match(metadata *adapter.InboundContext) bool {
return metadata.Destination.IsIP() && metadata.Destination.Family().IsIPv6() == r.isIPv6
}
func (r *IPVersionItem) String() string {
var versionStr string
if r.isIPv6 {
versionStr = "6"
} else {
versionStr = "4"
}
return "ip_version=" + versionStr
}

View file

@ -0,0 +1,71 @@
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"
)
var _ adapter.Rule = (*LogicalRule)(nil)
type LogicalRule struct {
mode string
rules []*DefaultRule
outbound string
}
func NewLogicalRule(router adapter.Router, logger log.Logger, options option.LogicalRule) (*LogicalRule, error) {
r := &LogicalRule{
rules: make([]*DefaultRule, len(options.Rules)),
outbound: options.Outbound,
}
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 := NewDefaultRule(router, logger, subRule)
if err != nil {
return nil, E.Cause(err, "sub rule[", i, "]")
}
r.rules[i] = rule
}
return r, 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)
})
} else {
return common.Any(r.rules, func(it *DefaultRule) bool {
return it.Match(metadata)
})
}
}
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 = "||"
}
return "logical(" + strings.Join(common.Map(r.rules, F.ToString0[*DefaultRule]), " "+op+" ") + ")"
}

View file

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

View file

@ -0,0 +1,53 @@
package route
import (
"strings"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common"
F "github.com/sagernet/sing/common/format"
)
var _ RuleItem = (*PortItem)(nil)
type PortItem struct {
ports []uint16
portMap map[uint16]bool
isSource bool
}
func NewPortItem(isSource bool, ports []uint16) *PortItem {
portMap := make(map[uint16]bool)
for _, port := range ports {
portMap[port] = true
}
return &PortItem{
ports: ports,
portMap: portMap,
isSource: isSource,
}
}
func (r *PortItem) Match(metadata *adapter.InboundContext) bool {
if r.isSource {
return r.portMap[metadata.Source.Port]
} else {
return r.portMap[metadata.Destination.Port]
}
}
func (r *PortItem) String() string {
var description string
if r.isSource {
description = "source_port="
} else {
description = "port="
}
pLen := len(r.ports)
if pLen == 1 {
description += F.ToString(r.ports[0])
} else {
description += "[" + strings.Join(common.Map(r.ports, F.ToString0[uint16]), " ") + "]"
}
return description
}

View file

@ -0,0 +1,37 @@
package route
import (
"strings"
"github.com/sagernet/sing-box/adapter"
F "github.com/sagernet/sing/common/format"
)
var _ RuleItem = (*ProtocolItem)(nil)
type ProtocolItem struct {
protocols []string
protocolMap map[string]bool
}
func NewProtocolItem(protocols []string) *ProtocolItem {
protocolMap := make(map[string]bool)
for _, protocol := range protocols {
protocolMap[protocol] = true
}
return &ProtocolItem{
protocols: protocols,
protocolMap: protocolMap,
}
}
func (r *ProtocolItem) Match(metadata *adapter.InboundContext) bool {
return r.protocolMap[metadata.Protocol]
}
func (r *ProtocolItem) String() string {
if len(r.protocols) == 1 {
return F.ToString("protocol=", r.protocols[0])
}
return F.ToString("protocol=[", strings.Join(r.protocols, " "), "]")
}

View file

@ -4,19 +4,23 @@ import (
"context" "context"
"net" "net"
"github.com/oschwald/geoip2-golang"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
) )
type Router interface { type Router interface {
Start() error
Close() error
DefaultOutbound() Outbound DefaultOutbound() Outbound
Outbound(tag string) (Outbound, bool) Outbound(tag string) (Outbound, bool)
RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
Close() error GeoIPReader() *geoip2.Reader
} }
type Rule interface { type Rule interface {
Match(metadata InboundContext) bool Match(metadata *InboundContext) bool
Outbound() string Outbound() string
String() string String() string
} }

View file

@ -43,7 +43,7 @@ func run(cmd *cobra.Command, args []string) {
} }
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
service, err := box.NewService(ctx, &options) service, err := box.NewService(ctx, options)
if err != nil { if err != nil {
logrus.Fatal("create service: ", err) logrus.Fatal("create service: ", err)
} }

35
constant/path.go Normal file
View file

@ -0,0 +1,35 @@
package constant
import (
"os"
"path/filepath"
"github.com/sagernet/sing/common/rw"
)
const dirName = "sing-box"
var resourcePaths []string
func Find(name string) (string, bool) {
name = os.ExpandEnv(name)
if rw.FileExists(name) {
return name, true
}
for _, dir := range resourcePaths {
if path := filepath.Join(dir, dirName, name); rw.FileExists(path) {
return path, true
}
}
return name, false
}
func init() {
resourcePaths = append(resourcePaths, ".")
if userConfigDir, err := os.UserConfigDir(); err == nil {
resourcePaths = append(resourcePaths, userConfigDir)
}
if userCacheDir, err := os.UserCacheDir(); err == nil {
resourcePaths = append(resourcePaths, userCacheDir)
}
}

17
constant/path_unix.go Normal file
View file

@ -0,0 +1,17 @@
//go:build unix
package constant
import (
"os"
)
func init() {
resourcePaths = append(resourcePaths, "/etc/config")
resourcePaths = append(resourcePaths, "/usr/share")
resourcePaths = append(resourcePaths, "/usr/local/etc/config")
resourcePaths = append(resourcePaths, "/usr/local/share")
if homeDir := os.Getenv("HOME"); homeDir != "" {
resourcePaths = append(resourcePaths, homeDir+"/.local/share")
}
}

7
constant/require.go Normal file
View file

@ -0,0 +1,7 @@
//go:build !go1.19
package constant
func init() {
panic("sing-box requires Go 1.19 or later")
}

View file

@ -4,3 +4,8 @@ const (
RuleTypeDefault = "default" RuleTypeDefault = "default"
RuleTypeLogical = "logical" RuleTypeLogical = "logical"
) )
const (
LogicalTypeAnd = "and"
LogicalTypeOr = "or"
)

6
go.mod
View file

@ -6,18 +6,22 @@ require (
github.com/database64128/tfo-go v1.0.4 github.com/database64128/tfo-go v1.0.4
github.com/logrusorgru/aurora v2.0.3+incompatible github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/oschwald/geoip2-golang v1.7.0 github.com/oschwald/geoip2-golang v1.7.0
github.com/sagernet/sing v0.0.0-20220701084654-2a0502dd664e github.com/sagernet/sing v0.0.0-20220702141141-b3923d54845b
github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649 github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.5.0 github.com/spf13/cobra v1.5.0
github.com/stretchr/testify v1.7.1
) )
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/klauspost/cpuid/v2 v2.0.12 // indirect github.com/klauspost/cpuid/v2 v2.0.12 // indirect
github.com/oschwald/maxminddb-golang v1.9.0 // indirect github.com/oschwald/maxminddb-golang v1.9.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b // indirect golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
lukechampine.com/blake3 v1.1.7 // indirect lukechampine.com/blake3 v1.1.7 // indirect
) )

9
go.sum
View file

@ -1,6 +1,7 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/database64128/tfo-go v1.0.4 h1:0D9CsLor6q+2UrLhFYY3MkKkxRGf2W+27beMAo43SJc= github.com/database64128/tfo-go v1.0.4 h1:0D9CsLor6q+2UrLhFYY3MkKkxRGf2W+27beMAo43SJc=
github.com/database64128/tfo-go v1.0.4/go.mod h1:q5W+W0+2IHrw/Lnl0yg4sz7Kz5IDsm9x0vhwZXkRwG4= github.com/database64128/tfo-go v1.0.4/go.mod h1:q5W+W0+2IHrw/Lnl0yg4sz7Kz5IDsm9x0vhwZXkRwG4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
@ -17,8 +18,8 @@ github.com/oschwald/maxminddb-golang v1.9.0/go.mod h1:TK+s/Z2oZq0rSl4PSeAEoP0bgm
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagernet/sing v0.0.0-20220701084654-2a0502dd664e h1:GHT5FW/T8ckRe2BuHoCpzx9zrMPtUO7hvfjqs1Tak0I= github.com/sagernet/sing v0.0.0-20220702141141-b3923d54845b h1:oK5RglZ0s4oXNSrIsLJkBiHbYoAUMOGLN3a0JgDNzVM=
github.com/sagernet/sing v0.0.0-20220701084654-2a0502dd664e/go.mod h1:3ZmoGNg/nNJTyHAZFNRSPaXpNIwpDvyIiAUd0KIWV5c= github.com/sagernet/sing v0.0.0-20220702141141-b3923d54845b/go.mod h1:3ZmoGNg/nNJTyHAZFNRSPaXpNIwpDvyIiAUd0KIWV5c=
github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649 h1:whNDUGOAX5GPZkSy4G3Gv9QyIgk5SXRyjkRuP7ohF8k= github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649 h1:whNDUGOAX5GPZkSy4G3Gv9QyIgk5SXRyjkRuP7ohF8k=
github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649/go.mod h1:MuyT+9fEPjvauAv0fSE0a6Q+l0Tv2ZrAafTkYfnxBFw= github.com/sagernet/sing-shadowsocks v0.0.0-20220701084835-2208da1d8649/go.mod h1:MuyT+9fEPjvauAv0fSE0a6Q+l0Tv2ZrAafTkYfnxBFw=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
@ -27,15 +28,19 @@ github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b h1:2n253B2r0pYSmEV+UNCQoPfU/FiaizQEK5Gu4Bq4JE8= golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b h1:2n253B2r0pYSmEV+UNCQoPfU/FiaizQEK5Gu4Bq4JE8=
golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220627191245-f75cf1eec38b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0= lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA=

View file

@ -1,10 +1,10 @@
package option package option
type Options struct { type Options struct {
Log *LogOption `json:"log"` Log *LogOption `json:"log"`
Inbounds []Inbound `json:"inbounds,omitempty"` Inbounds []Inbound `json:"inbounds,omitempty"`
Outbounds []Outbound `json:"outbounds,omitempty"` Outbounds []Outbound `json:"outbounds,omitempty"`
Routes []Rule `json:"routes,omitempty"` Route *RouteOptions `json:"route,omitempty"`
} }
type LogOption struct { type LogOption struct {

27
option/listable.go Normal file
View file

@ -0,0 +1,27 @@
package option
import "encoding/json"
type Listable[T any] []T
func (l *Listable[T]) MarshalJSON() ([]byte, error) {
arrayList := []T(*l)
if len(arrayList) == 1 {
return json.Marshal(arrayList[0])
}
return json.Marshal(arrayList)
}
func (l *Listable[T]) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*[]T)(l))
if err == nil {
return nil
}
var singleItem T
err = json.Unmarshal(bytes, &singleItem)
if err != nil {
return err
}
*l = []T{singleItem}
return nil
}

View file

@ -2,7 +2,6 @@ package option
import ( import (
"encoding/json" "encoding/json"
"strings"
C "github.com/sagernet/sing-box/constant" C "github.com/sagernet/sing-box/constant"
E "github.com/sagernet/sing/common/exceptions" E "github.com/sagernet/sing/common/exceptions"
@ -11,19 +10,24 @@ import (
type NetworkList []string type NetworkList []string
func (v *NetworkList) UnmarshalJSON(data []byte) error { func (v *NetworkList) UnmarshalJSON(data []byte) error {
var networkList string var networkList []string
err := json.Unmarshal(data, &networkList) err := json.Unmarshal(data, &networkList)
if err != nil { if err != nil {
return err var networkItem string
err = json.Unmarshal(data, &networkItem)
if err != nil {
return err
}
networkList = []string{networkItem}
} }
for _, networkName := range strings.Split(networkList, ",") { for _, networkName := range networkList {
switch networkName { switch networkName {
case "tcp", "udp": case "tcp", "udp":
*v = append(*v, networkName)
default: default:
return E.New("unknown network: " + networkName) return E.New("unknown network: " + networkName)
} }
} }
*v = networkList
return nil return nil
} }

View file

@ -9,10 +9,21 @@ import (
var ErrUnknownRuleType = E.New("unknown rule type") var ErrUnknownRuleType = E.New("unknown rule type")
type RouteOptions struct {
GeoIP *GeoIPOptions `json:"geoip,omitempty"`
Rules []Rule `json:"rules,omitempty"`
}
type GeoIPOptions struct {
Path string `json:"path,omitempty"`
DownloadURL string `json:"download_url,omitempty"`
DownloadDetour string `json:"download_detour,omitempty"`
}
type _Rule struct { type _Rule struct {
Type string `json:"type"` Type string `json:"type,omitempty"`
DefaultOptions DefaultRule `json:"default_options,omitempty"` DefaultOptions *DefaultRule `json:"default_options,omitempty"`
LogicalOptions LogicalRule `json:"logical_options,omitempty"` LogicalOptions *LogicalRule `json:"logical_options,omitempty"`
} }
type Rule _Rule type Rule _Rule
@ -45,9 +56,15 @@ func (r *Rule) UnmarshalJSON(bytes []byte) error {
} }
switch r.Type { switch r.Type {
case "", C.RuleTypeDefault: case "", C.RuleTypeDefault:
err = json.Unmarshal(bytes, &r.DefaultOptions) if r.DefaultOptions == nil {
break
}
err = json.Unmarshal(bytes, r.DefaultOptions)
case C.RuleTypeLogical: case C.RuleTypeLogical:
err = json.Unmarshal(bytes, &r.LogicalOptions) if r.LogicalOptions == nil {
break
}
err = json.Unmarshal(bytes, r.LogicalOptions)
default: default:
err = E.Extend(ErrUnknownRuleType, r.Type) err = E.Extend(ErrUnknownRuleType, r.Type)
} }
@ -55,22 +72,22 @@ func (r *Rule) UnmarshalJSON(bytes []byte) error {
} }
type DefaultRule struct { type DefaultRule struct {
Inbound []string `json:"inbound,omitempty"` Inbound Listable[string] `json:"inbound,omitempty"`
IPVersion []int `json:"ip_version,omitempty"` IPVersion int `json:"ip_version,omitempty"`
Network []string `json:"network,omitempty"` Network string `json:"network,omitempty"`
Protocol []string `json:"protocol,omitempty"` Protocol Listable[string] `json:"protocol,omitempty"`
Domain []string `json:"domain,omitempty"` Domain Listable[string] `json:"domain,omitempty"`
DomainSuffix []string `json:"domain_suffix,omitempty"` DomainSuffix Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword []string `json:"domain_keyword,omitempty"` DomainKeyword Listable[string] `json:"domain_keyword,omitempty"`
SourceGeoIP []string `json:"source_geoip,omitempty"` SourceGeoIP Listable[string] `json:"source_geoip,omitempty"`
GeoIP []string `json:"geoip,omitempty"` GeoIP Listable[string] `json:"geoip,omitempty"`
SourceIPCIDR []string `json:"source_ipcidr,omitempty"` SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"`
SourcePort []string `json:"source_port,omitempty"` IPCIDR Listable[string] `json:"ip_cidr,omitempty"`
IPCIDR []string `json:"destination_ipcidr,omitempty"` SourcePort Listable[uint16] `json:"source_port,omitempty"`
Port []string `json:"destination_port,omitempty"` Port Listable[uint16] `json:"port,omitempty"`
ProcessName []string `json:"process_name,omitempty"` // ProcessName Listable[string] `json:"process_name,omitempty"`
ProcessPath []string `json:"process_path,omitempty"` // ProcessPath Listable[string] `json:"process_path,omitempty"`
Outbound string `json:"outbound,omitempty"` Outbound string `json:"outbound,omitempty"`
} }
type LogicalRule struct { type LogicalRule struct {

View file

@ -21,27 +21,24 @@ type Service struct {
outbounds []adapter.Outbound outbounds []adapter.Outbound
} }
func NewService(ctx context.Context, options *option.Options) (*Service, error) { func NewService(ctx context.Context, options option.Options) (*Service, error) {
var logOptions option.LogOption logger, err := log.NewLogger(common.PtrValueOrDefault(options.Log))
if options.Log != nil {
logOptions = *options.Log
}
logger, err := log.NewLogger(logOptions)
if err != nil { if err != nil {
return nil, err return nil, err
} }
router := route.NewRouter(logger) router, err := route.NewRouter(ctx, logger, common.PtrValueOrDefault(options.Route))
var inbounds []adapter.Inbound if err != nil {
var outbounds []adapter.Outbound return nil, err
if len(options.Inbounds) > 0 { }
for i, inboundOptions := range options.Inbounds { inbounds := make([]adapter.Inbound, 0, len(options.Inbounds))
var inboundService adapter.Inbound outbounds := make([]adapter.Outbound, 0, len(options.Outbounds))
inboundService, err = inbound.New(ctx, router, logger, i, inboundOptions) for i, inboundOptions := range options.Inbounds {
if err != nil { var inboundService adapter.Inbound
return nil, err inboundService, err = inbound.New(ctx, router, logger, i, inboundOptions)
} if err != nil {
inbounds = append(inbounds, inboundService) return nil, err
} }
inbounds = append(inbounds, inboundService)
} }
for i, outboundOptions := range options.Outbounds { for i, outboundOptions := range options.Outbounds {
var outboundService adapter.Outbound var outboundService adapter.Outbound
@ -52,13 +49,9 @@ func NewService(ctx context.Context, options *option.Options) (*Service, error)
outbounds = append(outbounds, outboundService) outbounds = append(outbounds, outboundService)
} }
if len(outbounds) == 0 { if len(outbounds) == 0 {
outbounds = append(outbounds, outbound.NewDirect(nil, logger, "direct", &option.DirectOutboundOptions{})) outbounds = append(outbounds, outbound.NewDirect(nil, logger, "direct", option.DirectOutboundOptions{}))
} }
router.UpdateOutbounds(outbounds) router.UpdateOutbounds(outbounds)
err = router.UpdateRules(options.Routes)
if err != nil {
return nil, err
}
return &Service{ return &Service{
router: router, router: router,
logger: logger, logger: logger,