add subscribe support

This commit is contained in:
yaotthaha 2023-04-12 15:47:15 +08:00
parent 926b622710
commit 6a5962be57
23 changed files with 2258 additions and 5 deletions

8
box.go
View File

@ -106,23 +106,23 @@ func New(options Options) (*Box, error) {
inbounds = append(inbounds, in)
}
for i, outboundOptions := range options.Outbounds {
var out adapter.Outbound
var outs []adapter.Outbound
var tag string
if outboundOptions.Tag != "" {
tag = outboundOptions.Tag
} else {
tag = F.ToString(i)
}
out, err = outbound.New(
outs, err = outbound.NewGroup(
ctx,
router,
logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")),
logFactory,
tag,
outboundOptions)
if err != nil {
return nil, E.Cause(err, "parse outbound[", i, "]")
}
outbounds = append(outbounds, out)
outbounds = append(outbounds, outs...)
}
err = router.Initialize(inbounds, outbounds, func() adapter.Outbound {
out, oErr := outbound.New(ctx, router, logFactory.NewLogger("outbound/direct"), "direct", option.Outbound{Type: "direct", Tag: "default"})

View File

@ -0,0 +1,106 @@
//go:build with_subscribe
package main
import (
"context"
"encoding/json"
"fmt"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/subscribe"
E "github.com/sagernet/sing/common/exceptions"
"github.com/spf13/cobra"
"time"
)
var commandShowSubscribePeer = &cobra.Command{
Use: "showsub",
Short: "Show subscribe peer",
Run: func(cmd *cobra.Command, args []string) {
err := showSubscribePeer()
if err != nil {
log.Fatal(err)
}
},
}
var showSubscribePeerTags []string
func init() {
commandShowSubscribePeer.Flags().StringArrayVarP(&showSubscribePeerTags, "tag", "t", nil, "set tag")
mainCommand.AddCommand(commandShowSubscribePeer)
}
func showSubscribePeer() error {
options, err := readConfigAndMerge()
if err != nil {
return err
}
if options.Outbounds == nil || len(options.Outbounds) == 0 {
return E.New("no outbound found")
}
subscribeOptionsMap := make(map[string]option.Outbound)
all := false
if showSubscribePeerTags == nil || len(showSubscribePeerTags) == 0 {
all = true
} else {
for _, t := range showSubscribePeerTags {
subscribeOptionsMap[t] = option.Outbound{}
}
}
for _, o := range options.Outbounds {
if all {
if o.Type == C.TypeSubscribe {
subscribeOptionsMap[o.Tag] = o
}
continue
}
if _, ok := subscribeOptionsMap[o.Tag]; ok {
if o.Type != C.TypeSubscribe {
return E.New("outbound ", o.Tag, " is not subscribe")
}
subscribeOptionsMap[o.Tag] = o
}
}
if len(subscribeOptionsMap) > 0 {
for tag, opt := range subscribeOptionsMap {
if opt.Type == "" {
return E.New("outbound ", tag, " not found")
}
}
}
outs := make([]option.Outbound, 0)
for _, o := range subscribeOptionsMap {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
peers, err := subscribe.ParsePeer(ctx, o.Tag, o.SubscribeOptions)
cancel()
if err != nil {
return E.Cause(err, "show subscribe '", o.Tag, "' peer fail")
}
outs = append(outs, peers...)
}
m := map[string]any{
"outbounds": outs,
}
content, err := json.MarshalIndent(m, "", " ")
if err != nil {
return E.Cause(err, "show subscribe peer fail")
}
fmt.Println(string(content))
return nil
}

View File

@ -0,0 +1,91 @@
//go:build with_subscribe
package main
import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/subscribe"
E "github.com/sagernet/sing/common/exceptions"
"github.com/spf13/cobra"
"golang.org/x/net/context"
"time"
)
var commandUpdateSubscribe = &cobra.Command{
Use: "upsub",
Short: "Update subscribe",
Run: func(cmd *cobra.Command, args []string) {
err := updateSubscribe()
if err != nil {
log.Fatal(err)
}
},
}
var updateSubscribeTags []string
func init() {
commandUpdateSubscribe.Flags().StringArrayVarP(&updateSubscribeTags, "tag", "t", nil, "set tag")
mainCommand.AddCommand(commandUpdateSubscribe)
}
func updateSubscribe() error {
options, err := readConfigAndMerge()
if err != nil {
return err
}
if options.Outbounds == nil || len(options.Outbounds) == 0 {
return E.New("no outbound found")
}
subscribeOptionsMap := make(map[string]option.Outbound)
all := false
if updateSubscribeTags == nil || len(updateSubscribeTags) == 0 {
all = true
} else {
for _, t := range updateSubscribeTags {
subscribeOptionsMap[t] = option.Outbound{}
}
}
for _, o := range options.Outbounds {
if all {
if o.Type == C.TypeSubscribe {
subscribeOptionsMap[o.Tag] = o
}
continue
}
if _, ok := subscribeOptionsMap[o.Tag]; ok {
if o.Type != C.TypeSubscribe {
return E.New("outbound ", o.Tag, " is not subscribe")
}
subscribeOptionsMap[o.Tag] = o
}
}
if len(subscribeOptionsMap) > 0 {
for tag, opt := range subscribeOptionsMap {
if opt.Type == "" {
return E.New("outbound ", tag, " not found")
}
}
}
for _, o := range subscribeOptionsMap {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
err := subscribe.RequestAndCache(ctx, o.SubscribeOptions)
cancel()
if err != nil {
return E.Cause(err, "update subscribe '", o.Tag, "' fail")
}
log.Info("update subscribe '", o.Tag, "' success")
}
return nil
}

View File

@ -147,6 +147,80 @@ func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDia
}
}
func NewSimple(options option.DialerOptions) *DefaultDialer {
var dialer net.Dialer
var listener net.ListenConfig
if options.BindInterface != "" {
warnBindInterfaceOnUnsupportedPlatform.Check()
bindFunc := control.BindToInterface(control.DefaultInterfaceFinder(), options.BindInterface, -1)
dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)
}
if options.RoutingMark != 0 {
warnRoutingMarkOnUnsupportedPlatform.Check()
dialer.Control = control.Append(dialer.Control, control.RoutingMark(options.RoutingMark))
listener.Control = control.Append(listener.Control, control.RoutingMark(options.RoutingMark))
}
if options.ReuseAddr {
warnReuseAdderOnUnsupportedPlatform.Check()
listener.Control = control.Append(listener.Control, control.ReuseAddr())
}
if options.ProtectPath != "" {
warnProtectPathOnNonAndroid.Check()
dialer.Control = control.Append(dialer.Control, control.ProtectPath(options.ProtectPath))
listener.Control = control.Append(listener.Control, control.ProtectPath(options.ProtectPath))
}
if options.ConnectTimeout != 0 {
dialer.Timeout = time.Duration(options.ConnectTimeout)
} else {
dialer.Timeout = C.TCPTimeout
}
if options.TCPFastOpen {
warnTFOOnUnsupportedPlatform.Check()
}
var udpFragment bool
if options.UDPFragment != nil {
udpFragment = *options.UDPFragment
} else {
udpFragment = options.UDPFragmentDefault
}
if !udpFragment {
dialer.Control = control.Append(dialer.Control, control.DisableUDPFragment())
listener.Control = control.Append(listener.Control, control.DisableUDPFragment())
}
var (
dialer4 = dialer
udpDialer4 = dialer
udpAddr4 string
)
if options.Inet4BindAddress != nil {
bindAddr := options.Inet4BindAddress.Build()
dialer4.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()}
udpDialer4.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
udpAddr4 = M.SocksaddrFrom(bindAddr, 0).String()
}
var (
dialer6 = dialer
udpDialer6 = dialer
udpAddr6 string
)
if options.Inet6BindAddress != nil {
bindAddr := options.Inet6BindAddress.Build()
dialer6.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()}
udpDialer6.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String()
}
return &DefaultDialer{
tfo.Dialer{Dialer: dialer4, DisableTFO: !options.TCPFastOpen},
tfo.Dialer{Dialer: dialer6, DisableTFO: !options.TCPFastOpen},
udpDialer4,
udpDialer6,
listener,
udpAddr4,
udpAddr6,
}
}
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
if !address.IsValid() {
return nil, E.New("invalid address")

View File

@ -27,3 +27,5 @@ const (
TypeSelector = "selector"
TypeURLTest = "urltest"
)
const TypeSubscribe = "subscribe"

View File

@ -25,6 +25,7 @@ type _Outbound struct {
VLESSOptions VLESSOutboundOptions `json:"-"`
SelectorOptions SelectorOutboundOptions `json:"-"`
URLTestOptions URLTestOutboundOptions `json:"-"`
SubscribeOptions SubscribeOutboundOptions `json:"-"`
}
type Outbound _Outbound
@ -64,6 +65,8 @@ func (h Outbound) MarshalJSON() ([]byte, error) {
v = h.SelectorOptions
case C.TypeURLTest:
v = h.URLTestOptions
case C.TypeSubscribe:
v = h.SubscribeOptions
default:
return nil, E.New("unknown outbound type: ", h.Type)
}
@ -109,6 +112,8 @@ func (h *Outbound) UnmarshalJSON(bytes []byte) error {
v = &h.SelectorOptions
case C.TypeURLTest:
v = &h.URLTestOptions
case C.TypeSubscribe:
v = &h.SubscribeOptions
default:
return E.New("unknown outbound type: ", h.Type)
}

83
option/subscribe.go Normal file
View File

@ -0,0 +1,83 @@
package option
import (
"github.com/sagernet/sing-box/common/json"
E "github.com/sagernet/sing/common/exceptions"
"regexp"
)
type SubscribeOutboundOptions struct {
Url string `json:"url"`
CacheFile string `json:"cache_file,omitempty"`
ForceUpdateDuration Duration `json:"force_update_duration,omitempty"`
DNS string `json:"dns,omitempty"`
Filter *Filter `json:"filter,omitempty"`
RequestDialerOptions *RequestDialerOptions `json:"request_dialer,omitempty"`
DialerOptions *DialerOptions `json:"dialer,omitempty"`
ProxyGroupOptions
CustomGroup Listable[CustomGroupOptions] `json:"custom_group,omitempty"`
}
type ProxyGroupOptions struct {
ProxyType string `json:"proxy_type"`
SelectorOptions *SelectorOutboundOptions `json:"selector,omitempty"`
URLTestOptions *URLTestOutboundOptions `json:"urltest,omitempty"`
}
type CustomGroupOptions struct {
Tag string `json:"tag,omitempty"`
Filter *Filter `json:"filter,omitempty"`
ProxyGroupOptions
}
type Filter struct {
WhiteMode bool `json:"white_mode,omitempty"`
Rule Listable[*regexp.Regexp] `json:"rule,omitempty"`
}
type _filter struct {
WhiteMode bool `json:"white_mode,omitempty"`
Rule Listable[string] `json:"rule,omitempty"`
}
func (f *Filter) UnmarshalJSON(content []byte) error {
var _f _filter
err := json.Unmarshal(content, &_f)
if err != nil {
return err
}
f.WhiteMode = _f.WhiteMode
f.Rule = make(Listable[*regexp.Regexp], 0)
for _, r := range _f.Rule {
reg, err := regexp.Compile(r)
if err != nil {
return E.New("invalid regexp: ", r)
}
f.Rule = append(f.Rule, reg)
}
return nil
}
func (f Filter) MarshalJSON() ([]byte, error) {
_f := _filter{
WhiteMode: f.WhiteMode,
Rule: make(Listable[string], 0),
}
for _, r := range f.Rule {
_f.Rule = append(_f.Rule, r.String())
}
return json.Marshal(_f)
}
type RequestDialerOptions struct {
BindInterface string `json:"bind_interface,omitempty"`
Inet4BindAddress *ListenAddress `json:"inet4_bind_address,omitempty"`
Inet6BindAddress *ListenAddress `json:"inet6_bind_address,omitempty"`
ProtectPath string `json:"protect_path,omitempty"`
RoutingMark int `json:"routing_mark,omitempty"`
ReuseAddr bool `json:"reuse_addr,omitempty"`
ConnectTimeout Duration `json:"connect_timeout,omitempty"`
TCPFastOpen bool `json:"tcp_fast_open,omitempty"`
UDPFragment *bool `json:"udp_fragment,omitempty"`
UDPFragmentDefault bool `json:"-"`
}

View File

@ -82,7 +82,7 @@ func (v NetworkList) Build() []string {
return strings.Split(string(v), "\n")
}
type Listable[T comparable] []T
type Listable[T any] []T
func (l Listable[T]) MarshalJSON() ([]byte, error) {
arrayList := []T(l)

View File

@ -2,6 +2,7 @@ package outbound
import (
"context"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
@ -11,6 +12,7 @@ import (
)
func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Outbound) (adapter.Outbound, error) {
/**
var metadata *adapter.InboundContext
if tag != "" {
ctx, metadata = adapter.AppendContext(ctx)
@ -20,6 +22,7 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, t
return nil, E.New("missing outbound type")
}
ctx = ContextWithTag(ctx, tag)
*/
switch options.Type {
case C.TypeDirect:
return NewDirect(router, logger, tag, options.DirectOptions)
@ -59,3 +62,25 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, t
return nil, E.New("unknown outbound type: ", options.Type)
}
}
func NewGroup(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, options option.Outbound) ([]adapter.Outbound, error) {
var metadata *adapter.InboundContext
if tag != "" {
ctx, metadata = adapter.AppendContext(ctx)
metadata.Outbound = tag
}
if options.Type == "" {
return nil, E.New("missing outbound type")
}
ctx = ContextWithTag(ctx, tag)
switch options.Type {
case C.TypeSubscribe:
return NewSubscribe(ctx, router, logFactory, tag, options.SubscribeOptions)
default:
out, err := New(ctx, router, logFactory.NewLogger(F.ToString("outbound/", options.Type, "[", tag, "]")), tag, options)
if err != nil {
return nil, err
}
return []adapter.Outbound{out}, nil
}
}

37
outbound/subscribe.go Normal file
View File

@ -0,0 +1,37 @@
package outbound
import (
"context"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/subscribe"
F "github.com/sagernet/sing/common/format"
)
func NewSubscribe(ctx context.Context, router adapter.Router, logFactory log.Factory, tag string, options option.SubscribeOutboundOptions) ([]adapter.Outbound, error) {
outboundOptions, err := subscribe.ParsePeer(ctx, tag, options)
if err != nil {
return nil, err
}
outbounds := make([]adapter.Outbound, 0)
for i, outOptions := range outboundOptions {
var out adapter.Outbound
var outTag string
if outOptions.Tag != "" {
outTag = outOptions.Tag
} else {
outTag = F.ToString(tag, ":", i)
outOptions.Tag = outTag
}
out, err := New(ctx, router, logFactory.NewLogger(F.ToString("outbound/", outOptions.Type, "[", outTag, "]")), outTag, outOptions)
if err != nil {
return nil, err
}
outbounds = append(outbounds, out)
}
return outbounds, nil
}

23
subscribe/dialer.go Normal file
View File

@ -0,0 +1,23 @@
package subscribe
import (
D "github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/option"
N "github.com/sagernet/sing/common/network"
)
func NewDialer(options option.RequestDialerOptions) N.Dialer {
opt := option.DialerOptions{
BindInterface: options.BindInterface,
Inet4BindAddress: options.Inet4BindAddress,
Inet6BindAddress: options.Inet6BindAddress,
ProtectPath: options.ProtectPath,
RoutingMark: options.RoutingMark,
ReuseAddr: options.ReuseAddr,
ConnectTimeout: options.ConnectTimeout,
TCPFastOpen: options.TCPFastOpen,
UDPFragment: options.UDPFragment,
UDPFragmentDefault: options.UDPFragmentDefault,
}
return D.NewSimple(opt)
}

320
subscribe/dns.go Normal file
View File

@ -0,0 +1,320 @@
package subscribe
import (
"context"
"fmt"
mDNS "github.com/miekg/dns"
dns "github.com/sagernet/sing-dns"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"net"
"net/netip"
"net/url"
"strings"
"sync"
"time"
)
const (
queryTimeout = 5 * time.Second
DefaultUDPDNS = "223.5.5.5"
)
type DNS struct {
ctx context.Context
transport dns.Transport
dialer N.Dialer
}
func NewDNS(ctx context.Context, addr string, dialer N.Dialer) (*DNS, error) {
switch {
case strings.Index(addr, "tcp://") == 0:
// tcp dns
addr = strings.TrimPrefix(addr, "tcp://")
// check is ip
ip, err := netip.ParseAddr(addr)
if err == nil {
d := &DNS{}
d.ctx = ctx
d.dialer = dialer
d.transport, err = dns.NewTCPTransport("dns-tcp", ctx, d.dialer, M.ParseSocksaddr(net.JoinHostPort(ip.String(), "53")))
if err != nil {
return nil, err
}
return d, nil
}
// check is ip:port
host, port, err := net.SplitHostPort(addr)
if err == nil {
ip, err := netip.ParseAddr(host)
if err != nil {
return nil, fmt.Errorf("invalid dns address: %s", "tcp://"+addr)
}
d := &DNS{}
d.ctx = ctx
d.dialer = dialer
d.transport, err = dns.NewTCPTransport("dns-tcp", ctx, d.dialer, M.ParseSocksaddr(net.JoinHostPort(ip.String(), port)))
if err != nil {
return nil, err
}
return d, nil
}
return nil, fmt.Errorf("invalid dns address: %s", "tcp://"+addr)
case strings.Index(addr, "udp://") == 0:
// udp dns
addr = strings.TrimPrefix(addr, "udp://")
// check is ip
ip, err := netip.ParseAddr(addr)
if err == nil {
d := &DNS{}
d.ctx = ctx
d.dialer = dialer
d.transport, err = dns.NewUDPTransport("dns-udp", ctx, d.dialer, M.ParseSocksaddr(net.JoinHostPort(ip.String(), "53")))
if err != nil {
return nil, err
}
return d, nil
}
// check is ip:port
host, port, err := net.SplitHostPort(addr)
if err == nil {
ip, err := netip.ParseAddr(host)
if err != nil {
return nil, fmt.Errorf("invalid dns address: %s", "udp://"+addr)
}
d := &DNS{}
d.ctx = ctx
d.dialer = dialer
d.transport, err = dns.NewUDPTransport("dns-udp", ctx, d.dialer, M.ParseSocksaddr(net.JoinHostPort(ip.String(), port)))
if err != nil {
return nil, err
}
return d, nil
}
return nil, fmt.Errorf("invalid dns address: %s", "udp://"+addr)
case strings.Index(addr, "tls://") == 0:
// dot dns
addr = strings.TrimPrefix(addr, "tls://")
// check is ip
ip, err := netip.ParseAddr(addr)
if err == nil {
d := &DNS{}
d.ctx = ctx
d.dialer = dialer
d.transport, err = dns.NewTLSTransport("dns-dot", ctx, d.dialer, M.ParseSocksaddr(net.JoinHostPort(ip.String(), "853")))
if err != nil {
return nil, err
}
return d, nil
}
// check is ip:port
host, port, err := net.SplitHostPort(addr)
if err == nil {
ip, err := netip.ParseAddr(host)
if err != nil {
return nil, fmt.Errorf("invalid dns address: %s", "tls://"+addr)
}
d := &DNS{}
d.ctx = ctx
d.dialer = dialer
d.transport, err = dns.NewTLSTransport("dns-dot", ctx, d.dialer, M.ParseSocksaddr(net.JoinHostPort(ip.String(), port)))
if err != nil {
return nil, err
}
return d, nil
}
return nil, fmt.Errorf("invalid dns address: %s", "tls://"+addr)
case strings.Index(addr, "https://") == 0:
// doh dns
u, err := url.Parse(addr)
if err != nil {
return nil, fmt.Errorf("invalid dns address: %s, err: %s", addr, err)
}
if u.Fragment != "" || u.RawFragment != "" || u.RawQuery != "" {
return nil, fmt.Errorf("invalid dns address: %s", addr)
}
hostAddr := u.Host
// check is ip
ip, err := netip.ParseAddr(hostAddr)
if err == nil {
d := &DNS{}
d.ctx = ctx
d.dialer = dialer
uNew := *u
uNew.Host = net.JoinHostPort(ip.String(), "443")
d.transport = dns.NewHTTPSTransport("dns-https", d.dialer, uNew.String())
return d, nil
}
// check is ip:port
host, port, err := net.SplitHostPort(hostAddr)
if err == nil {
ip, err := netip.ParseAddr(host)
if err != nil {
return nil, fmt.Errorf("invalid dns address: %s", "https://"+addr)
}
d := &DNS{}
d.ctx = ctx
d.dialer = dialer
uNew := *u
uNew.Host = net.JoinHostPort(ip.String(), port)
d.transport = dns.NewHTTPSTransport("dns-https", d.dialer, uNew.String())
return d, nil
}
return nil, fmt.Errorf("invalid dns address: %s, domain is not supported", addr)
case addr == "":
ip, err := netip.ParseAddr(DefaultUDPDNS)
if err != nil {
return nil, fmt.Errorf("invalid dns address: %s", DefaultUDPDNS)
}
d := &DNS{}
d.ctx = ctx
d.dialer = dialer
d.transport, err = dns.NewUDPTransport("dns-udp", ctx, d.dialer, M.ParseSocksaddr(net.JoinHostPort(ip.String(), "53")))
if err != nil {
return nil, err
}
return d, nil
default:
// check is udp dns
// check is ip
ip, err := netip.ParseAddr(addr)
if err == nil {
d := &DNS{}
d.ctx = ctx
d.dialer = dialer
d.transport, err = dns.NewUDPTransport("dns-udp", ctx, d.dialer, M.ParseSocksaddr(net.JoinHostPort(ip.String(), "53")))
if err != nil {
return nil, err
}
return d, nil
}
// check is ip:port
host, port, err := net.SplitHostPort(addr)
if err == nil {
d := &DNS{}
d.ctx = ctx
d.dialer = dialer
d.transport, err = dns.NewUDPTransport("dns-udp", ctx, d.dialer, M.ParseSocksaddr(net.JoinHostPort(host, port)))
if err != nil {
return nil, err
}
return d, nil
}
return nil, fmt.Errorf("invalid dns address: %s", addr)
}
}
func (d *DNS) Query(msg *mDNS.Msg) (*mDNS.Msg, error) {
ctx, cancel := context.WithTimeout(d.ctx, queryTimeout)
defer cancel()
return d.transport.Exchange(ctx, msg)
}
func (d *DNS) QueryTypeA(domain string) ([]string, error) {
msg := new(mDNS.Msg)
msg.SetQuestion(mDNS.Fqdn(domain), mDNS.TypeA)
m, err := d.Query(msg)
if err != nil {
return nil, err
}
ips := make([]string, 0)
for _, a := range m.Answer {
if a.Header().Rrtype == mDNS.TypeA {
ips = append(ips, a.(*mDNS.A).A.String())
}
}
if len(ips) == 0 {
return nil, fmt.Errorf("no A record")
}
return ips, nil
}
func (d *DNS) QueryTypeAAAA(domain string) ([]string, error) {
msg := new(mDNS.Msg)
msg.SetQuestion(mDNS.Fqdn(domain), mDNS.TypeAAAA)
m, err := d.Query(msg)
if err != nil {
return nil, err
}
ips := make([]string, 0)
for _, a := range m.Answer {
if a.Header().Rrtype == mDNS.TypeAAAA {
ips = append(ips, a.(*mDNS.AAAA).AAAA.String())
}
}
if len(ips) == 0 {
return nil, fmt.Errorf("no A record")
}
return ips, nil
}
func (d *DNS) QueryIP(domain string) ([]string, error) {
wg := sync.WaitGroup{}
ch := make(chan []string, 2)
wg.Add(1)
go func() {
defer wg.Done()
msg, err := d.QueryTypeA(domain)
if err != nil {
return
}
ch <- msg
}()
wg.Add(1)
go func() {
defer wg.Done()
msg, err := d.QueryTypeAAAA(domain)
if err != nil {
return
}
ch <- msg
}()
wg.Wait()
ips := make([]string, 0)
for {
select {
case m := <-ch:
ips = append(ips, m...)
default:
close(ch)
if len(ips) == 0 {
return nil, fmt.Errorf("no ip found")
}
return ips, nil
}
}
}

97
subscribe/proxy/http.go Normal file
View File

@ -0,0 +1,97 @@
package proxy
import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"net"
"net/url"
"strconv"
)
type ProxyHTTP struct {
Tag string // 标签
Type string // 代理类型
//
Dialer option.DialerOptions
//
Address string // IP地址或域名
Port uint16 // 端口
Username string // 用户名
Password string // 密码
TLSEnable bool // 是否启用TLS
}
func (p *ProxyHTTP) GetTag() string {
return p.Tag
}
func (p *ProxyHTTP) GetType() string {
return C.TypeHTTP
}
func (p *ProxyHTTP) ParseLink(link string) error {
u, err := url.Parse(link)
if err != nil {
return err
}
p.Address = u.Hostname()
if u.Port() == "" {
if u.Scheme == "https" {
p.Port = 443
} else {
p.Port = 80
}
} else {
portUint16, err := strconv.ParseUint(u.Port(), 10, 16)
if err != nil {
return err
}
p.Port = uint16(portUint16)
}
p.Username = u.User.Username()
p.Password, _ = u.User.Password()
p.Tag = u.Fragment
if p.Tag == "" {
p.Tag = net.JoinHostPort(p.Address, strconv.FormatUint(uint64(p.Port), 10))
}
if u.Scheme == "https" {
p.TLSEnable = true
}
p.Type = C.TypeHTTP
return nil
}
func (p *ProxyHTTP) SetDialer(dialer option.DialerOptions) {
p.Dialer = dialer
}
func (p *ProxyHTTP) GenerateOutboundOptions() (option.Outbound, error) {
out := option.Outbound{
Tag: p.Tag,
Type: C.TypeHTTP,
HTTPOptions: option.HTTPOutboundOptions{
ServerOptions: option.ServerOptions{
Server: p.Address,
ServerPort: p.Port,
},
Username: p.Username,
Password: p.Password,
},
}
if p.TLSEnable {
out.HTTPOptions.TLS = &option.OutboundTLSOptions{
Enabled: true,
}
}
out.HTTPOptions.DialerOptions = p.Dialer
return out, nil
}

162
subscribe/proxy/hysteria.go Normal file
View File

@ -0,0 +1,162 @@
package proxy
import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"net"
"net/url"
"strconv"
"strings"
)
type ProxyHysteria struct {
Tag string // 标签
Type string // 代理类型
//
Dialer option.DialerOptions
//
Address string // IP地址或域名
Port uint16 // 端口
Protocol string // 协议 udp/wechat-video/faketcp
AuthStr string // 认证字符串
SNI string // TLS SNI
Insecure bool // TLS Insecure
UpMbps uint64 // 上行速度
DownMbps uint64 // 下行速度
ALPN []string // QUIC TLS ALPN
Obfs string // 混淆
ObfsParam string // 混淆参数
}
func (p *ProxyHysteria) GetType() string {
return C.TypeHysteria
}
func (p *ProxyHysteria) GetTag() string {
return p.Tag
}
func (p *ProxyHysteria) ParseLink(link string) error {
configStr := strings.TrimPrefix(link, "hysteria://")
u, err := url.Parse("http://" + configStr)
if err != nil {
return E.New("invalid hysteria link")
}
host, port, err := net.SplitHostPort(u.Host)
if err != nil {
return E.New("invalid hysteria link")
}
portUint, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return E.New("invalid hysteria link")
}
query := u.Query()
if query == nil {
return E.New("invalid hysteria link")
}
p.Address = host
p.Port = uint16(portUint)
if query.Get("protocol") != "" {
p.Protocol = query.Get("protocol")
} else {
p.Protocol = "udp"
}
if query.Get("auth") != "" {
p.AuthStr = query.Get("auth")
}
if query.Get("upmbps") != "" {
upMbpsStr := query.Get("upmbps")
upMbps, err := strconv.ParseUint(upMbpsStr, 10, 32)
if err != nil {
return E.New("invalid hysteria link")
}
p.UpMbps = upMbps
} else {
return E.New("invalid hysteria link")
}
if query.Get("downmbps") != "" {
downMbpsStr := query.Get("upmbps")
downMbps, err := strconv.ParseUint(downMbpsStr, 10, 32)
if err != nil {
return E.New("invalid hysteria link")
}
p.DownMbps = downMbps
} else {
return E.New("invalid hysteria link")
}
if query.Get("obfs") != "" {
p.Obfs = query.Get("obfs")
if query.Get("obfsParam") != "" {
p.ObfsParam = query.Get("obfsParam")
}
}
if query.Get("peer") != "" {
p.SNI = query.Get("peer")
}
if query.Get("insecure") == "1" {
p.Insecure = true
}
if query.Get("alpn") != "" {
p.ALPN = []string{query.Get("alpn")}
}
if u.Fragment != "" {
p.Tag = u.Fragment
} else {
p.Tag = net.JoinHostPort(host, port)
}
p.Type = C.TypeHysteria
return nil
}
func (p *ProxyHysteria) SetDialer(dialer option.DialerOptions) {
p.Dialer = dialer
}
func (p *ProxyHysteria) GenerateOutboundOptions() (option.Outbound, error) {
if p.Protocol != "udp" {
return option.Outbound{}, E.New("hysteria protocol '", p.Protocol, "' not supported in sing-box")
}
/**
if p.Obfs == "xplus" {
return nil, E.New("hysteria obfs `%s` not supported in sing-box", p.Obfs)
}
*/
out := option.Outbound{
Tag: p.Tag,
Type: C.TypeHysteria,
HysteriaOptions: option.HysteriaOutboundOptions{
ServerOptions: option.ServerOptions{
Server: p.Address,
ServerPort: p.Port,
},
AuthString: p.AuthStr,
UpMbps: int(p.UpMbps),
DownMbps: int(p.DownMbps),
Obfs: p.ObfsParam,
TLS: &option.OutboundTLSOptions{
Enabled: true,
ServerName: p.SNI,
Insecure: p.Insecure,
ALPN: p.ALPN,
},
},
}
out.HysteriaOptions.DialerOptions = p.Dialer
return out, nil
}

