mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-13 21:54:13 +08:00
add subscribe support
This commit is contained in:
parent
926b622710
commit
6a5962be57
8
box.go
8
box.go
@ -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"})
|
||||
|
106
cmd/sing-box/cmd_show_subscribe_peer.go
Normal file
106
cmd/sing-box/cmd_show_subscribe_peer.go
Normal 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
|
||||
}
|
91
cmd/sing-box/cmd_update_subscribe.go
Normal file
91
cmd/sing-box/cmd_update_subscribe.go
Normal 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
|
||||
}
|
@ -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")
|
||||
|
@ -27,3 +27,5 @@ const (
|
||||
TypeSelector = "selector"
|
||||
TypeURLTest = "urltest"
|
||||
)
|
||||
|
||||
const TypeSubscribe = "subscribe"
|
||||
|
@ -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
83
option/subscribe.go
Normal 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:"-"`
|
||||
}
|
@ -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)
|
||||
|
@ -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
37
outbound/subscribe.go
Normal 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
23
subscribe/dialer.go
Normal 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
320
subscribe/dns.go
Normal 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
97
subscribe/proxy/http.go
Normal 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
162
subscribe/proxy/hysteria.go
Normal 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
93
subscribe/proxy/proxy.go
Normal 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
|
||||
}
|
181
subscribe/proxy/shadowsocks.go
Normal file
181
subscribe/proxy/shadowsocks.go
Normal 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
|
||||
}
|
143
subscribe/proxy/shadowsocksr.go
Normal file
143
subscribe/proxy/shadowsocksr.go
Normal 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
92
subscribe/proxy/socks.go
Normal 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
44
subscribe/proxy/tools.go
Normal 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
92
subscribe/proxy/trojan.go
Normal 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
269
subscribe/proxy/vmess.go
Normal 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
301
subscribe/subscribe.go
Normal 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
|
||||
}
|
13
subscribe/subscribe_stub.go
Normal file
13
subscribe/subscribe_stub.go
Normal 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`)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user