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)
|
inbounds = append(inbounds, in)
|
||||||
}
|
}
|
||||||
for i, outboundOptions := range options.Outbounds {
|
for i, outboundOptions := range options.Outbounds {
|
||||||
var out adapter.Outbound
|
var outs []adapter.Outbound
|
||||||
var tag string
|
var tag string
|
||||||
if outboundOptions.Tag != "" {
|
if outboundOptions.Tag != "" {
|
||||||
tag = outboundOptions.Tag
|
tag = outboundOptions.Tag
|
||||||
} else {
|
} else {
|
||||||
tag = F.ToString(i)
|
tag = F.ToString(i)
|
||||||
}
|
}
|
||||||
out, err = outbound.New(
|
outs, err = outbound.NewGroup(
|
||||||
ctx,
|
ctx,
|
||||||
router,
|
router,
|
||||||
logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")),
|
logFactory,
|
||||||
tag,
|
tag,
|
||||||
outboundOptions)
|
outboundOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "parse outbound[", i, "]")
|
return nil, E.Cause(err, "parse outbound[", i, "]")
|
||||||
}
|
}
|
||||||
outbounds = append(outbounds, out)
|
outbounds = append(outbounds, outs...)
|
||||||
}
|
}
|
||||||
err = router.Initialize(inbounds, outbounds, func() adapter.Outbound {
|
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"})
|
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) {
|
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
|
||||||
if !address.IsValid() {
|
if !address.IsValid() {
|
||||||
return nil, E.New("invalid address")
|
return nil, E.New("invalid address")
|
||||||
|
@ -27,3 +27,5 @@ const (
|
|||||||
TypeSelector = "selector"
|
TypeSelector = "selector"
|
||||||
TypeURLTest = "urltest"
|
TypeURLTest = "urltest"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const TypeSubscribe = "subscribe"
|
||||||
|
@ -25,6 +25,7 @@ type _Outbound struct {
|
|||||||
VLESSOptions VLESSOutboundOptions `json:"-"`
|
VLESSOptions VLESSOutboundOptions `json:"-"`
|
||||||
SelectorOptions SelectorOutboundOptions `json:"-"`
|
SelectorOptions SelectorOutboundOptions `json:"-"`
|
||||||
URLTestOptions URLTestOutboundOptions `json:"-"`
|
URLTestOptions URLTestOutboundOptions `json:"-"`
|
||||||
|
SubscribeOptions SubscribeOutboundOptions `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Outbound _Outbound
|
type Outbound _Outbound
|
||||||
@ -64,6 +65,8 @@ func (h Outbound) MarshalJSON() ([]byte, error) {
|
|||||||
v = h.SelectorOptions
|
v = h.SelectorOptions
|
||||||
case C.TypeURLTest:
|
case C.TypeURLTest:
|
||||||
v = h.URLTestOptions
|
v = h.URLTestOptions
|
||||||
|
case C.TypeSubscribe:
|
||||||
|
v = h.SubscribeOptions
|
||||||
default:
|
default:
|
||||||
return nil, E.New("unknown outbound type: ", h.Type)
|
return nil, E.New("unknown outbound type: ", h.Type)
|
||||||
}
|
}
|
||||||
@ -109,6 +112,8 @@ func (h *Outbound) UnmarshalJSON(bytes []byte) error {
|
|||||||
v = &h.SelectorOptions
|
v = &h.SelectorOptions
|
||||||
case C.TypeURLTest:
|
case C.TypeURLTest:
|
||||||
v = &h.URLTestOptions
|
v = &h.URLTestOptions
|
||||||
|
case C.TypeSubscribe:
|
||||||
|
v = &h.SubscribeOptions
|
||||||
default:
|
default:
|
||||||
return E.New("unknown outbound type: ", h.Type)
|
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")
|
return strings.Split(string(v), "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
type Listable[T comparable] []T
|
type Listable[T any] []T
|
||||||
|
|
||||||
func (l Listable[T]) MarshalJSON() ([]byte, error) {
|
func (l Listable[T]) MarshalJSON() ([]byte, error) {
|
||||||
arrayList := []T(l)
|
arrayList := []T(l)
|
||||||
|
@ -2,6 +2,7 @@ package outbound
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
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) {
|
func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Outbound) (adapter.Outbound, error) {
|
||||||
|
/**
|
||||||
var metadata *adapter.InboundContext
|
var metadata *adapter.InboundContext
|
||||||
if tag != "" {
|
if tag != "" {
|
||||||
ctx, metadata = adapter.AppendContext(ctx)
|
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")
|
return nil, E.New("missing outbound type")
|
||||||
}
|
}
|
||||||
ctx = ContextWithTag(ctx, tag)
|
ctx = ContextWithTag(ctx, tag)
|
||||||
|
*/
|
||||||
switch options.Type {
|
switch options.Type {
|
||||||
case C.TypeDirect:
|
case C.TypeDirect:
|
||||||
return NewDirect(router, logger, tag, options.DirectOptions)
|
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)
|
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