93
subscribe/proxy/proxy.go Normal file
View File

@ -0,0 +1,93 @@
package proxy
import (
"fmt"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"strings"
)
type Proxy interface {
GetTag() string // 获取节点名称
GetType() string // 获取节点类型
SetDialer(dialer option.DialerOptions) // 设置Dialer
ParseLink(link string) error // 解析链接
// ParseClash(config string) error // 解析Clash配置
GenerateOutboundOptions() (option.Outbound, error) // 获取配置
}
func CheckLink(link string) string {
switch {
case strings.Index(link, "http://") == 0:
return C.TypeHTTP
case strings.Index(link, "https://") == 0:
return C.TypeHTTP
case strings.Index(link, "socks") == 0:
return C.TypeSocks
case strings.Index(link, "socks4") == 0:
return C.TypeSocks
case strings.Index(link, "socks4a") == 0:
return C.TypeSocks
case strings.Index(link, "socks5") == 0:
return C.TypeSocks
case strings.Index(link, "socks5h") == 0:
return C.TypeSocks
case strings.Index(link, "vmess://") == 0:
return C.TypeVMess
case strings.Index(link, "ss://") == 0:
return C.TypeShadowsocks
case strings.Index(link, "ssr://") == 0:
return C.TypeShadowsocksR
case strings.Index(link, "trojan://") == 0:
return C.TypeTrojan
case strings.Index(link, "hysteria://") == 0:
return C.TypeHysteria
default:
return ""
}
}
func ParsePeers(content string) ([]Proxy, error) {
raw, err := Base64Decode(content)
if err != nil {
return nil, fmt.Errorf("parse peers failed: %s", err)
}
peerLinks := strings.Split(string(raw), "\r\n")
peers := make([]Proxy, 0)
for _, link := range peerLinks {
proxyType := CheckLink(link)
var proxy Proxy
switch proxyType {
case C.TypeHTTP:
proxy = &ProxyHTTP{}
case C.TypeSocks:
proxy = &ProxySocks{}
case C.TypeVMess:
proxy = &ProxyVMess{}
case C.TypeShadowsocks:
proxy = &ProxyShadowsocks{}
case C.TypeShadowsocksR:
proxy = &ProxyShadowsocksR{}
case C.TypeTrojan:
proxy = &ProxyTrojan{}
case C.TypeHysteria:
proxy = &ProxyHysteria{}
default:
continue
}
err := proxy.ParseLink(link)
if err != nil {
continue
}
peers = append(peers, proxy)
}
if len(peers) == 0 {
return nil, fmt.Errorf("no valid peers")
}
return peers, nil
}

View File

@ -0,0 +1,181 @@
package proxy
import (
"encoding/base64"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"net"
"net/url"
"strconv"
"strings"
)
type ProxyShadowsocks struct {
Tag string // 标签
Type string // 代理类型
//
Dialer option.DialerOptions
//
Address string // IP地址或域名
Port uint16 // 端口
Method string // 加密方式
Password string // 密码
Plugin string // 插件
PluginOptions string // 插件选项
}
func (p *ProxyShadowsocks) GetType() string {
return C.TypeSocks
}
func (p *ProxyShadowsocks) GetTag() string {
return p.Tag
}
func (p *ProxyShadowsocks) ParseLink(link string) error {
configStr := strings.TrimPrefix(link, "ss://")
r := func(uri string) int {
func() {
var (
suri = strings.Split(uri, "#")
stag = ""
)
if len(suri) <= 2 {
if len(suri) == 2 {
stag = "#" + suri[1]
}
suriDecode, err := Base64Decode(suri[0])
if err != nil {
return
}
uri = string(suriDecode) + stag
}
}()
u, err := url.Parse("http://" + uri)
if err != nil {
return 1
}
if u.Host == "" {
return 1
}
host, port, err := net.SplitHostPort(u.Host)
if err != nil {
host = u.Hostname()
port = "80"
}
portUint, err := strconv.ParseUint(port, 10, 16)
if err != nil || portUint == 0 || portUint > 65535 {
return 1
}
var userinfo []string
if u.User != nil {
username := u.User.Username()
password, _ := u.User.Password()
if username != "" && password != "" {
userinfo = []string{username, password}
} else if username != "" {
usernameDecode, err := base64.RawURLEncoding.DecodeString(username)
if err != nil {
return 1
}
userinfo = strings.Split(string(usernameDecode), ":")
if len(userinfo) != 2 {
return 1
}
}
} else {
return 1
}
p.Address = host
p.Port = uint16(portUint)
p.Method = userinfo[0]
p.Password = userinfo[1]
if u.RawQuery != "" {
plugin := u.Query().Get("plugin")
if plugin != "" {
pluginInfo := strings.Split(plugin, ";")
pi := pluginInfo[0]
p.Plugin = pi
if pi != "" {
pluginOpts := strings.Join(pluginInfo[1:], ";")
p.PluginOptions = pluginOpts
}
}
}
if u.Fragment != "" {
p.Tag = u.Fragment
} else {
p.Tag = net.JoinHostPort(host, port)
}
return 0
}(configStr)
if r != 0 {
uriSlice := strings.Split(configStr, "@")
if len(uriSlice) < 2 {
return E.New("invalid shadowsocks link")
} else if len(uriSlice) > 2 {
uriSlice = []string{strings.Join(uriSlice[:len(uriSlice)-1], "@"), uriSlice[len(uriSlice)-1]}
}
host, port, err := net.SplitHostPort(uriSlice[1])
if err != nil {
return E.New("invalid shadowsocks link")
}
portUint, err := strconv.ParseUint(port, 10, 16)
if err != nil || portUint == 0 || portUint > 65535 {
return E.New("invalid shadowsocks link")
}
authInfo := strings.SplitN(uriSlice[0], ":", 2)
p.Address = host
p.Port = uint16(portUint)
p.Method = authInfo[0]
p.Password = authInfo[1]
p.Tag = net.JoinHostPort(host, port)
}
p.Type = C.TypeShadowsocks
return nil
}
func (p *ProxyShadowsocks) SetDialer(dialer option.DialerOptions) {
p.Dialer = dialer
}
func (p *ProxyShadowsocks) GenerateOutboundOptions() (option.Outbound, error) {
if !checkShadowsocksAllowMethod(p.Method) {
return option.Outbound{}, E.New("shadowsocks method '", p.Method, "' is not supported in sing-box")
}
out := option.Outbound{
Tag: p.Tag,
Type: C.TypeShadowsocks,
ShadowsocksOptions: option.ShadowsocksOutboundOptions{
ServerOptions: option.ServerOptions{
Server: p.Address,
ServerPort: p.Port,
},
Password: p.Password,
Method: p.Method,
},
}
if p.Plugin != "" {
out.ShadowsocksOptions.Plugin = p.Plugin
out.ShadowsocksOptions.PluginOptions = p.PluginOptions
}
out.ShadowsocksOptions.DialerOptions = p.Dialer
return out, nil
}

View File

@ -0,0 +1,143 @@
package proxy
import (
"encoding/base64"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"net"
"net/url"
"strconv"
"strings"
)
type ProxyShadowsocksR struct {
Tag string // 标签
Type string // 代理类型
//
Dialer option.DialerOptions
//
Address string // IP地址或域名
Port uint16 // 端口
Method string // 加密方式
Password string // 密码
Obfs string // 混淆
ObfsParam string // 混淆参数
Protocol string // 协议
ProtocolParam string // 协议参数
}
func (p *ProxyShadowsocksR) GetType() string {
return C.TypeShadowsocksR
}
func (p *ProxyShadowsocksR) GetTag() string {
return p.Tag
}
func (p *ProxyShadowsocksR) ParseLink(link string) error {
link = strings.TrimPrefix(link, "ssr://")
uriDecodeBytes, err := base64.StdEncoding.DecodeString(link)
if err != nil {
return E.New("invalid shadowsocksR link: ", err.Error())
}
uriSlice := strings.Split(string(uriDecodeBytes), "/")
userInfo := strings.Split(uriSlice[0], ":")
if len(userInfo) != 6 {
return E.New("invalid shadowsocksR link")
}
_, params, found := strings.Cut(uriSlice[1], "?")
if !found {
return E.New("invalid shadowsocksR link")
}
query, err := url.ParseQuery(params)
if err != nil {
return E.New("invalid shadowsocksR link: ", err.Error())
}
var (
protoParam string
obfsParam string
remarks string
)
if query.Get("protoparam") != "" {
protoParamBytes, err := base64.StdEncoding.DecodeString(query.Get("protoparam"))
if err == nil {
protoParam = string(protoParamBytes)
}
}
if query.Get("obfsparam") != "" {
obfsParamBytes, err := base64.StdEncoding.DecodeString(query.Get("obfsparam"))
if err == nil {
obfsParam = string(obfsParamBytes)
}
}
if query.Get("remarks") != "" {
remarksBytes, err := base64.StdEncoding.DecodeString(query.Get("remarks"))
if err == nil {
remarks = string(remarksBytes)
}
}
var (
server = userInfo[0]
serverPort = userInfo[1]
protocol = userInfo[2]
method = userInfo[3]
obfs = userInfo[4]
passwordBase64 = userInfo[5]
password string
)
passwordBytes, err := base64.StdEncoding.DecodeString(passwordBase64)
if err != nil {
return E.New("invalid shadowsocksR link")
}
password = string(passwordBytes)
portUint, err := strconv.ParseUint(serverPort, 10, 16)
if err != nil || portUint == 0 || portUint > 65535 {
return E.New("invalid shadowsocksR link")
}
if remarks == "" {
remarks = net.JoinHostPort(server, serverPort)
}
p.Address = server
p.Port = uint16(portUint)
p.Method = method
p.Password = password
p.Obfs = obfs
p.ObfsParam = obfsParam
p.Protocol = protocol
p.ProtocolParam = protoParam
p.Tag = remarks
p.Type = C.TypeShadowsocksR
return nil
}
func (p *ProxyShadowsocksR) SetDialer(dialer option.DialerOptions) {
p.Dialer = dialer
}
func (p *ProxyShadowsocksR) GenerateOutboundOptions() (option.Outbound, error) {
out := option.Outbound{
Tag: p.Tag,
Type: C.TypeShadowsocksR,
ShadowsocksROptions: option.ShadowsocksROutboundOptions{
ServerOptions: option.ServerOptions{
Server: p.Address,
ServerPort: p.Port,
},
Method: p.Method,
Password: p.Password,
Obfs: p.Obfs,
ObfsParam: p.ObfsParam,
Protocol: p.Protocol,
ProtocolParam: p.ProtocolParam,
},
}
out.ShadowsocksROptions.DialerOptions = p.Dialer
return out, nil
}

92
subscribe/proxy/socks.go Normal file
View File

@ -0,0 +1,92 @@
package proxy
import (
C "github.com/sagernet/sing-box/constant"
option "github.com/sagernet/sing-box/option"
"net"
"net/url"
"strconv"
"strings"
)
type ProxySocks struct {
Tag string // 标签
Type string // 代理类型
//
Dialer option.DialerOptions
//
Address string // IP地址或域名
Port uint16 // 端口
Username string // 用户名
Password string // 密码
SocksVersion string // SOCKS版本
}
func (p *ProxySocks) GetType() string {
return C.TypeSocks
}
func (p *ProxySocks) GetTag() string {
return p.Tag
}
func (p *ProxySocks) ParseLink(link string) error {
u, err := url.Parse(link)
if err != nil {
return err
}
p.Address = u.Hostname()
if u.Port() == "" {
p.Port = 80
} else {
portUint16, err := strconv.ParseUint(u.Port(), 10, 16)
if err != nil {
return err
}
p.Port = uint16(portUint16)
}
p.Username = u.User.Username()
p.Password, _ = u.User.Password()
if strings.Index(u.Scheme, "4") > 0 {
p.SocksVersion = "4"
} else {
p.SocksVersion = "5"
}
p.Tag = u.Fragment
if p.Tag == "" {
p.Tag = net.JoinHostPort(p.Address, strconv.FormatUint(uint64(p.Port), 10))
}
p.Type = C.TypeSocks
return nil
}
func (p *ProxySocks) SetDialer(dialer option.DialerOptions) {
p.Dialer = dialer
}
func (p *ProxySocks) GenerateOutboundOptions() (option.Outbound, error) {
out := option.Outbound{
Tag: p.Tag,
Type: C.TypeSocks,
SocksOptions: option.SocksOutboundOptions{
ServerOptions: option.ServerOptions{
Server: p.Address,
ServerPort: p.Port,
},
Username: p.Username,
Password: p.Password,
Version: p.SocksVersion,
},
}
out.SocksOptions.DialerOptions = p.Dialer
return out, nil
}

44
subscribe/proxy/tools.go Normal file
View File

@ -0,0 +1,44 @@
package proxy
import "encoding/base64"
func Base64Decode(rawData string) ([]byte, error) {
r, err := base64.RawURLEncoding.DecodeString(rawData)
if err == nil {
return r, nil
}
r, err = base64.URLEncoding.DecodeString(rawData)
if err == nil {
return r, nil
}
r, err = base64.StdEncoding.DecodeString(rawData)
if err == nil {
return r, nil
}
return nil, err
}
var allowShadowsocksMethod = map[string]bool{
"none": true,
"2022-blake3-aes-128-gcm": true,
"2022-blake3-aes-256-gcm": true,
"2022-blake3-chacha20-poly1305": true,
"aes-128-gcm": true,
"aes-192-gcm": true,
"aes-256-gcm": true,
"chacha20-ietf-poly1305": true,
"xchacha20-ietf-poly1305": true,
"aes-128-ctr": true,
"aes-192-ctr": true,
"aes-256-ctr": true,
"aes-128-cfb": true,
"aes-192-cfb": true,
"aes-256-cfb": true,
"rc4-md5": true,
"chacha20-ietf": true,
"xchacha20": true,
}
func checkShadowsocksAllowMethod(method string) bool {
return allowShadowsocksMethod[method]
}

92
subscribe/proxy/trojan.go Normal file
View File

@ -0,0 +1,92 @@
package proxy
import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"net"
"net/url"
"strconv"
"strings"
)
type ProxyTrojan struct {
Tag string // 标签
Type string // 代理类型
//
Dialer option.DialerOptions
//
Address string // IP地址或域名
Port uint16 // 端口
Password string // 密码
SNI string // SNI
}
func (p *ProxyTrojan) GetTag() string {
return p.Tag
}
func (p *ProxyTrojan) GetType() string {
return C.TypeVMess
}
func (p *ProxyTrojan) ParseLink(link string) error {
link = strings.TrimPrefix(link, "trojan://")
u, err := url.Parse("http://" + link)
if err != nil {
return E.New("invalid trojan link: ", err.Error())
}
if u.User == nil || u.User.Username() == "" {
return E.New("invalid trojan link")
}
host, port, err := net.SplitHostPort(u.Host)
if err != nil {
host = u.Hostname()
port = "443"
}
portUint, err := strconv.ParseUint(port, 10, 16)
if err != nil || portUint == 0 || portUint > 65535 {
return E.New("invalid trojan link: ", err.Error())
}
p.Address = host
p.Port = uint16(portUint)
p.Password = u.User.Username()
p.SNI = u.Query().Get("sni")
if u.Fragment != "" {
p.Tag = u.Fragment
} else {
p.Tag = u.Host
}
p.Type = C.TypeTrojan
return nil
}
func (p *ProxyTrojan) SetDialer(dialer option.DialerOptions) {
p.Dialer = dialer
}
func (p *ProxyTrojan) GenerateOutboundOptions() (option.Outbound, error) {
out := option.Outbound{
Tag: p.Tag,
Type: C.TypeTrojan,
TrojanOptions: option.TrojanOutboundOptions{
ServerOptions: option.ServerOptions{
Server: p.Address,
ServerPort: p.Port,
},
Password: p.Password,
TLS: &option.OutboundTLSOptions{
Enabled: true,
ServerName: p.SNI,
},
},
}
out.TrojanOptions.DialerOptions = p.Dialer
return out, nil
}

269
subscribe/proxy/vmess.go Normal file
View File

@ -0,0 +1,269 @@
package proxy
import (
"encoding/json"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"net"
"strconv"
"strings"
)
type ProxyVMess struct {
Tag string // 标签
Type string // 代理类型
//
Dialer option.DialerOptions
//
Address string // IP地址或域名
Port uint16 // 端口
UUID string // UUID
AlterID uint16 // AlterID
Encrypt string // 加密方式
TLSEnable bool // 是否启用TLS
TLSSNI string // TLS SNI
TLSALPN []string // TLS ALPN
//
TransportMode string // 传输方式 TCP/KCP/WS/H2/QUIC/GRPC
//
TransportFakeType string // 伪装类型 none/http/srtp/utp/wechat-video
//
TransportTCPHost []string // TCP Host
//
TransportH2Host string // HTTP/2 Host
TransportH2Path string // HTTP/2 Path
//
TransportWSHost string // WebSocket Host
TransportWSEarlyDataHeader string // WebSocket Early-Data Header
TransportWSPath string // WebSocket Path
//
TransportGRPCServiceName string // gRPC Service Name
//
TransportQUICSecurity string // QUIC Security
TransportQUICKey string // QUIC Key
//
TransportKCPSeed string // KCP Seed
}
type configVMessJSON struct {
Version string `json:"v"`
Tag string `json:"ps"`
Address string `json:"add"`
Port string `json:"port"`
UUID string `json:"id"`
AlterID string `json:"aid"`
Security string `json:"scy"`
Network string `json:"net"`
Type string `json:"type"`
Host string `json:"host"`
Path string `json:"path"`
TLS string `json:"tls"`
SNI string `json:"sni"`
}
func (p *ProxyVMess) GetTag() string {
return p.Tag
}
func (p *ProxyVMess) GetType() string {
return C.TypeVMess
}
func (p *ProxyVMess) ParseLink(link string) error {
configStr := strings.TrimPrefix(link, "vmess://")
configDec, err := Base64Decode(configStr)
if err != nil {
return err
}
var j configVMessJSON
err = json.Unmarshal([]byte(configDec), &j)
if err != nil {
return err
}
p.Address = j.Address
portUint16, err := strconv.ParseUint(j.Port, 10, 16)
if err != nil {
return err
}
p.Port = uint16(portUint16)
p.UUID = j.UUID
if j.AlterID != "" {
alterIDInt, err := strconv.ParseInt(j.AlterID, 10, 16)
if err != nil {
return err
}
p.AlterID = uint16(alterIDInt)
}
if j.Security != "" {
p.Encrypt = j.Security
} else {
p.Encrypt = "auto"
}
switch j.Network {
case "tcp":
p.TransportMode = "TCP"
if j.Type != "" {
p.TransportFakeType = j.Type
}
if j.Host != "" {
p.TransportTCPHost = strings.Split(j.Host, ",")
}
case "kcp":
p.TransportMode = "KCP"
if j.Type != "" {
p.TransportFakeType = j.Type
}
if j.Path != "" {
p.TransportKCPSeed = j.Path
}
case "ws":
p.TransportMode = "WS"
if j.Host != "" {
p.TransportWSHost = j.Host
}
if j.Path != "" {
p.TransportWSPath = j.Path
}
case "h2":
p.TransportMode = "H2"
if j.Host != "" {
p.TransportH2Host = j.Host
}
if j.Path != "" {
p.TransportH2Path = j.Path
}
case "quic":
p.TransportMode = "QUIC"
if j.Type != "" {
p.TransportFakeType = j.Type
}
if j.Host != "" {
p.TransportQUICSecurity = j.Host
}
if j.Path != "" {
p.TransportQUICKey = j.Path
}
case "grpc":
p.TransportMode = "GRPC"
if j.Path != "" {
p.TransportGRPCServiceName = j.Path
}
default:
return E.New("unknown net: ", j.Network)
}
if j.TLS != "" {
p.TLSEnable = true
if j.SNI != "" {
p.TLSSNI = j.SNI
}
}
if j.Tag != "" {
p.Tag = j.Tag
} else {
p.Tag = net.JoinHostPort(p.Address, strconv.Itoa(int(p.Port)))
}
p.Type = C.TypeVMess
return nil
}
func (p *ProxyVMess) SetDialer(dialer option.DialerOptions) {
p.Dialer = dialer
}
func (p *ProxyVMess) GenerateOutboundOptions() (option.Outbound, error) {
if p.TransportMode == "KCP" {
return option.Outbound{}, E.New("vmess kcp not supported in sing-box")
}
if p.TransportFakeType != "" && p.TransportFakeType != "none" && p.TransportFakeType != "http" {
return option.Outbound{}, E.New("vmess fake type `%s` not supported in sing-box", p.TransportFakeType)
}
out := option.Outbound{
Tag: p.Tag,
Type: C.TypeVMess,
VMessOptions: option.VMessOutboundOptions{
ServerOptions: option.ServerOptions{
Server: p.Address,
ServerPort: p.Port,
},
UUID: p.UUID,
Security: p.Encrypt,
AlterId: int(p.AlterID),
},
}
switch p.TransportMode {
case "TCP":
if p.TransportTCPHost != nil {
out.VMessOptions.Transport = &option.V2RayTransportOptions{
Type: C.V2RayTransportTypeHTTP,
HTTPOptions: option.V2RayHTTPOptions{
Host: p.TransportTCPHost,
},
}
}
case "WS":
out.VMessOptions.Transport = &option.V2RayTransportOptions{
Type: C.V2RayTransportTypeWebsocket,
WebsocketOptions: option.V2RayWebsocketOptions{
Path: p.TransportWSPath,
Headers: map[string]option.Listable[string]{
"Host": {p.TransportWSHost},
},
},
}
case "H2":
out.VMessOptions.Transport = &option.V2RayTransportOptions{
Type: C.V2RayTransportTypeHTTP,
HTTPOptions: option.V2RayHTTPOptions{
Host: p.TransportTCPHost,
Path: p.TransportH2Path,
},
}
if !p.TLSEnable {
return option.Outbound{}, E.New("vmess h2 must enable tls")
}
case "QUIC":
out.VMessOptions.Transport = &option.V2RayTransportOptions{
Type: C.V2RayTransportTypeQUIC,
QUICOptions: option.V2RayQUICOptions{},
}
if !p.TLSEnable {
return option.Outbound{}, E.New("vmess quic must enable tls")
}
case "GRPC":
out.VMessOptions.Transport = &option.V2RayTransportOptions{
Type: C.V2RayTransportTypeGRPC,
GRPCOptions: option.V2RayGRPCOptions{
ServiceName: p.TransportGRPCServiceName,
},
}
}
if p.TLSEnable {
out.VMessOptions.TLS = &option.OutboundTLSOptions{
Enabled: true,
ServerName: p.TLSSNI,
ALPN: p.TLSALPN,
}
}
out.VMessOptions.DialerOptions = p.Dialer
return out, nil
}

301
subscribe/subscribe.go Normal file
View File

@ -0,0 +1,301 @@
//go:build with_subscribe
package subscribe
import (
"bytes"
"context"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/subscribe/proxy"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
"net"
"net/http"
"net/netip"
"net/url"
"os"
"time"
)
const requestTimeout = 20 * time.Second
func ParsePeer(ctx context.Context, tag string, options option.SubscribeOutboundOptions) ([]option.Outbound, error) {
content, err := requestWithCache(ctx, options)
if err != nil {
return nil, err
}
outboundRawOptions, err := proxy.ParsePeers(string(content))
if err != nil {
return nil, err
}
if options.Filter != nil {
newOutboundRawOptions := make([]proxy.Proxy, 0)
for _, outboundRawOption := range outboundRawOptions {
if filterMatch(options.Filter, outboundRawOption.GetTag()) {
newOutboundRawOptions = append(newOutboundRawOptions, outboundRawOption)
}
}
outboundRawOptions = newOutboundRawOptions
}
if options.DialerOptions != nil {
for i := range outboundRawOptions {
outboundRawOptions[i].SetDialer(*options.DialerOptions)
}
}
outboundOptions := make([]option.Outbound, 0)
for _, outboundRawOption := range outboundRawOptions {
outboundOption, err := outboundRawOption.GenerateOutboundOptions()
if err != nil {
return nil, err
}
outboundOptions = append(outboundOptions, outboundOption)
}
globalOptions := make([]option.Outbound, 0)
for _, o := range outboundOptions {
globalOptions = append(globalOptions, o)
}
if options.CustomGroup != nil && len(options.CustomGroup) > 0 {
groupOptions := make([]option.Outbound, 0)
for _, c := range options.CustomGroup {
if c.Tag == "" {
return nil, E.New("group tag cannot be empty")
}
groupOutTags := make([]string, 0)
groupOutTagMap := make(map[string]bool)
for _, o := range outboundOptions {
if filterMatch(c.Filter, o.Tag) {
groupOutTags = append(groupOutTags, o.Tag)
groupOutTagMap[o.Tag] = true
}
}
groupOut := option.Outbound{}
switch c.ProxyType {
case C.TypeSelector:
groupOut.Tag = c.Tag
groupOut.Type = C.TypeSelector
if c.SelectorOptions != nil {
groupOut.SelectorOptions = *c.SelectorOptions
if c.SelectorOptions.Default != "" {
if _, ok := groupOutTagMap[c.SelectorOptions.Default]; !ok {
return nil, E.New("default outbound '", c.SelectorOptions.Default, "' not found")
}
}
} else {
groupOut.SelectorOptions = option.SelectorOutboundOptions{}
}
groupOut.SelectorOptions.Outbounds = groupOutTags
case C.TypeURLTest:
groupOut.Tag = c.Tag
groupOut.Type = C.TypeURLTest
if c.URLTestOptions != nil {
groupOut.URLTestOptions = *c.URLTestOptions
} else {
groupOut.URLTestOptions = option.URLTestOutboundOptions{}
}
groupOut.URLTestOptions.Outbounds = groupOutTags
default:
return nil, E.New("unsupported proxy type: ", c.ProxyType)
}
groupOptions = append(groupOptions, groupOut)
}
globalOptions = append(globalOptions, groupOptions...)
}
globalTags := make([]string, 0)
globalTagMap := make(map[string]bool)
for _, g := range globalOptions {
globalTagMap[g.Tag] = true
globalTags = append(globalTags, g.Tag)
}
globalOut := option.Outbound{}
switch options.ProxyType {
case C.TypeSelector:
globalOut.Tag = tag
globalOut.Type = C.TypeSelector
if options.SelectorOptions != nil {
globalOut.SelectorOptions = *options.SelectorOptions
if options.SelectorOptions.Default != "" {
if _, ok := globalTagMap[options.SelectorOptions.Default]; !ok {
return nil, E.New("default outbound '", options.SelectorOptions.Default, "' not found")
}
}
} else {
globalOut.SelectorOptions = option.SelectorOutboundOptions{}
}
globalOut.SelectorOptions.Outbounds = globalTags
case C.TypeURLTest:
globalOut.Tag = tag
globalOut.Type = C.TypeURLTest
if options.URLTestOptions != nil {
globalOut.URLTestOptions = *options.URLTestOptions
} else {
globalOut.URLTestOptions = option.URLTestOutboundOptions{}
}
globalOut.URLTestOptions.Outbounds = globalTags
default:
return nil, E.New("unsupported proxy type: ", options.ProxyType)
}
globalOptions = append(globalOptions, globalOut)
return globalOptions, nil
}
func request(ctx context.Context, options option.SubscribeOutboundOptions) ([]byte, error) {
u, err := url.Parse(options.Url)
if err != nil {
return nil, err
}
if options.RequestDialerOptions == nil {
options.RequestDialerOptions = &option.RequestDialerOptions{}
}
dialer := NewDialer(*options.RequestDialerOptions)
host := u.Hostname()
ip, err := netip.ParseAddr(host)
if err != nil {
dns, err := NewDNS(ctx, options.DNS, dialer)
if err != nil {
return nil, err
}
ips, err := dns.QueryIP(host)
if err != nil {
return nil, err
}
ip, _ = netip.ParseAddr(ips[0])
}
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
req.RemoteAddr = net.JoinHostPort(ip.String(), u.Port())
client := &http.Client{
Transport: &http.Transport{
ForceAttemptHTTP2: true,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
},
},
}
ctx, cancel := context.WithTimeout(ctx, requestTimeout)
defer cancel()
req = req.WithContext(ctx)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
reader := bytes.NewBuffer(nil)
_, err = reader.ReadFrom(resp.Body)
if err != nil {
return nil, err
}
return reader.Bytes(), nil
}
func requestWithCache(ctx context.Context, options option.SubscribeOutboundOptions) ([]byte, error) {
var cache []byte
if options.CacheFile != "" {
f, err := os.OpenFile(options.CacheFile, os.O_RDONLY, 0666)
if err == nil {
fs, err := f.Stat()
if err == nil {
if fs.Size() > 0 {
readBuf := bytes.NewBuffer(nil)
_, err = readBuf.ReadFrom(f)
if err == nil {
cache = readBuf.Bytes()
if time.Now().Before(fs.ModTime().Add(time.Duration(options.ForceUpdateDuration))) {
f.Close()
return cache, nil
}
}
}
}
}
f.Close()
}
content, err := request(ctx, options)
if err != nil {
if cache != nil {
return cache, nil
}
return nil, err
}
if options.CacheFile != "" {
err = os.WriteFile(options.CacheFile, content, 0666)
if err != nil {
return nil, err
}
}
return content, nil
}
func RequestAndCache(ctx context.Context, options option.SubscribeOutboundOptions) error {
content, err := request(ctx, options)
if err != nil {
return err
}
return os.WriteFile(options.CacheFile, content, 0666)
}
func filterMatch(f *option.Filter, tag string) bool {
if f.Rule != nil && len(f.Rule) > 0 {
match := false
for _, r := range f.Rule {
if r.MatchString(tag) {
match = true
break
}
}
if f.WhiteMode {
if !match {
return false
}
} else {
if match {
return false
}
}
}
return true
}

View File

@ -0,0 +1,13 @@
//go:build !with_subscribe
package subscribe
import (
"context"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
func ParsePeer(ctx context.Context, tag string, options option.SubscribeOutboundOptions) ([]option.Outbound, error) {
return nil, E.New(`Subscribe is not included in this build, rebuild with -tags with_subscribe`)
}