fix: outbound

This commit is contained in:
mzz2017 2024-06-16 05:07:00 +08:00
parent 5de32db41b
commit 07d48c7732
38 changed files with 141 additions and 2577 deletions

View File

@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2022-2023, daeuniverse Organization <dae@v2raya.org>
* Copyright (c) 2022-2024, daeuniverse Organization <dae@v2raya.org>
*/
package dialer

View File

@ -1,3 +1,8 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2022-2024, daeuniverse Organization <dae@v2raya.org>
*/
package dialer
import (

View File

@ -1,50 +1,19 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2022-2023, daeuniverse Organization <dae@v2raya.org>
* Copyright (c) 2022-2024, daeuniverse Organization <dae@v2raya.org>
*/
package dialer
import (
"fmt"
"net"
D "github.com/daeuniverse/outbound/dialer"
"github.com/daeuniverse/outbound/netproxy"
)
type blockDialer struct {
DialCallback func()
}
func (d *blockDialer) Dial(network, addr string) (c netproxy.Conn, err error) {
magicNetwork, err := netproxy.ParseMagicNetwork(network)
if err != nil {
return nil, err
}
switch magicNetwork.Network {
case "tcp":
return d.DialTcp(addr)
case "udp":
return d.DialUdp(addr)
default:
return nil, fmt.Errorf("%w: %v", netproxy.UnsupportedTunnelTypeError, network)
}
}
func (d *blockDialer) DialTcp(addr string) (c netproxy.Conn, err error) {
d.DialCallback()
return nil, net.ErrClosed
}
func (d *blockDialer) DialUdp(addr string) (c netproxy.PacketConn, err error) {
d.DialCallback()
return nil, net.ErrClosed
}
func NewBlockDialer(option *GlobalOption, dialCallback func()) (netproxy.Dialer, *Property) {
return &blockDialer{DialCallback: dialCallback}, &Property{
Name: "block",
Address: "",
Protocol: "",
Link: "",
d, _p := D.NewBlockDialer(&option.ExtraOption, dialCallback)
return d, &Property{
Property: *_p,
SubscriptionTag: "",
}
}

View File

@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2022-2023, daeuniverse Organization <dae@v2raya.org>
* Copyright (c) 2022-2024, daeuniverse Organization <dae@v2raya.org>
*/
package dialer
@ -272,10 +272,10 @@ type CheckOption struct {
func (d *Dialer) ActivateCheck() {
d.tickerMu.Lock()
defer d.tickerMu.Unlock()
if d.InstanceOption.CheckEnabled {
if d.InstanceOption.DisableCheck || d.checkActivated {
return
}
d.InstanceOption.CheckEnabled = true
d.checkActivated = true
go d.aliveBackground()
}

View File

@ -1,3 +1,8 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2022-2024, daeuniverse Organization <dae@v2raya.org>
*/
package dialer
import (
@ -6,6 +11,7 @@ import (
"sync"
"time"
D "github.com/daeuniverse/outbound/dialer"
"github.com/daeuniverse/outbound/netproxy"
"github.com/sirupsen/logrus"
)
@ -29,29 +35,26 @@ type Dialer struct {
checkCh chan time.Time
ctx context.Context
cancel context.CancelFunc
checkActivated bool
}
type GlobalOption struct {
D.ExtraOption
Log *logrus.Logger
TcpCheckOptionRaw TcpCheckOptionRaw // Lazy parse
CheckDnsOptionRaw CheckDnsOptionRaw // Lazy parse
CheckInterval time.Duration
CheckTolerance time.Duration
CheckDnsTcp bool
AllowInsecure bool
TlsImplementation string
UtlsImitate string
}
type InstanceOption struct {
CheckEnabled bool
DisableCheck bool
}
type Property struct {
Name string
Address string
Protocol string
Link string
D.Property
SubscriptionTag string
}
@ -77,9 +80,6 @@ func NewDialer(dialer netproxy.Dialer, option *GlobalOption, iOption InstanceOpt
ctx: ctx,
cancel: cancel,
}
if iOption.CheckEnabled {
go d.aliveBackground()
}
return d
}

View File

@ -1,20 +1,19 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2022-2024, daeuniverse Organization <dae@v2raya.org>
*/
package dialer
import (
D "github.com/daeuniverse/outbound/dialer"
"github.com/daeuniverse/outbound/netproxy"
softwindDirect "github.com/daeuniverse/outbound/protocol/direct"
)
func NewDirectDialer(option *GlobalOption, fullcone bool) (netproxy.Dialer, *Property) {
property := &Property{
Name: "direct",
Address: "",
Protocol: "",
Link: "",
}
if fullcone {
return softwindDirect.FullconeDirect, property
} else {
return softwindDirect.SymmetricDirect, property
d, _p := D.NewDirectDialer(&option.ExtraOption, fullcone)
return d, &Property{
Property: *_p,
SubscriptionTag: "",
}
}

View File

@ -1,115 +0,0 @@
package http
import (
"fmt"
"net"
"net/url"
"strconv"
"github.com/daeuniverse/dae/common"
"github.com/daeuniverse/dae/component/outbound/dialer"
"github.com/daeuniverse/outbound/netproxy"
"github.com/daeuniverse/outbound/protocol/http"
)
func init() {
dialer.FromLinkRegister("http", NewHTTP)
dialer.FromLinkRegister("https", NewHTTP)
}
type HTTP struct {
Name string `json:"name"`
Server string `json:"server"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
SNI string `json:"sni"`
Protocol string `json:"protocol"`
AllowInsecure bool `json:"allowInsecure"`
}
func NewHTTP(option *dialer.GlobalOption, nextDialer netproxy.Dialer, link string) (netproxy.Dialer, *dialer.Property, error) {
s, err := ParseHTTPURL(link)
if err != nil {
return nil, nil, fmt.Errorf("%w: %v", dialer.InvalidParameterErr, err)
}
return s.Dialer(option, nextDialer)
}
func ParseHTTPURL(link string) (data *HTTP, err error) {
u, err := url.Parse(link)
if err != nil || (u.Scheme != "http" && u.Scheme != "https") {
return nil, fmt.Errorf("%w: %v", dialer.InvalidParameterErr, err)
}
pwd, _ := u.User.Password()
strPort := u.Port()
if strPort == "" {
if u.Scheme == "http" {
strPort = "80"
} else if u.Scheme == "https" {
strPort = "443"
}
}
port, err := strconv.Atoi(strPort)
if err != nil {
return nil, fmt.Errorf("error when parsing port: %w", err)
}
allowInsecure, _ := strconv.ParseBool(u.Query().Get("allowInsecure"))
if !allowInsecure {
allowInsecure, _ = strconv.ParseBool(u.Query().Get("allow_insecure"))
}
if !allowInsecure {
allowInsecure, _ = strconv.ParseBool(u.Query().Get("allowinsecure"))
}
if !allowInsecure {
allowInsecure, _ = strconv.ParseBool(u.Query().Get("skipVerify"))
}
return &HTTP{
Name: u.Fragment,
Server: u.Hostname(),
Port: port,
Username: u.User.Username(),
Password: pwd,
SNI: u.Query().Get("sni"),
Protocol: u.Scheme,
AllowInsecure: allowInsecure,
}, nil
}
func (s *HTTP) Dialer(option *dialer.GlobalOption, nextDialer netproxy.Dialer) (netproxy.Dialer, *dialer.Property, error) {
u := s.URL()
d, err := http.NewHTTPProxy(&u, nextDialer)
if err != nil {
return nil, nil, err
}
return d, &dialer.Property{
Name: s.Name,
Address: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
Protocol: s.Protocol,
Link: u.String(),
}, nil
}
func (s *HTTP) URL() url.URL {
u := url.URL{
Scheme: s.Protocol,
Host: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
Fragment: s.Name,
}
if s.SNI != "" {
u.RawQuery = url.Values{"sni": []string{s.SNI}, "allowInsecure": []string{common.BoolToString(s.AllowInsecure)}}.Encode()
}
if s.Username != "" {
if s.Password != "" {
u.User = url.UserPassword(s.Username, s.Password)
} else {
u.User = url.User(s.Username)
}
}
return u
}
func (s *HTTP) ExportToURL() string {
u := s.URL()
return u.String()
}

View File

@ -1,153 +0,0 @@
package juicity
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"fmt"
"net"
"net/url"
"strconv"
"github.com/daeuniverse/dae/common"
"github.com/daeuniverse/dae/component/outbound/dialer"
"github.com/daeuniverse/outbound/netproxy"
"github.com/daeuniverse/outbound/protocol"
)
func init() {
dialer.FromLinkRegister("juicity", NewJuicity)
}
type Juicity struct {
Name string
Server string
Port int
User string
Password string
Sni string
AllowInsecure bool
CongestionControl string
PinnedCertchainSha256 string
Protocol string
}
func NewJuicity(option *dialer.GlobalOption, nextDialer netproxy.Dialer, link string) (netproxy.Dialer, *dialer.Property, error) {
s, err := ParseJuicityURL(link)
if err != nil {
return nil, nil, err
}
return s.Dialer(option, nextDialer)
}
func (s *Juicity) Dialer(option *dialer.GlobalOption, nextDialer netproxy.Dialer) (netproxy.Dialer, *dialer.Property, error) {
d := nextDialer
var err error
var flags protocol.Flags
tlsConfig := &tls.Config{
NextProtos: []string{"h3"},
MinVersion: tls.VersionTLS13,
ServerName: s.Sni,
InsecureSkipVerify: s.AllowInsecure || option.AllowInsecure,
}
if s.PinnedCertchainSha256 != "" {
pinnedHash, err := base64.URLEncoding.DecodeString(s.PinnedCertchainSha256)
if err != nil {
pinnedHash, err = base64.StdEncoding.DecodeString(s.PinnedCertchainSha256)
if err != nil {
pinnedHash, err = hex.DecodeString(s.PinnedCertchainSha256)
if err != nil {
return nil, nil, fmt.Errorf("failed to decode PinnedCertchainSha256")
}
}
}
tlsConfig.InsecureSkipVerify = true
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
if !bytes.Equal(common.GenerateCertChainHash(rawCerts), pinnedHash) {
return fmt.Errorf("pinned hash of cert chain does not match")
}
return nil
}
}
if d, err = protocol.NewDialer("juicity", d, protocol.Header{
ProxyAddress: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
Feature1: s.CongestionControl,
TlsConfig: tlsConfig,
User: s.User,
Password: s.Password,
IsClient: true,
Flags: flags,
}); err != nil {
return nil, nil, err
}
return d, &dialer.Property{
Name: s.Name,
Address: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
Protocol: s.Protocol,
Link: s.ExportToURL(),
}, nil
}
func ParseJuicityURL(u string) (data *Juicity, err error) {
//trojan://password@server:port#escape(remarks)
t, err := url.Parse(u)
if err != nil {
err = fmt.Errorf("invalid trojan format")
return
}
allowInsecure, _ := strconv.ParseBool(t.Query().Get("allowInsecure"))
if !allowInsecure {
allowInsecure, _ = strconv.ParseBool(t.Query().Get("allow_insecure"))
}
if !allowInsecure {
allowInsecure, _ = strconv.ParseBool(t.Query().Get("allowinsecure"))
}
if !allowInsecure {
allowInsecure, _ = strconv.ParseBool(t.Query().Get("skipVerify"))
}
sni := t.Query().Get("peer")
if sni == "" {
sni = t.Query().Get("sni")
}
if sni == "" {
sni = t.Hostname()
}
port, err := strconv.Atoi(t.Port())
if err != nil {
return nil, dialer.InvalidParameterErr
}
password, _ := t.User.Password()
data = &Juicity{
Name: t.Fragment,
Server: t.Hostname(),
Port: port,
User: t.User.Username(),
Password: password,
Sni: sni,
AllowInsecure: allowInsecure,
CongestionControl: t.Query().Get("congestion_control"),
PinnedCertchainSha256: t.Query().Get("pinned_certchain_sha256"),
Protocol: "juicity",
}
return data, nil
}
func (t *Juicity) ExportToURL() string {
u := &url.URL{
Scheme: "juicity",
User: url.UserPassword(t.User, t.Password),
Host: net.JoinHostPort(t.Server, strconv.Itoa(t.Port)),
Fragment: t.Name,
}
q := u.Query()
if t.AllowInsecure {
q.Set("allow_insecure", "1")
}
common.SetValue(&q, "sni", t.Sni)
common.SetValue(&q, "congestion_control", t.CongestionControl)
common.SetValue(&q, "pinned_certchain_sha256", t.PinnedCertchainSha256)
u.RawQuery = q.Encode()
return u.String()
}

View File

@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2022-2023, daeuniverse Organization <dae@v2raya.org>
* Copyright (c) 2022-2024, daeuniverse Organization <dae@v2raya.org>
*/
package dialer

View File

@ -1,74 +1,23 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2022-2023, daeuniverse Organization <dae@v2raya.org>
* Copyright (c) 2022-2024, daeuniverse Organization <dae@v2raya.org>
*/
package dialer
import (
"fmt"
"net/url"
"strings"
"github.com/daeuniverse/dae/common"
"github.com/daeuniverse/outbound/netproxy"
D "github.com/daeuniverse/outbound/dialer"
"github.com/daeuniverse/outbound/protocol/direct"
)
type FromLinkCreator func(gOption *GlobalOption, nextDialer netproxy.Dialer, link string) (dialer netproxy.Dialer, property *Property, err error)
var fromLinkCreators = make(map[string]FromLinkCreator)
func FromLinkRegister(name string, creator FromLinkCreator) {
fromLinkCreators[name] = creator
}
func NewFromLink(gOption *GlobalOption, iOption InstanceOption, link string, subscriptionTag string) (*Dialer, error) {
/// Get overwritten name.
overwrittenName, linklike := common.GetTagFromLinkLikePlaintext(link)
links := strings.Split(linklike, "->")
d := direct.SymmetricDirect
p := &Property{
Name: "",
Address: "",
Protocol: "",
Link: link,
d, _p, err := D.NewNetproxyDialerFromLink(direct.SymmetricDirect, &gOption.ExtraOption, link)
if err != nil {
return nil, err
}
p := Property{
Property: *_p,
SubscriptionTag: subscriptionTag,
}
for i := len(links) - 1; i >= 0; i-- {
link := strings.TrimSpace(links[i])
u, err := url.Parse(link)
if err != nil {
return nil, err
}
creator, ok := fromLinkCreators[u.Scheme]
if !ok {
return nil, fmt.Errorf("unexpected link type: %v", u.Scheme)
}
var _property *Property
d, _property, err = creator(gOption, d, link)
if err != nil {
return nil, fmt.Errorf("create %v: %w", link, err)
}
if p.Name == "" {
p.Name = _property.Name
} else {
p.Name = _property.Name + "->" + p.Name
}
if p.Protocol == "" {
p.Protocol = _property.Protocol
} else {
p.Protocol = _property.Protocol + "->" + p.Protocol
}
if p.Address == "" {
p.Address = _property.Address
} else {
p.Address = _property.Address + "->" + p.Address
}
}
if overwrittenName != "" {
p.Name = overwrittenName
}
node := NewDialer(d, gOption, iOption, p)
return node, nil
return NewDialer(d, gOption, iOption, &p), nil
}

View File

@ -1,250 +0,0 @@
package shadowsocks
import (
"encoding/base64"
"fmt"
"net"
"net/url"
"strconv"
"strings"
"github.com/daeuniverse/dae/common"
"github.com/daeuniverse/dae/component/outbound/dialer"
"github.com/daeuniverse/dae/component/outbound/transport/simpleobfs"
"github.com/daeuniverse/outbound/netproxy"
"github.com/daeuniverse/outbound/protocol"
"github.com/daeuniverse/outbound/protocol/shadowsocks"
)
func init() {
// Use random salt by default to decrease the boot time
shadowsocks.DefaultSaltGeneratorType = shadowsocks.RandomSaltGeneratorType
dialer.FromLinkRegister("shadowsocks", NewShadowsocksFromLink)
dialer.FromLinkRegister("ss", NewShadowsocksFromLink)
}
type Shadowsocks struct {
Name string `json:"name"`
Server string `json:"server"`
Port int `json:"port"`
Password string `json:"password"`
Cipher string `json:"cipher"`
Plugin Sip003 `json:"plugin"`
UDP bool `json:"udp"`
Protocol string `json:"protocol"`
}
func NewShadowsocksFromLink(option *dialer.GlobalOption, nextDialer netproxy.Dialer, link string) (npd netproxy.Dialer, property *dialer.Property, err error) {
s, err := ParseSSURL(link)
if err != nil {
return nil, nil, err
}
return s.Dialer(option, nextDialer)
}
func (s *Shadowsocks) Dialer(option *dialer.GlobalOption, nextDialer netproxy.Dialer) (netproxy.Dialer, *dialer.Property, error) {
var err error
d := nextDialer
switch s.Plugin.Name {
case "simple-obfs":
switch s.Plugin.Opts.Obfs {
case "http", "tls":
default:
return nil, nil, fmt.Errorf("unsupported obfs %v of plugin %v", s.Plugin.Opts.Obfs, s.Plugin.Name)
}
host := s.Plugin.Opts.Host
if host == "" {
host = "cloudflare.com"
}
path := s.Plugin.Opts.Path
uSimpleObfs := url.URL{
Scheme: "simple-obfs",
Host: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
RawQuery: url.Values{
"obfs": []string{s.Plugin.Opts.Obfs},
"host": []string{host},
"uri": []string{path},
}.Encode(),
}
d, _, err = simpleobfs.NewSimpleObfs(option, d, uSimpleObfs.String())
if err != nil {
return nil, nil, err
}
default:
}
var nextDialerName string
switch s.Cipher {
case "aes-256-gcm", "aes-128-gcm", "chacha20-poly1305", "chacha20-ietf-poly1305":
nextDialerName = "shadowsocks"
case "aes-128-cfb", "aes-192-cfb", "aes-256-cfb", "aes-128-ctr", "aes-192-ctr", "aes-256-ctr", "aes-128-ofb", "aes-192-ofb", "aes-256-ofb", "des-cfb", "bf-cfb", "cast5-cfb", "rc4-md5", "rc4-md5-6", "chacha20", "chacha20-ietf", "salsa20", "camellia-128-cfb", "camellia-192-cfb", "camellia-256-cfb", "idea-cfb", "rc2-cfb", "seed-cfb", "rc4", "none", "plain":
nextDialerName = "shadowsocks_stream"
default:
return nil, nil, fmt.Errorf("unsupported shadowsocks encryption method: %v", s.Cipher)
}
d, err = protocol.NewDialer(nextDialerName, d, protocol.Header{
ProxyAddress: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
Cipher: s.Cipher,
Password: s.Password,
IsClient: true,
})
if err != nil {
return nil, nil, err
}
return d, &dialer.Property{
Name: s.Name,
Address: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
Protocol: s.Protocol,
Link: s.ExportToURL(),
}, nil
}
func ParseSSURL(u string) (data *Shadowsocks, err error) {
// parse attempts to parse ss:// links
parse := func(content string) (v *Shadowsocks, ok bool) {
// try to parse in the format of ss://BASE64(method:password)@server:port/?plugin=xxxx#name
u, err := url.Parse(content)
if err != nil {
return nil, false
}
username := u.User.String()
username, _ = common.Base64UrlDecode(username)
arr := strings.SplitN(username, ":", 2)
if len(arr) != 2 {
return nil, false
}
cipher := arr[0]
password := arr[1]
var sip003 Sip003
plugin := u.Query().Get("plugin")
if len(plugin) > 0 {
sip003 = ParseSip003(plugin)
}
port, err := strconv.Atoi(u.Port())
if err != nil {
return nil, false
}
return &Shadowsocks{
Cipher: strings.ToLower(cipher),
Password: password,
Server: u.Hostname(),
Port: port,
Name: u.Fragment,
Plugin: sip003,
UDP: sip003.Name == "",
Protocol: "shadowsocks",
}, true
}
var (
v *Shadowsocks
ok bool
)
content := u
// try to parse the ss:// link, if it fails, base64 decode first
if v, ok = parse(content); !ok {
// 进行base64解码并unmarshal到VmessInfo上
t := content[5:]
var l, r string
if ind := strings.Index(t, "#"); ind > -1 {
l = t[:ind]
r = t[ind+1:]
} else {
l = t
}
l, err = common.Base64StdDecode(l)
if err != nil {
l, err = common.Base64UrlDecode(l)
if err != nil {
return
}
}
t = "ss://" + l
if len(r) > 0 {
t += "#" + r
}
v, ok = parse(t)
}
if !ok {
return nil, fmt.Errorf("%w: unrecognized ss address", dialer.InvalidParameterErr)
}
return v, nil
}
type Sip003 struct {
Name string `json:"name"`
Opts Sip003Opts `json:"opts"`
}
type Sip003Opts struct {
Tls string `json:"tls"` // for v2ray-plugin
Obfs string `json:"obfs"`
Host string `json:"host"`
Path string `json:"uri"`
}
func ParseSip003Opts(opts string) Sip003Opts {
var sip003Opts Sip003Opts
fields := strings.Split(opts, ";")
for i := range fields {
a := strings.Split(fields[i], "=")
if len(a) == 1 {
// to avoid panic
a = append(a, "")
}
switch a[0] {
case "tls":
sip003Opts.Tls = "tls"
case "obfs", "mode":
sip003Opts.Obfs = a[1]
case "obfs-path", "obfs-uri", "path":
if !strings.HasPrefix(a[1], "/") {
a[1] += "/"
}
sip003Opts.Path = a[1]
case "obfs-host", "host":
sip003Opts.Host = a[1]
}
}
return sip003Opts
}
func ParseSip003(plugin string) Sip003 {
var sip003 Sip003
fields := strings.SplitN(plugin, ";", 2)
switch fields[0] {
case "obfs-local", "simpleobfs":
sip003.Name = "simple-obfs"
default:
sip003.Name = fields[0]
}
sip003.Opts = ParseSip003Opts(fields[1])
return sip003
}
func (s *Sip003) String() string {
list := []string{s.Name}
if s.Opts.Obfs != "" {
list = append(list, "obfs="+s.Opts.Obfs)
}
if s.Opts.Host != "" {
list = append(list, "obfs-host="+s.Opts.Host)
}
if s.Opts.Path != "" {
list = append(list, "obfs-uri="+s.Opts.Path)
}
return strings.Join(list, ";")
}
func (s *Shadowsocks) ExportToURL() string {
// sip002
u := &url.URL{
Scheme: "ss",
User: url.User(strings.TrimSuffix(base64.URLEncoding.EncodeToString([]byte(s.Cipher+":"+s.Password)), "=")),
Host: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
Fragment: s.Name,
}
if s.Plugin.Name != "" {
q := u.Query()
q.Set("plugin", s.Plugin.String())
u.RawQuery = q.Encode()
}
return u.String()
}

View File

@ -1,167 +0,0 @@
package shadowsocksr
import (
"encoding/base64"
"fmt"
"net"
"net/url"
"strconv"
"strings"
"github.com/daeuniverse/dae/common"
"github.com/daeuniverse/dae/component/outbound/dialer"
"github.com/daeuniverse/outbound/netproxy"
"github.com/daeuniverse/outbound/protocol"
"github.com/daeuniverse/outbound/protocol/shadowsocks_stream"
"github.com/daeuniverse/outbound/transport/shadowsocksr/obfs"
"github.com/daeuniverse/outbound/transport/shadowsocksr/proto"
)
func init() {
dialer.FromLinkRegister("shadowsocksr", NewShadowsocksR)
dialer.FromLinkRegister("ssr", NewShadowsocksR)
}
type ShadowsocksR struct {
Name string `json:"name"`
Server string `json:"server"`
Port int `json:"port"`
Password string `json:"password"`
Cipher string `json:"cipher"`
Proto string `json:"proto"`
ProtoParam string `json:"protoParam"`
Obfs string `json:"obfs"`
ObfsParam string `json:"obfsParam"`
Protocol string `json:"protocol"`
}
func NewShadowsocksR(option *dialer.GlobalOption, nextDialer netproxy.Dialer, link string) (netproxy.Dialer, *dialer.Property, error) {
s, err := ParseSSRURL(link)
if err != nil {
return nil, nil, err
}
return s.Dialer(option, nextDialer)
}
func (s *ShadowsocksR) Dialer(option *dialer.GlobalOption, nextDialer netproxy.Dialer) (netproxy.Dialer, *dialer.Property, error) {
d := nextDialer
obfsDialer, err := obfs.NewDialer(d, &obfs.ObfsParam{
ObfsHost: s.Server,
ObfsPort: uint16(s.Port),
Obfs: s.Obfs,
ObfsParam: s.ObfsParam,
})
if err != nil {
return nil, nil, err
}
d = obfsDialer
d, err = shadowsocks_stream.NewDialer(d, protocol.Header{
ProxyAddress: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
Cipher: s.Cipher,
Password: s.Password,
IsClient: true,
})
if err != nil {
return nil, nil, err
}
d = &proto.Dialer{
NextDialer: d,
Protocol: s.Proto,
ProtocolParam: s.ProtoParam,
ObfsOverhead: obfsDialer.ObfsOverhead(),
}
return d, &dialer.Property{
Name: s.Name,
Address: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
Protocol: s.Protocol,
Link: s.ExportToURL(),
}, nil
}
func ParseSSRURL(u string) (data *ShadowsocksR, err error) {
// parse attempts to parse ss:// links
parse := func(content string) (v ShadowsocksR, ok bool) {
arr := strings.Split(content, "/?")
if strings.Contains(content, ":") && len(arr) < 2 {
content += "/?remarks=&protoparam=&obfsparam="
arr = strings.Split(content, "/?")
} else if len(arr) != 2 {
return v, false
}
pre := strings.Split(arr[0], ":")
if len(pre) > 6 {
//if the length is more than 6, it means that the host contains the characters:,
//re-merge the first few groups into the host
pre[len(pre)-6] = strings.Join(pre[:len(pre)-5], ":")
pre = pre[len(pre)-6:]
} else if len(pre) < 6 {
return v, false
}
q, err := url.ParseQuery(arr[1])
if err != nil {
return v, false
}
pswd, _ := common.Base64UrlDecode(pre[5])
add, _ := common.Base64UrlDecode(pre[0])
remarks, _ := common.Base64UrlDecode(q.Get("remarks"))
protoparam, _ := common.Base64UrlDecode(q.Get("protoparam"))
obfsparam, _ := common.Base64UrlDecode(q.Get("obfsparam"))
port, err := strconv.Atoi(pre[1])
if err != nil {
return v, false
}
v = ShadowsocksR{
Name: remarks,
Server: add,
Port: port,
Password: pswd,
Cipher: pre[3],
Proto: pre[2],
ProtoParam: protoparam,
Obfs: pre[4],
ObfsParam: obfsparam,
Protocol: "shadowsocksr",
}
return v, true
}
content := u[6:]
var (
info ShadowsocksR
ok bool
)
// try parsing the ssr:// link, if it fails, base64 decode first
if info, ok = parse(content); !ok {
// perform base64 decoding and parse again
content, err = common.Base64StdDecode(content)
if err != nil {
content, err = common.Base64UrlDecode(content)
if err != nil {
return
}
}
info, ok = parse(content)
}
if !ok {
err = fmt.Errorf("%w: unrecognized ssr address", dialer.InvalidParameterErr)
return
}
return &info, nil
}
func (s *ShadowsocksR) ExportToURL() string {
/* ssr://server:port:proto:method:obfs:URLBASE64(password)/?remarks=URLBASE64(remarks)&protoparam=URLBASE64(protoparam)&obfsparam=URLBASE64(obfsparam)) */
return fmt.Sprintf("ssr://%v", strings.TrimSuffix(base64.URLEncoding.EncodeToString([]byte(
fmt.Sprintf(
"%v:%v:%v:%v:%v/?remarks=%v&protoparam=%v&obfsparam=%v",
net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
s.Proto,
s.Cipher,
s.Obfs,
base64.URLEncoding.EncodeToString([]byte(s.Password)),
base64.URLEncoding.EncodeToString([]byte(s.Name)),
base64.URLEncoding.EncodeToString([]byte(s.ProtoParam)),
base64.URLEncoding.EncodeToString([]byte(s.ObfsParam)),
),
)), "="))
}

View File

@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2022-2023, daeuniverse Organization <dae@v2raya.org>
* Copyright (c) 2022-2024, daeuniverse Organization <dae@v2raya.org>
*/
package dialer

View File

@ -1,104 +0,0 @@
package socks
import (
"fmt"
"net"
"net/url"
"strconv"
"github.com/daeuniverse/dae/component/outbound/dialer"
"github.com/daeuniverse/outbound/netproxy"
"github.com/daeuniverse/outbound/protocol/socks5"
)
func init() {
dialer.FromLinkRegister("socks", NewSocks) // socks -> socks5
//dialer.FromLinkRegister("socks4", NewSocks)
//dialer.FromLinkRegister("socks4a", NewSocks)
dialer.FromLinkRegister("socks5", NewSocks)
}
type Socks struct {
Name string `json:"name"`
Server string `json:"server"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
Protocol string `json:"protocol"`
}
func NewSocks(option *dialer.GlobalOption, nextDialer netproxy.Dialer, link string) (netproxy.Dialer, *dialer.Property, error) {
s, err := ParseSocksURL(link)
if err != nil {
return nil, nil, dialer.InvalidParameterErr
}
return s.Dialer(option, nextDialer)
}
func (s *Socks) Dialer(option *dialer.GlobalOption, nextDialer netproxy.Dialer) (netproxy.Dialer, *dialer.Property, error) {
link := s.ExportToURL()
d := nextDialer
switch s.Protocol {
case "", "socks", "socks5":
d, err := socks5.NewSocks5Dialer(link, d) // Socks5 Proxy supports full-cone.
if err != nil {
return nil, nil, err
}
return d, &dialer.Property{
Name: s.Name,
Address: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
Protocol: s.Protocol,
Link: link,
}, nil
//case "socks4", "socks4a":
// d, err := socks4.NewSocks4Dialer(link, &proxy.Direct{})
// if err != nil {
// return nil, err
// }
// return dialer.NewDialer(d, false, s.Name, s.Protocol, link), nil
default:
return nil, nil, fmt.Errorf("unexpected protocol: %v", s.Protocol)
}
}
func ParseSocksURL(link string) (data *Socks, err error) {
u, err := url.Parse(link)
if err != nil {
return nil, dialer.InvalidParameterErr
}
pwd, _ := u.User.Password()
strPort := u.Port()
port, err := strconv.Atoi(strPort)
if err != nil {
return nil, err
}
// socks -> socks5
if u.Scheme == "socks" {
u.Scheme = "socks5"
}
return &Socks{
Name: u.Fragment,
Server: u.Hostname(),
Port: port,
Username: u.User.Username(),
Password: pwd,
Protocol: u.Scheme,
}, nil
}
func (s *Socks) ExportToURL() string {
var user *url.Userinfo
if s.Password != "" {
user = url.UserPassword(s.Username, s.Password)
} else {
user = url.User(s.Username)
}
u := url.URL{
Scheme: s.Protocol,
User: user,
Host: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
Fragment: s.Name,
}
return u.String()
}

View File

@ -1,56 +0,0 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2023, daeuniverse Organization <dae@v2raya.org>
*/
package socks
import (
"context"
"net/netip"
"testing"
"time"
"github.com/daeuniverse/dae/common/netutils"
"github.com/daeuniverse/dae/component/outbound/dialer"
"github.com/daeuniverse/outbound/protocol/direct"
dnsmessage "github.com/miekg/dns"
"github.com/sirupsen/logrus"
)
func TestSocks5(t *testing.T) {
c, err := ParseSocksURL("socks5://192.168.31.6:1081")
if err != nil {
t.Fatal(err)
}
log := logrus.StandardLogger()
d, _, err := c.Dialer(&dialer.GlobalOption{
Log: log,
TcpCheckOptionRaw: dialer.TcpCheckOptionRaw{
Log: log,
Raw: []string{"http://gstatic.com/generate_204"},
ResolverNetwork: "udp",
Method: "HEAD",
},
CheckDnsOptionRaw: dialer.CheckDnsOptionRaw{
Raw: []string{"dns.google.com:53"},
ResolverNetwork: "udp",
},
CheckInterval: 10 * time.Second,
CheckTolerance: 0,
CheckDnsTcp: true,
AllowInsecure: false,
TlsImplementation: "",
UtlsImitate: "",
}, direct.SymmetricDirect)
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
addrs, err := netutils.ResolveNetip(ctx, d, netip.MustParseAddrPort("8.8.8.8:53"), "apple.com", dnsmessage.TypeA, "udp")
if err != nil {
t.Fatal(err)
}
t.Log(addrs)
}

View File

@ -1,194 +0,0 @@
package trojan
import (
"fmt"
"net"
"net/url"
"strconv"
"strings"
"github.com/daeuniverse/dae/component/outbound/transport/tls"
"github.com/daeuniverse/dae/common"
"github.com/daeuniverse/dae/component/outbound/dialer"
"github.com/daeuniverse/dae/component/outbound/transport/ws"
"github.com/daeuniverse/outbound/netproxy"
"github.com/daeuniverse/outbound/protocol"
"github.com/daeuniverse/outbound/transport/grpc"
)
func init() {
dialer.FromLinkRegister("trojan", NewTrojan)
dialer.FromLinkRegister("trojan-go", NewTrojan)
}
type Trojan struct {
Name string `json:"name"`
Server string `json:"server"`
Port int `json:"port"`
Password string `json:"password"`
Sni string `json:"sni"`
Type string `json:"type"`
Encryption string `json:"encryption"`
Host string `json:"host"`
Path string `json:"path"`
ServiceName string `json:"serviceName"`
AllowInsecure bool `json:"allowInsecure"`
Protocol string `json:"protocol"`
}
func NewTrojan(option *dialer.GlobalOption, nextDialer netproxy.Dialer, link string) (netproxy.Dialer, *dialer.Property, error) {
s, err := ParseTrojanURL(link)
if err != nil {
return nil, nil, err
}
return s.Dialer(option, nextDialer)
}
func (s *Trojan) Dialer(option *dialer.GlobalOption, nextDialer netproxy.Dialer) (netproxy.Dialer, *dialer.Property, error) {
d := nextDialer
var err error
if s.Type != "grpc" {
// grpc contains tls
u := url.URL{
Scheme: option.TlsImplementation,
Host: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
RawQuery: url.Values{
"sni": []string{s.Sni},
"allowInsecure": []string{common.BoolToString(s.AllowInsecure || option.AllowInsecure)},
"utlsImitate": []string{option.UtlsImitate},
}.Encode(),
}
if d, _, err = tls.NewTls(option, d, u.String()); err != nil {
return nil, nil, err
}
}
// "tls,ws,ss,trojanc"
switch s.Type {
case "ws":
u := url.URL{
Scheme: "ws",
Host: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
RawQuery: url.Values{
"host": []string{s.Host},
"path": []string{s.Path},
}.Encode(),
}
if d, _, err = ws.NewWs(option, d, u.String()); err != nil {
return nil, nil, err
}
case "grpc":
serviceName := s.ServiceName
if serviceName == "" {
serviceName = "GunService"
}
d = &grpc.Dialer{
NextDialer: &netproxy.ContextDialerConverter{Dialer: d},
ServiceName: serviceName,
ServerName: s.Sni,
AllowInsecure: s.AllowInsecure || option.AllowInsecure,
}
}
if strings.HasPrefix(s.Encryption, "ss;") {
fields := strings.SplitN(s.Encryption, ";", 3)
if d, err = protocol.NewDialer("shadowsocks", d, protocol.Header{
ProxyAddress: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
Cipher: fields[1],
Password: fields[2],
IsClient: false,
}); err != nil {
return nil, nil, err
}
}
if d, err = protocol.NewDialer("trojanc", d, protocol.Header{
ProxyAddress: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
Password: s.Password,
IsClient: true,
}); err != nil {
return nil, nil, err
}
return d, &dialer.Property{
Name: s.Name,
Address: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
Protocol: s.Protocol,
Link: s.ExportToURL(),
}, nil
}
func ParseTrojanURL(u string) (data *Trojan, err error) {
//trojan://password@server:port#escape(remarks)
t, err := url.Parse(u)
if err != nil {
err = fmt.Errorf("invalid trojan format")
return
}
allowInsecure, _ := strconv.ParseBool(t.Query().Get("allowInsecure"))
if !allowInsecure {
allowInsecure, _ = strconv.ParseBool(t.Query().Get("allow_insecure"))
}
if !allowInsecure {
allowInsecure, _ = strconv.ParseBool(t.Query().Get("allowinsecure"))
}
if !allowInsecure {
allowInsecure, _ = strconv.ParseBool(t.Query().Get("skipVerify"))
}
sni := t.Query().Get("peer")
if sni == "" {
sni = t.Query().Get("sni")
}
if sni == "" {
sni = t.Hostname()
}
port, err := strconv.Atoi(t.Port())
if err != nil {
return nil, dialer.InvalidParameterErr
}
data = &Trojan{
Name: t.Fragment,
Server: t.Hostname(),
Port: port,
Password: t.User.Username(),
Sni: sni,
AllowInsecure: allowInsecure,
Protocol: "trojan",
}
if t.Query().Get("type") != "" {
t.Scheme = "trojan-go"
}
if t.Scheme == "trojan-go" {
data.Protocol = "trojan-go"
data.Encryption = t.Query().Get("encryption")
data.Host = t.Query().Get("host")
data.Path = t.Query().Get("path")
data.Type = t.Query().Get("type")
data.ServiceName = t.Query().Get("serviceName")
if data.Type == "grpc" && data.ServiceName == "" {
data.ServiceName = data.Path
}
}
return data, nil
}
func (t *Trojan) ExportToURL() string {
u := &url.URL{
Scheme: "trojan",
User: url.User(t.Password),
Host: net.JoinHostPort(t.Server, strconv.Itoa(t.Port)),
Fragment: t.Name,
}
q := u.Query()
if t.AllowInsecure {
q.Set("allowInsecure", "1")
}
common.SetValue(&q, "sni", t.Sni)
if t.Protocol == "trojan-go" {
u.Scheme = "trojan-go"
common.SetValue(&q, "host", t.Host)
common.SetValue(&q, "encryption", t.Encryption)
common.SetValue(&q, "type", t.Type)
common.SetValue(&q, "path", t.Path)
}
u.RawQuery = q.Encode()
return u.String()
}

View File

@ -1,160 +0,0 @@
package tuic
import (
"crypto/tls"
"fmt"
"net"
"net/url"
"strconv"
"strings"
"github.com/daeuniverse/dae/common"
"github.com/daeuniverse/dae/component/outbound/dialer"
"github.com/daeuniverse/outbound/netproxy"
"github.com/daeuniverse/outbound/protocol"
)
func init() {
dialer.FromLinkRegister("tuic", NewTuic)
}
type Tuic struct {
Name string
Server string
Port int
User string
Password string
Sni string
AllowInsecure bool
DisableSni bool
CongestionControl string
Alpn []string
Protocol string
UdpRelayMode string
}
func NewTuic(option *dialer.GlobalOption, nextDialer netproxy.Dialer, link string) (netproxy.Dialer, *dialer.Property, error) {
s, err := ParseTuicURL(link)
if err != nil {
return nil, nil, err
}
return s.Dialer(option, nextDialer)
}
func (s *Tuic) Dialer(option *dialer.GlobalOption, nextDialer netproxy.Dialer) (netproxy.Dialer, *dialer.Property, error) {
d := nextDialer
var err error
var flags protocol.Flags
if s.UdpRelayMode == "quic" {
flags |= protocol.Flags_Tuic_UdpRelayModeQuic
}
if d, err = protocol.NewDialer("tuic", d, protocol.Header{
ProxyAddress: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
Feature1: s.CongestionControl,
TlsConfig: &tls.Config{
NextProtos: s.Alpn,
MinVersion: tls.VersionTLS13,
ServerName: s.Sni,
InsecureSkipVerify: s.AllowInsecure || option.AllowInsecure,
},
User: s.User,
Password: s.Password,
IsClient: true,
Flags: flags,
}); err != nil {
return nil, nil, err
}
return d, &dialer.Property{
Name: s.Name,
Address: net.JoinHostPort(s.Server, strconv.Itoa(s.Port)),
Protocol: s.Protocol,
Link: s.ExportToURL(),
}, nil
}
func ParseTuicURL(u string) (data *Tuic, err error) {
//trojan://password@server:port#escape(remarks)
t, err := url.Parse(u)
if err != nil {
err = fmt.Errorf("invalid trojan format")
return
}
var alpn []string
if t.Query().Has("alpn") {
alpn = strings.Split(t.Query().Get("alpn"), ",")
for i := range alpn {
alpn[i] = strings.TrimSpace(alpn[i])
}
}
allowInsecure, _ := strconv.ParseBool(t.Query().Get("allowInsecure"))
if !allowInsecure {
allowInsecure, _ = strconv.ParseBool(t.Query().Get("allow_insecure"))
}
if !allowInsecure {
allowInsecure, _ = strconv.ParseBool(t.Query().Get("allowinsecure"))
}
if !allowInsecure {
allowInsecure, _ = strconv.ParseBool(t.Query().Get("skipVerify"))
}
sni := t.Query().Get("peer")
if sni == "" {
sni = t.Query().Get("sni")
}
if sni == "" {
sni = t.Hostname()
}
disableSni, _ := strconv.ParseBool(t.Query().Get("disable_sni"))
if disableSni {
sni = ""
allowInsecure = true
}
port, err := strconv.Atoi(t.Port())
if err != nil {
return nil, dialer.InvalidParameterErr
}
password, _ := t.User.Password()
data = &Tuic{
Name: t.Fragment,
Server: t.Hostname(),
Port: port,
User: t.User.Username(),
Password: password,
Sni: sni,
AllowInsecure: allowInsecure,
DisableSni: disableSni,
CongestionControl: t.Query().Get("congestion_control"),
Alpn: alpn,
UdpRelayMode: strings.ToLower(t.Query().Get("udp_relay_mode")),
Protocol: "tuic",
}
return data, nil
}
func (t *Tuic) ExportToURL() string {
u := &url.URL{
Scheme: "tuic",
User: url.UserPassword(t.User, t.Password),
Host: net.JoinHostPort(t.Server, strconv.Itoa(t.Port)),
Fragment: t.Name,
}
q := u.Query()
if t.AllowInsecure {
q.Set("allow_insecure", "1")
}
common.SetValue(&q, "sni", t.Sni)
if t.DisableSni {
common.SetValue(&q, "disable_sni", "1")
}
if t.CongestionControl != "" {
common.SetValue(&q, "congestion_control", t.CongestionControl)
}
if len(t.Alpn) > 0 {
common.SetValue(&q, "alpn", strings.Join(t.Alpn, ","))
}
if t.UdpRelayMode != "" {
common.SetValue(&q, "udp_relay_mode", t.UdpRelayMode)
}
u.RawQuery = q.Encode()
return u.String()
}

View File

@ -1,3 +1,8 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2022-2024, daeuniverse Organization <dae@v2raya.org>
*/
package dialer
import "time"

View File

@ -1,26 +0,0 @@
package v2ray
import (
"runtime"
"golang.org/x/sys/cpu"
)
var (
hasGCMAsmAMD64 = cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ
hasGCMAsmARM64 = cpu.ARM64.HasAES && cpu.ARM64.HasPMULL
// Keep in sync with crypto/aes/cipher_s390x.go.
hasGCMAsmS390X = cpu.S390X.HasAES && cpu.S390X.HasAESCBC && cpu.S390X.HasAESCTR &&
(cpu.S390X.HasGHASH || cpu.S390X.HasAESGCM)
hasAESGCMHardwareSupport = runtime.GOARCH == "amd64" && hasGCMAsmAMD64 ||
runtime.GOARCH == "arm64" && hasGCMAsmARM64 ||
runtime.GOARCH == "s390x" && hasGCMAsmS390X
)
func getAutoCipher() string {
if hasAESGCMHardwareSupport {
return "aes-128-gcm"
}
return "chacha20-ietf-poly1305"
}

View File

@ -1,384 +0,0 @@
package v2ray
import (
"encoding/base64"
"fmt"
"net"
"net/url"
"regexp"
"strings"
"github.com/daeuniverse/dae/common"
"github.com/daeuniverse/dae/component/outbound/dialer"
"github.com/daeuniverse/dae/component/outbound/transport/tls"
"github.com/daeuniverse/dae/component/outbound/transport/ws"
"github.com/daeuniverse/outbound/netproxy"
"github.com/daeuniverse/outbound/protocol"
"github.com/daeuniverse/outbound/protocol/direct"
"github.com/daeuniverse/outbound/protocol/http"
"github.com/daeuniverse/outbound/transport/grpc"
"github.com/daeuniverse/outbound/transport/meek"
jsoniter "github.com/json-iterator/go"
)
func init() {
dialer.FromLinkRegister("vmess", NewV2Ray)
dialer.FromLinkRegister("vless", NewV2Ray)
}
type V2Ray struct {
Ps string `json:"ps"`
Add string `json:"add"`
Port string `json:"port"`
ID string `json:"id"`
Aid string `json:"aid"`
Net string `json:"net"`
Type string `json:"type"`
Host string `json:"host"`
SNI string `json:"sni"`
Path string `json:"path"`
TLS string `json:"tls"`
Flow string `json:"flow,omitempty"`
Alpn string `json:"alpn,omitempty"`
AllowInsecure bool `json:"allowInsecure"`
V string `json:"v"`
Protocol string `json:"protocol"`
}
func NewV2Ray(option *dialer.GlobalOption, nextDialer netproxy.Dialer, link string) (netproxy.Dialer, *dialer.Property, error) {
var (
s *V2Ray
err error
)
switch {
case strings.HasPrefix(link, "vmess://"):
s, err = ParseVmessURL(link)
if err != nil {
return nil, nil, err
}
if s.Aid != "0" && s.Aid != "" {
return nil, nil, fmt.Errorf("%w: aid: %v, we only support AEAD encryption", dialer.UnexpectedFieldErr, s.Aid)
}
case strings.HasPrefix(link, "vless://"):
s, err = ParseVlessURL(link)
if err != nil {
return nil, nil, err
}
default:
return nil, nil, dialer.InvalidParameterErr
}
return s.Dialer(option, nextDialer)
}
func (s *V2Ray) Dialer(option *dialer.GlobalOption, nextDialer netproxy.Dialer) (npd netproxy.Dialer, property *dialer.Property, err error) {
d := nextDialer
switch s.Protocol {
case "vmess", "vless":
default:
return nil, nil, fmt.Errorf("V2Ray.Dialer: unexpected protocol: %v", s.Protocol)
}
switch strings.ToLower(s.Net) {
case "ws":
scheme := "ws"
if s.TLS == "tls" || s.TLS == "xtls" {
scheme = "wss"
}
sni := s.SNI
if sni == "" {
sni = s.Host
}
u := url.URL{
Scheme: scheme,
Host: net.JoinHostPort(s.Add, s.Port),
Path: s.Path,
RawQuery: url.Values{
"host": []string{s.Host},
"sni": []string{sni},
"allowInsecure": []string{common.BoolToString(s.AllowInsecure || option.AllowInsecure)},
}.Encode(),
}
d, _, err = ws.NewWs(option, d, u.String())
if err != nil {
return nil, nil, err
}
case "tcp":
if s.TLS == "tls" || s.TLS == "xtls" {
sni := s.SNI
if sni == "" {
sni = s.Host
}
u := url.URL{
Scheme: option.TlsImplementation,
Host: net.JoinHostPort(s.Add, s.Port),
RawQuery: url.Values{
"sni": []string{sni},
"allowInsecure": []string{common.BoolToString(s.AllowInsecure || option.AllowInsecure)},
"utlsImitate": []string{option.UtlsImitate},
}.Encode(),
}
d, _, err = tls.NewTls(option, d, u.String())
if err != nil {
return nil, nil, err
}
}
if s.Type != "none" && s.Type != "" {
return nil, nil, fmt.Errorf("%w: type: %v", dialer.UnexpectedFieldErr, s.Type)
}
case "grpc":
sni := s.SNI
if sni == "" {
sni = s.Host
}
serviceName := s.Path
if serviceName == "" {
serviceName = "GunService"
}
d = &grpc.Dialer{
NextDialer: &netproxy.ContextDialerConverter{Dialer: d},
ServiceName: serviceName,
ServerName: sni,
AllowInsecure: s.AllowInsecure || option.AllowInsecure,
}
case "http", "http2", "h2":
sni := s.SNI
if sni == "" {
sni = s.Add
}
scheme := "http"
if s.TLS == "tls" {
scheme = "https"
}
u := url.URL{
Scheme: scheme,
Host: net.JoinHostPort(s.Add, s.Port),
Path: s.Path,
RawQuery: url.Values{
"sni": []string{sni},
"allowInsecure": []string{common.BoolToString(s.AllowInsecure || option.AllowInsecure)},
"tlsImplementation": []string{option.TlsImplementation},
"utlsImitate": []string{option.UtlsImitate},
"host": []string{s.Host},
"alpn": []string{s.Alpn},
"transport": []string{"1"},
}.Encode(),
}
d, err = http.NewHTTPProxy(&u, direct.SymmetricDirect)
if err != nil {
return nil, nil, err
}
case "meek":
if strings.HasPrefix(s.Path, "https://") && s.TLS != "tls" && s.TLS != "utls" {
return nil, nil, fmt.Errorf("%w: meek: tls should be enabled", dialer.InvalidParameterErr)
}
u := url.URL{
Scheme: "meek",
Host: net.JoinHostPort(s.Add, s.Port),
RawQuery: url.Values{
"url": []string{s.Path},
"alpn": []string{s.Alpn},
"serverName": []string{s.SNI},
"skipCertVerify": []string{common.BoolToString(s.AllowInsecure || option.AllowInsecure)},
}.Encode(),
}
d, err = meek.NewDialer(u.String(), d)
if err != nil {
return nil, nil, err
}
default:
return nil, nil, fmt.Errorf("%w: network: %v", dialer.UnexpectedFieldErr, s.Net)
}
if d, err = protocol.NewDialer(s.Protocol, d, protocol.Header{
ProxyAddress: net.JoinHostPort(s.Add, s.Port),
Cipher: getAutoCipher(),
Password: s.ID,
IsClient: true,
//Flags: protocol.Flags_VMess_UsePacketAddr,
}); err != nil {
return nil, nil, err
}
return d, &dialer.Property{
Name: s.Ps,
Address: net.JoinHostPort(s.Add, s.Port),
Protocol: s.Protocol,
Link: s.ExportToURL(),
}, nil
}
func ParseVlessURL(vless string) (data *V2Ray, err error) {
u, err := url.Parse(vless)
if err != nil {
return nil, err
}
data = &V2Ray{
Ps: u.Fragment,
Add: u.Hostname(),
Port: u.Port(),
ID: u.User.String(),
Net: u.Query().Get("type"),
Type: u.Query().Get("headerType"),
SNI: u.Query().Get("sni"),
Host: u.Query().Get("host"),
Path: u.Query().Get("path"),
TLS: u.Query().Get("security"),
Flow: u.Query().Get("flow"),
Alpn: u.Query().Get("alpn"),
Protocol: "vless",
}
if data.Net == "" {
data.Net = "tcp"
}
if data.Net == "grpc" {
data.Path = u.Query().Get("serviceName")
}
if data.Net == "meek" {
data.Path = u.Query().Get("url")
}
if data.Type == "" {
data.Type = "none"
}
if data.TLS == "" {
data.TLS = "none"
}
if data.Flow == "" {
data.Flow = "xtls-rprx-direct"
}
if data.Type == "mkcp" || data.Type == "kcp" {
data.Path = u.Query().Get("seed")
}
return data, nil
}
func ParseVmessURL(vmess string) (data *V2Ray, err error) {
var info V2Ray
// perform base64 decoding and unmarshal to VmessInfo
raw, err := common.Base64StdDecode(vmess[8:])
if err != nil {
raw, err = common.Base64UrlDecode(vmess[8:])
}
if err != nil {
// not in json format, try to resolve as vmess://BASE64(Security:ID@Add:Port)?remarks=Ps&obfsParam=Host&Path=Path&obfs=Net&tls=TLS
var u *url.URL
u, err = url.Parse(vmess)
if err != nil {
return
}
re := regexp.MustCompile(`.*:(.+)@(.+):(\d+)`)
s := strings.Split(vmess[8:], "?")[0]
s, err = common.Base64StdDecode(s)
if err != nil {
s, _ = common.Base64UrlDecode(s)
}
subMatch := re.FindStringSubmatch(s)
if subMatch == nil {
err = fmt.Errorf("unrecognized vmess address")
return
}
q := u.Query()
ps := q.Get("remarks")
if ps == "" {
ps = q.Get("remark")
}
obfs := q.Get("obfs")
obfsParam := q.Get("obfsParam")
path := q.Get("path")
if obfs == "kcp" || obfs == "mkcp" {
m := make(map[string]string)
//cater to v2rayN definition
_ = jsoniter.Unmarshal([]byte(obfsParam), &m)
path = m["seed"]
obfsParam = ""
}
aid := q.Get("alterId")
if aid == "" {
aid = q.Get("aid")
}
sni := q.Get("peer")
info = V2Ray{
ID: subMatch[1],
Add: subMatch[2],
Port: subMatch[3],
Ps: ps,
Host: jsoniter.Get([]byte(obfsParam), "host").ToString(),
Path: path,
Net: obfs,
Aid: aid,
TLS: map[string]string{"1": "tls"}[q.Get("tls")],
SNI: sni,
AllowInsecure: false,
}
if info.Net == "websocket" {
info.Net = "ws"
}
} else {
err = jsoniter.Unmarshal([]byte(raw), &info)
if err != nil {
return
}
}
// correct the wrong vmess as much as possible
if strings.HasPrefix(info.Host, "/") && info.Path == "" {
info.Path = info.Host
info.Host = ""
}
if info.Aid == "" {
info.Aid = "0"
}
info.Protocol = "vmess"
return &info, nil
}
func (s *V2Ray) ExportToURL() string {
switch s.Protocol {
case "vless":
// https://github.com/XTLS/Xray-core/issues/91
var query = make(url.Values)
common.SetValue(&query, "type", s.Net)
common.SetValue(&query, "security", s.TLS)
switch s.Net {
case "websocket", "ws", "http", "h2":
common.SetValue(&query, "path", s.Path)
common.SetValue(&query, "host", s.Host)
case "mkcp", "kcp":
common.SetValue(&query, "headerType", s.Type)
common.SetValue(&query, "seed", s.Path)
case "tcp":
common.SetValue(&query, "headerType", s.Type)
common.SetValue(&query, "host", s.Host)
common.SetValue(&query, "path", s.Path)
case "grpc":
common.SetValue(&query, "serviceName", s.Path)
case "meek":
common.SetValue(&query, "url", s.Host)
}
//TODO: QUIC
if s.TLS != "none" {
common.SetValue(&query, "sni", s.Host) // FIXME: it may be different from ws's host
common.SetValue(&query, "alpn", s.Alpn)
}
if s.TLS == "xtls" {
common.SetValue(&query, "flow", s.Flow)
}
U := url.URL{
Scheme: "vless",
User: url.User(s.ID),
Host: net.JoinHostPort(s.Add, s.Port),
RawQuery: query.Encode(),
Fragment: s.Ps,
}
return U.String()
case "vmess":
s.V = "2"
b, _ := jsoniter.Marshal(s)
return "vmess://" + strings.TrimSuffix(base64.StdEncoding.EncodeToString(b), "=")
}
//log.Warn("unexpected protocol: %v", v.Protocol)
return ""
}

View File

@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2022-2023, daeuniverse Organization <dae@v2raya.org>
* Copyright (c) 2022-2024, daeuniverse Organization <dae@v2raya.org>
*/
package outbound

View File

@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2022-2023, daeuniverse Organization <dae@v2raya.org>
* Copyright (c) 2022-2024, daeuniverse Organization <dae@v2raya.org>
*/
package outbound
@ -28,6 +28,12 @@ var TestNetworkType = &dialer.NetworkType{
var log = logger.NewLogger("trace", false, nil)
func newDirectDialer(option *dialer.GlobalOption, fullcone bool) *dialer.Dialer {
_d, p := dialer.NewDirectDialer(option, true)
d := dialer.NewDialer(_d, option, dialer.InstanceOption{DisableCheck: false}, p)
return d
}
func TestDialerGroup_Select_Fixed(t *testing.T) {
option := &dialer.GlobalOption{
Log: log,
@ -38,14 +44,15 @@ func TestDialerGroup_Select_Fixed(t *testing.T) {
CheckDnsTcp: false,
}
dialers := []*dialer.Dialer{
dialer.NewDirectDialer(option, true),
dialer.NewDirectDialer(option, false),
newDirectDialer(option, true),
newDirectDialer(option, false),
}
fixedIndex := 1
g := NewDialerGroup(option, "test-group", dialers, DialerSelectionPolicy{
Policy: consts.DialerSelectionPolicy_Fixed,
FixedIndex: fixedIndex,
}, func(alive bool, networkType *dialer.NetworkType, isInit bool) {})
g := NewDialerGroup(option, "test-group", dialers, []*dialer.Annotation{{}},
DialerSelectionPolicy{
Policy: consts.DialerSelectionPolicy_Fixed,
FixedIndex: fixedIndex,
}, func(alive bool, networkType *dialer.NetworkType, isInit bool) {})
for i := 0; i < 10; i++ {
d, _, err := g.Select(TestNetworkType, false)
if err != nil {
@ -78,20 +85,21 @@ func TestDialerGroup_Select_MinLastLatency(t *testing.T) {
CheckInterval: 15 * time.Second,
}
dialers := []*dialer.Dialer{
dialer.NewDirectDialer(option, false),
dialer.NewDirectDialer(option, false),
dialer.NewDirectDialer(option, false),
dialer.NewDirectDialer(option, false),
dialer.NewDirectDialer(option, false),
dialer.NewDirectDialer(option, false),
dialer.NewDirectDialer(option, false),
dialer.NewDirectDialer(option, false),
dialer.NewDirectDialer(option, false),
dialer.NewDirectDialer(option, false),
newDirectDialer(option, false),
newDirectDialer(option, false),
newDirectDialer(option, false),
newDirectDialer(option, false),
newDirectDialer(option, false),
newDirectDialer(option, false),
newDirectDialer(option, false),
newDirectDialer(option, false),
newDirectDialer(option, false),
newDirectDialer(option, false),
}
g := NewDialerGroup(option, "test-group", dialers, DialerSelectionPolicy{
Policy: consts.DialerSelectionPolicy_MinLastLatency,
}, func(alive bool, networkType *dialer.NetworkType, isInit bool) {})
g := NewDialerGroup(option, "test-group", dialers, []*dialer.Annotation{{}},
DialerSelectionPolicy{
Policy: consts.DialerSelectionPolicy_MinLastLatency,
}, func(alive bool, networkType *dialer.NetworkType, isInit bool) {})
// Test 1000 times.
for i := 0; i < 1000; i++ {
@ -147,15 +155,16 @@ func TestDialerGroup_Select_Random(t *testing.T) {
CheckInterval: 15 * time.Second,
}
dialers := []*dialer.Dialer{
dialer.NewDirectDialer(option, false),
dialer.NewDirectDialer(option, false),
dialer.NewDirectDialer(option, false),
dialer.NewDirectDialer(option, false),
dialer.NewDirectDialer(option, false),
newDirectDialer(option, false),
newDirectDialer(option, false),
newDirectDialer(option, false),
newDirectDialer(option, false),
newDirectDialer(option, false),
}
g := NewDialerGroup(option, "test-group", dialers, DialerSelectionPolicy{
Policy: consts.DialerSelectionPolicy_Random,
}, func(alive bool, networkType *dialer.NetworkType, isInit bool) {})
g := NewDialerGroup(option, "test-group", dialers, []*dialer.Annotation{{}},
DialerSelectionPolicy{
Policy: consts.DialerSelectionPolicy_Random,
}, func(alive bool, networkType *dialer.NetworkType, isInit bool) {})
count := make([]int, len(dialers))
for i := 0; i < 100; i++ {
d, _, err := g.Select(TestNetworkType, false)
@ -186,15 +195,16 @@ func TestDialerGroup_SetAlive(t *testing.T) {
CheckInterval: 15 * time.Second,
}
dialers := []*dialer.Dialer{
dialer.NewDirectDialer(option, false),
dialer.NewDirectDialer(option, false),
dialer.NewDirectDialer(option, false),
dialer.NewDirectDialer(option, false),
dialer.NewDirectDialer(option, false),
newDirectDialer(option, false),
newDirectDialer(option, false),
newDirectDialer(option, false),
newDirectDialer(option, false),
newDirectDialer(option, false),
}
g := NewDialerGroup(option, "test-group", dialers, DialerSelectionPolicy{
Policy: consts.DialerSelectionPolicy_Random,
}, func(alive bool, networkType *dialer.NetworkType, isInit bool) {})
g := NewDialerGroup(option, "test-group", dialers, []*dialer.Annotation{{}},
DialerSelectionPolicy{
Policy: consts.DialerSelectionPolicy_Random,
}, func(alive bool, networkType *dialer.NetworkType, isInit bool) {})
zeroTarget := 3
g.MustGetAliveDialerSet(TestNetworkType).NotifyLatencyChange(dialers[zeroTarget], false)
count := make([]int, len(dialers))

View File

@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2022-2023, daeuniverse Organization <dae@v2raya.org>
* Copyright (c) 2022-2024, daeuniverse Organization <dae@v2raya.org>
*/
package outbound

View File

@ -1,17 +1,18 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2022-2023, daeuniverse Organization <dae@v2raya.org>
* Copyright (c) 2022-2024, daeuniverse Organization <dae@v2raya.org>
*/
package outbound
import (
"fmt"
"regexp"
"strings"
"github.com/daeuniverse/dae/component/outbound/dialer"
"github.com/daeuniverse/dae/pkg/config_parser"
"github.com/dlclark/regexp2"
"github.com/sirupsen/logrus"
)
const (
@ -28,20 +29,22 @@ const (
)
type DialerSet struct {
log *logrus.Logger
dialers []*dialer.Dialer
nodeToTagMap map[*dialer.Dialer]string
}
func NewDialerSetFromLinks(option *dialer.GlobalOption, tagToNodeList map[string][]string) *DialerSet {
s := &DialerSet{
log: option.Log,
dialers: make([]*dialer.Dialer, 0),
nodeToTagMap: make(map[*dialer.Dialer]string),
}
for subscriptionTag, nodes := range tagToNodeList {
for _, node := range nodes {
d, err := dialer.NewFromLink(option, dialer.InstanceOption{CheckEnabled: false}, node, subscriptionTag)
d, err := dialer.NewFromLink(option, dialer.InstanceOption{DisableCheck: false}, node, subscriptionTag)
if err != nil {
option.Log.Infof("failed to parse node: %v", err)
s.log.Infof("failed to parse node: %v", err)
continue
}
s.dialers = append(s.dialers, d)
@ -69,24 +72,29 @@ func (s *DialerSet) filterHit(dialer *dialer.Dialer, filters []*config_parser.Fu
switch filter.Name {
case FilterInput_Name:
// Or
loop:
for _, param := range filter.Params {
switch param.Key {
case FilterKey_Name_Regex:
matched, _ := regexp.MatchString(param.Val, dialer.Property().Name)
regex, err := regexp2.Compile(param.Val, 0)
if err != nil {
return false, fmt.Errorf("bad regexp in filter %v: %w", filter.String(false, true, true), err)
}
matched, _ := regex.MatchString(dialer.Property().Name)
//logrus.Warnln(param.Val, matched, dialer.Name())
if matched {
subFilterHit = true
break
break loop
}
case FilterKey_Name_Keyword:
if strings.Contains(dialer.Property().Name, param.Val) {
subFilterHit = true
break
break loop
}
case "":
if dialer.Property().Name == param.Val {
subFilterHit = true
break
break loop
}
default:
return false, fmt.Errorf(`unsupported filter key "%v" in "filter: %v()"`, param.Key, filter.Name)
@ -94,20 +102,25 @@ func (s *DialerSet) filterHit(dialer *dialer.Dialer, filters []*config_parser.Fu
}
case FilterInput_SubscriptionTag:
// Or
loop2:
for _, param := range filter.Params {
switch param.Key {
case FilterInput_SubscriptionTag_Regex:
matched, _ := regexp.MatchString(param.Val, s.nodeToTagMap[dialer])
//logrus.Warnln(param.Val, matched, dialer.Name())
regex, err := regexp2.Compile(param.Val, 0)
if err != nil {
return false, fmt.Errorf("bad regexp in filter %v: %w", filter.String(false, true, true), err)
}
matched, _ := regex.MatchString(s.nodeToTagMap[dialer])
if matched {
subFilterHit = true
break
break loop2
}
//logrus.Warnln(param.Val, matched, dialer.Name())
case "":
// Full
if s.nodeToTagMap[dialer] == param.Val {
subFilterHit = true
break
break loop2
}
default:
return false, fmt.Errorf(`unsupported filter key "%v" in "filter: %v()"`, param.Key, filter.Name)

View File

@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: AGPL-3.0-only
* Copyright (c) 2022-2023, daeuniverse Organization <dae@v2raya.org>
* Copyright (c) 2022-2024, daeuniverse Organization <dae@v2raya.org>
*/
package outbound

View File

@ -1,7 +0,0 @@
package simpleobfs
import "github.com/daeuniverse/dae/component/outbound/dialer"
func init() {
dialer.FromLinkRegister("simpleobfs", NewSimpleObfs)
}

View File

@ -1,108 +0,0 @@
// from https://github.com/Dreamacro/clash/blob/master/component/simple-obfs/http.go
package simpleobfs
import (
"bytes"
"encoding/base64"
"fmt"
"io"
"net/http"
"strings"
"sync"
"github.com/daeuniverse/outbound/netproxy"
"github.com/daeuniverse/outbound/pkg/fastrand"
"github.com/daeuniverse/outbound/pool"
)
// HTTPObfs is shadowsocks http simple-obfs implementation
type HTTPObfs struct {
netproxy.Conn
host string
port string
path string
buf []byte
offset int
firstRequest bool
firstResponse bool
wMu sync.Mutex
rMu sync.Mutex
}
func (ho *HTTPObfs) Read(b []byte) (int, error) {
ho.rMu.Lock()
defer ho.rMu.Unlock()
if ho.buf != nil {
n := copy(b, ho.buf[ho.offset:])
ho.offset += n
if ho.offset == len(ho.buf) {
pool.Put(ho.buf)
ho.buf = nil
}
return n, nil
}
if ho.firstResponse {
buf := pool.Get(1 << 15)
n, err := ho.Conn.Read(buf)
if err != nil {
pool.Put(buf)
return 0, err
}
idx := bytes.Index(buf[:n], []byte("\r\n\r\n"))
if idx == -1 {
pool.Put(buf)
return 0, io.EOF
}
ho.firstResponse = false
length := n - (idx + 4)
n = copy(b, buf[idx+4:n])
if length > n {
ho.buf = buf[:idx+4+length]
ho.offset = idx + 4 + n
} else {
pool.Put(buf)
}
return n, nil
}
return ho.Conn.Read(b)
}
func (ho *HTTPObfs) Write(b []byte) (int, error) {
ho.wMu.Lock()
defer ho.wMu.Unlock()
if ho.firstRequest {
req, _ := http.NewRequest("GET", fmt.Sprintf("http://%s%s", ho.host, ho.path), bytes.NewBuffer(b[:]))
req.Header.Set("User-Agent", fmt.Sprintf("curl/7.%d.%d", fastrand.Rand().Int()%87, fastrand.Rand().Int()%2))
req.Header.Set("Upgrade", "websocket")
req.Header.Set("Connection", "Upgrade")
if ho.port != "80" {
req.Host = fmt.Sprintf("%s:%s", ho.host, ho.port)
}
randBytes := make([]byte, 16)
fastrand.Read(randBytes)
req.Header.Set("Sec-WebSocket-Key", base64.URLEncoding.EncodeToString(randBytes))
req.ContentLength = int64(len(b))
err := req.Write(ho.Conn)
ho.firstRequest = false
return len(b), err
}
return ho.Conn.Write(b)
}
// NewHTTPObfs return a HTTPObfs
func NewHTTPObfs(conn netproxy.Conn, host string, port string, path string) netproxy.Conn {
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
return &HTTPObfs{
Conn: conn,
firstRequest: true,
firstResponse: true,
host: host,
port: port,
path: path,
}
}

View File

@ -1,97 +0,0 @@
package simpleobfs
import (
"fmt"
"net"
"net/url"
"strings"
"github.com/daeuniverse/dae/component/outbound/dialer"
"github.com/daeuniverse/outbound/netproxy"
)
type ObfsType int
const (
HTTP ObfsType = iota
TLS
)
// SimpleObfs is a base http-obfs struct
type SimpleObfs struct {
dialer netproxy.Dialer
obfstype ObfsType
addr string
path string
host string
}
// NewSimpleobfs returns a simpleobfs proxy.
func NewSimpleObfs(option *dialer.GlobalOption, nextDialer netproxy.Dialer, link string) (netproxy.Dialer, *dialer.Property, error) {
u, err := url.Parse(link)
if err != nil {
return nil, nil, fmt.Errorf("simpleobfs: %w", err)
}
t := &SimpleObfs{
dialer: nextDialer,
addr: u.Host,
}
query := u.Query()
obfstype := query.Get("type")
if obfstype == "" {
obfstype = query.Get("obfs")
}
switch strings.ToLower(obfstype) {
case "http":
t.obfstype = HTTP
case "tls":
t.obfstype = TLS
default:
return nil, nil, fmt.Errorf("unsupported obfs type %v", obfstype)
}
t.host = query.Get("host")
t.path = query.Get("path")
if t.path == "" {
t.path = query.Get("uri")
}
return t, &dialer.Property{
Name: u.Fragment,
Address: t.addr,
Protocol: "simpleobfs(" + obfstype + ")",
Link: link,
}, nil
}
func (s *SimpleObfs) Dial(network, addr string) (c netproxy.Conn, err error) {
magicNetwork, err := netproxy.ParseMagicNetwork(network)
if err != nil {
return nil, err
}
switch magicNetwork.Network {
case "tcp":
rc, err := s.dialer.Dial(network, s.addr)
if err != nil {
return nil, fmt.Errorf("[simpleobfs]: dial to %s: %w", s.addr, err)
}
host, port, err := net.SplitHostPort(s.addr)
if err != nil {
return nil, err
}
if s.host != "" {
host = s.host
}
switch s.obfstype {
case HTTP:
c = NewHTTPObfs(rc, host, port, s.path)
case TLS:
c = NewTLSObfs(rc, host)
}
return c, err
case "udp":
return nil, fmt.Errorf("%w: simpleobfs+udp", netproxy.UnsupportedTunnelTypeError)
default:
return nil, fmt.Errorf("%w: %v", netproxy.UnsupportedTunnelTypeError, network)
}
}

View File

@ -1,200 +0,0 @@
// from https://github.com/Dreamacro/clash/blob/master/component/simple-obfs/tls.go
package simpleobfs
import (
"bytes"
"encoding/binary"
"io"
"sync"
"time"
"github.com/daeuniverse/outbound/netproxy"
"github.com/daeuniverse/outbound/pkg/fastrand"
)
const (
chunkSize = 1 << 14 // 2 ** 14 == 16 * 1024
)
// TLSObfs is shadowsocks tls simple-obfs implementation
type TLSObfs struct {
netproxy.Conn
server string
remain int
firstRequest bool
firstResponse bool
rMu sync.Mutex
wMu sync.Mutex
}
func (to *TLSObfs) read(b []byte, discardN int) (int, error) {
buf := make([]byte, discardN)
_, err := io.ReadFull(to.Conn, buf)
if err != nil {
return 0, err
}
sizeBuf := make([]byte, 2)
_, err = io.ReadFull(to.Conn, sizeBuf)
if err != nil {
return 0, nil
}
length := int(binary.BigEndian.Uint16(sizeBuf))
if length > len(b) {
n, err := to.Conn.Read(b)
if err != nil {
return n, err
}
to.remain = length - n
return n, nil
}
return io.ReadFull(to.Conn, b[:length])
}
func (to *TLSObfs) Read(b []byte) (int, error) {
to.rMu.Lock()
defer to.rMu.Unlock()
if to.remain > 0 {
length := to.remain
if length > len(b) {
length = len(b)
}
n, err := io.ReadFull(to.Conn, b[:length])
to.remain -= n
return n, err
}
if to.firstResponse {
// type + ver + lensize + 91 = 96
// type + ver + lensize + 1 = 6
// type + ver = 3
to.firstResponse = false
return to.read(b, 105)
}
// type + ver = 3
return to.read(b, 3)
}
func (to *TLSObfs) Write(b []byte) (int, error) {
to.wMu.Lock()
defer to.wMu.Unlock()
length := len(b)
for i := 0; i < length; i += chunkSize {
end := i + chunkSize
if end > length {
end = length
}
n, err := to.write(b[i:end])
if err != nil {
return n, err
}
}
return length, nil
}
func (to *TLSObfs) write(b []byte) (int, error) {
if to.firstRequest {
helloMsg := makeClientHelloMsg(b, to.server)
_, err := to.Conn.Write(helloMsg)
to.firstRequest = false
return len(b), err
}
buf := &bytes.Buffer{}
buf.Write([]byte{0x17, 0x03, 0x03})
binary.Write(buf, binary.BigEndian, uint16(len(b)))
buf.Write(b)
_, err := to.Conn.Write(buf.Bytes())
return len(b), err
}
// NewTLSObfs return a SimpleObfs
func NewTLSObfs(conn netproxy.Conn, server string) netproxy.Conn {
return &TLSObfs{
Conn: conn,
server: server,
firstRequest: true,
firstResponse: true,
}
}
func makeClientHelloMsg(data []byte, server string) []byte {
random := make([]byte, 28)
sessionID := make([]byte, 32)
fastrand.Read(random)
fastrand.Read(sessionID)
buf := &bytes.Buffer{}
// handshake, TLS 1.0 version, length
buf.WriteByte(22)
buf.Write([]byte{0x03, 0x01})
length := uint16(212 + len(data) + len(server))
buf.WriteByte(byte(length >> 8))
buf.WriteByte(byte(length & 0xff))
// clientHello, length, TLS 1.2 version
buf.WriteByte(1)
buf.WriteByte(0)
binary.Write(buf, binary.BigEndian, uint16(208+len(data)+len(server)))
buf.Write([]byte{0x03, 0x03})
// random with timestamp, sid len, sid
binary.Write(buf, binary.BigEndian, uint32(time.Now().Unix()))
buf.Write(random)
buf.WriteByte(32)
buf.Write(sessionID)
// cipher suites
buf.Write([]byte{0x00, 0x38})
buf.Write([]byte{
0xc0, 0x2c, 0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0xaa, 0xc0, 0x2b, 0xc0, 0x2f,
0x00, 0x9e, 0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23, 0xc0, 0x27, 0x00, 0x67, 0xc0, 0x0a,
0xc0, 0x14, 0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33, 0x00, 0x9d, 0x00, 0x9c, 0x00, 0x3d,
0x00, 0x3c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0xff,
})
// compression
buf.Write([]byte{0x01, 0x00})
// extension length
binary.Write(buf, binary.BigEndian, uint16(79+len(data)+len(server)))
// session ticket
buf.Write([]byte{0x00, 0x23})
binary.Write(buf, binary.BigEndian, uint16(len(data)))
buf.Write(data)
// server name
buf.Write([]byte{0x00, 0x00})
binary.Write(buf, binary.BigEndian, uint16(len(server)+5))
binary.Write(buf, binary.BigEndian, uint16(len(server)+3))
buf.WriteByte(0)
binary.Write(buf, binary.BigEndian, uint16(len(server)))
buf.Write([]byte(server))
// ec_point
buf.Write([]byte{0x00, 0x0b, 0x00, 0x04, 0x03, 0x01, 0x00, 0x02})
// groups
buf.Write([]byte{0x00, 0x0a, 0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, 0x00, 0x18})
// signature
buf.Write([]byte{
0x00, 0x0d, 0x00, 0x20, 0x00, 0x1e, 0x06, 0x01, 0x06, 0x02, 0x06, 0x03, 0x05,
0x01, 0x05, 0x02, 0x05, 0x03, 0x04, 0x01, 0x04, 0x02, 0x04, 0x03, 0x03, 0x01,
0x03, 0x02, 0x03, 0x03, 0x02, 0x01, 0x02, 0x02, 0x02, 0x03,
})
// encrypt then mac
buf.Write([]byte{0x00, 0x16, 0x00, 0x00})
// extended master secret
buf.Write([]byte{0x00, 0x17, 0x00, 0x00})
return buf.Bytes()
}

View File

@ -1,8 +0,0 @@
package tls
import "github.com/daeuniverse/dae/component/outbound/dialer"
func init() {
dialer.FromLinkRegister("tls", NewTls)
dialer.FromLinkRegister("utls", NewTls)
}

View File

@ -1,131 +0,0 @@
package tls
import (
"crypto/tls"
"fmt"
"net/url"
"strconv"
"strings"
"github.com/daeuniverse/dae/component/outbound/dialer"
"github.com/daeuniverse/outbound/netproxy"
utls "github.com/refraction-networking/utls"
)
// Tls is a base Tls struct
type Tls struct {
dialer netproxy.Dialer
addr string
serverName string
skipVerify bool
tlsImplentation string
utlsImitate string
tlsConfig *tls.Config
}
// NewTls returns a Tls infra.
func NewTls(option *dialer.GlobalOption, nextDialer netproxy.Dialer, link string) (netproxy.Dialer, *dialer.Property, error) {
u, err := url.Parse(link)
if err != nil {
return nil, nil, fmt.Errorf("NewTls: %w", err)
}
query := u.Query()
tlsImplentation := u.Scheme
utlsImitate := query.Get("utlsImitate")
if tlsImplentation == "tls" && option.TlsImplementation != "" {
tlsImplentation = option.TlsImplementation
utlsImitate = option.UtlsImitate
}
t := &Tls{
dialer: nextDialer,
addr: u.Host,
tlsImplentation: tlsImplentation,
utlsImitate: utlsImitate,
serverName: query.Get("sni"),
}
if t.serverName == "" {
t.serverName = u.Hostname()
}
// skipVerify
allowInsecure, _ := strconv.ParseBool(u.Query().Get("allowInsecure"))
if !allowInsecure {
allowInsecure, _ = strconv.ParseBool(u.Query().Get("allow_insecure"))
}
if !allowInsecure {
allowInsecure, _ = strconv.ParseBool(u.Query().Get("allowinsecure"))
}
if !allowInsecure {
allowInsecure, _ = strconv.ParseBool(u.Query().Get("skipVerify"))
}
t.skipVerify = allowInsecure || option.AllowInsecure
t.tlsConfig = &tls.Config{
ServerName: t.serverName,
InsecureSkipVerify: t.skipVerify,
}
if len(query.Get("alpn")) > 0 {
t.tlsConfig.NextProtos = strings.Split(query.Get("alpn"), ",")
}
return t, &dialer.Property{
Name: u.Fragment,
Address: t.addr,
Protocol: tlsImplentation,
Link: link,
}, nil
}
func (s *Tls) Dial(network, addr string) (c netproxy.Conn, err error) {
magicNetwork, err := netproxy.ParseMagicNetwork(network)
if err != nil {
return nil, err
}
switch magicNetwork.Network {
case "tcp":
rc, err := s.dialer.Dial(network, s.addr)
if err != nil {
return nil, fmt.Errorf("[Tls]: dial to %s: %w", s.addr, err)
}
var tlsConn interface {
netproxy.Conn
Handshake() error
}
switch s.tlsImplentation {
case "tls":
tlsConn = tls.Client(&netproxy.FakeNetConn{
Conn: rc,
LAddr: nil,
RAddr: nil,
}, s.tlsConfig)
case "utls":
clientHelloID, err := nameToUtlsClientHelloID(s.utlsImitate)
if err != nil {
return nil, err
}
tlsConn = utls.UClient(&netproxy.FakeNetConn{
Conn: rc,
LAddr: nil,
RAddr: nil,
}, uTLSConfigFromTLSConfig(s.tlsConfig), *clientHelloID)
default:
return nil, fmt.Errorf("unknown tls implementation: %v", s.tlsImplentation)
}
if err := tlsConn.Handshake(); err != nil {
return nil, err
}
return tlsConn, err
case "udp":
return nil, fmt.Errorf("%w: tls+udp", netproxy.UnsupportedTunnelTypeError)
default:
return nil, fmt.Errorf("%w: %v", netproxy.UnsupportedTunnelTypeError, network)
}
}

View File

@ -1,63 +0,0 @@
package tls
import (
"crypto/tls"
"fmt"
utls "github.com/refraction-networking/utls"
)
func uTLSConfigFromTLSConfig(config *tls.Config) *utls.Config {
return &utls.Config{
ServerName: config.ServerName,
InsecureSkipVerify: config.InsecureSkipVerify,
}
}
var clientHelloIDMap = map[string]*utls.ClientHelloID{
"randomized": &utls.HelloRandomized,
"randomizedalpn": &utls.HelloRandomizedALPN,
"randomizednoalpn": &utls.HelloRandomizedNoALPN,
"firefox_auto": &utls.HelloFirefox_Auto,
"firefox_55": &utls.HelloFirefox_55,
"firefox_56": &utls.HelloFirefox_56,
"firefox_63": &utls.HelloFirefox_63,
"firefox_65": &utls.HelloFirefox_65,
"firefox_99": &utls.HelloFirefox_99,
"firefox_102": &utls.HelloFirefox_102,
"firefox_105": &utls.HelloFirefox_105,
"chrome_auto": &utls.HelloChrome_Auto,
"chrome_58": &utls.HelloChrome_58,
"chrome_62": &utls.HelloChrome_62,
"chrome_70": &utls.HelloChrome_70,
"chrome_72": &utls.HelloChrome_72,
"chrome_83": &utls.HelloChrome_83,
"chrome_87": &utls.HelloChrome_87,
"chrome_96": &utls.HelloChrome_96,
"chrome_100": &utls.HelloChrome_100,
"chrome_102": &utls.HelloChrome_102,
"ios_auto": &utls.HelloIOS_Auto,
"ios_11_1": &utls.HelloIOS_11_1,
"ios_12_1": &utls.HelloIOS_12_1,
"ios_13": &utls.HelloIOS_13,
"ios_14": &utls.HelloIOS_14,
"android_11_okhttp": &utls.HelloAndroid_11_OkHttp,
"edge_auto": &utls.HelloEdge_Auto,
"edge_85": &utls.HelloEdge_85,
"edge_106": &utls.HelloEdge_106,
"safari_auto": &utls.HelloSafari_Auto,
"safari_16_0": &utls.HelloSafari_16_0,
"360_auto": &utls.Hello360_Auto,
"360_7_5": &utls.Hello360_7_5,
"360_11_0": &utls.Hello360_11_0,
"qq_auto": &utls.HelloQQ_Auto,
"qq_11_1": &utls.HelloQQ_11_1,
}
func nameToUtlsClientHelloID(name string) (*utls.ClientHelloID, error) {
clientHelloID, ok := clientHelloIDMap[name]
if !ok {
return nil, fmt.Errorf("unknown uTLS Client Hello ID: %s", name)
}
return clientHelloID, nil
}

View File

@ -1,42 +0,0 @@
package ws
import (
"bytes"
"github.com/gorilla/websocket"
"time"
)
type conn struct {
*websocket.Conn
readBuffer bytes.Buffer
}
func newConn(wsc *websocket.Conn) *conn {
return &conn{
Conn: wsc,
}
}
func (c *conn) Read(b []byte) (n int, err error) {
if c.readBuffer.Len() > 0 {
return c.readBuffer.Read(b)
}
_, msg, err := c.Conn.ReadMessage()
if err != nil {
return 0, err
}
n = copy(b, msg)
if n < len(msg) {
c.readBuffer.Write(msg[n:])
}
return n, nil
}
func (c *conn) Write(b []byte) (n int, err error) {
return len(b), c.Conn.WriteMessage(websocket.BinaryMessage, b)
}
func (c *conn) SetDeadline(t time.Time) error {
_ = c.Conn.SetReadDeadline(t)
return c.Conn.SetWriteDeadline(t)
}

View File

@ -1,10 +0,0 @@
package ws
import (
"github.com/daeuniverse/dae/component/outbound/dialer"
)
func init() {
dialer.FromLinkRegister("ws", NewWs)
dialer.FromLinkRegister("wss", NewWs)
}

View File

@ -1,108 +0,0 @@
package ws
import (
"crypto/tls"
"fmt"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/daeuniverse/dae/component/outbound/dialer"
"github.com/daeuniverse/outbound/netproxy"
"github.com/gorilla/websocket"
)
// Ws is a base Ws struct
type Ws struct {
dialer netproxy.Dialer
wsAddr string
header http.Header
tlsClientConfig *tls.Config
}
// NewWs returns a Ws infra.
func NewWs(option *dialer.GlobalOption, nextDialer netproxy.Dialer, link string) (netproxy.Dialer, *dialer.Property, error) {
u, err := url.Parse(link)
if err != nil {
return nil, nil, fmt.Errorf("NewWs: %w", err)
}
t := &Ws{
dialer: nextDialer,
}
query := u.Query()
host := query.Get("host")
if host == "" {
host = u.Hostname()
}
t.header = http.Header{}
t.header.Set("Host", host)
wsUrl := url.URL{
Scheme: u.Scheme,
Host: u.Host,
}
t.wsAddr = wsUrl.String() + u.Path
if u.Scheme == "wss" {
allowInsecure, _ := strconv.ParseBool(u.Query().Get("allowInsecure"))
if !allowInsecure {
allowInsecure, _ = strconv.ParseBool(u.Query().Get("allow_insecure"))
}
if !allowInsecure {
allowInsecure, _ = strconv.ParseBool(u.Query().Get("allowinsecure"))
}
if !allowInsecure {
allowInsecure, _ = strconv.ParseBool(u.Query().Get("skipVerify"))
}
// TODO: utls
t.tlsClientConfig = &tls.Config{
ServerName: query.Get("sni"),
InsecureSkipVerify: allowInsecure || option.AllowInsecure,
}
if len(query.Get("alpn")) > 0 {
t.tlsClientConfig.NextProtos = strings.Split(query.Get("alpn"), ",")
}
}
return t, &dialer.Property{
Name: u.Fragment,
Address: wsUrl.Host,
Protocol: u.Scheme,
Link: link,
}, nil
}
func (s *Ws) Dial(network, addr string) (c netproxy.Conn, err error) {
magicNetwork, err := netproxy.ParseMagicNetwork(network)
if err != nil {
return nil, err
}
switch magicNetwork.Network {
case "tcp":
wsDialer := &websocket.Dialer{
NetDial: func(_, addr string) (net.Conn, error) {
c, err := s.dialer.Dial(network, addr)
if err != nil {
return nil, err
}
return &netproxy.FakeNetConn{
Conn: c,
LAddr: nil,
RAddr: nil,
}, nil
},
TLSClientConfig: s.tlsClientConfig,
}
rc, _, err := wsDialer.Dial(s.wsAddr, s.header)
if err != nil {
return nil, fmt.Errorf("[Ws]: dial to %s: %w", s.wsAddr, err)
}
return newConn(rc), err
case "udp":
return nil, fmt.Errorf("%w: ws+udp", netproxy.UnsupportedTunnelTypeError)
default:
return nil, fmt.Errorf("%w: %v", netproxy.UnsupportedTunnelTypeError, network)
}
}

View File

@ -33,6 +33,7 @@ import (
"github.com/daeuniverse/dae/config"
"github.com/daeuniverse/dae/pkg/config_parser"
internal "github.com/daeuniverse/dae/pkg/ebpf_internal"
D "github.com/daeuniverse/outbound/dialer"
"github.com/daeuniverse/outbound/pool"
"github.com/daeuniverse/outbound/protocol/direct"
"github.com/daeuniverse/outbound/transport/grpc"
@ -224,24 +225,17 @@ func NewControlPlane(
log.Warnln("AllowInsecure is enabled, but it is not recommended. Please make sure you have to turn it on.")
}
option := &dialer.GlobalOption{
Log: log,
TcpCheckOptionRaw: dialer.TcpCheckOptionRaw{
Raw: global.TcpCheckUrl,
Log: log,
ResolverNetwork: common.MagicNetwork("udp", global.SoMarkFromDae),
Method: global.TcpCheckHttpMethod,
},
CheckDnsOptionRaw: dialer.CheckDnsOptionRaw{
Raw: global.UdpCheckDns,
ResolverNetwork: common.MagicNetwork("udp", global.SoMarkFromDae),
Somark: global.SoMarkFromDae,
ExtraOption: D.ExtraOption{
AllowInsecure: global.AllowInsecure,
TlsImplementation: global.TlsImplementation,
UtlsImitate: global.UtlsImitate,
},
Log: log,
TcpCheckOptionRaw: dialer.TcpCheckOptionRaw{Raw: global.TcpCheckUrl, Log: log, ResolverNetwork: common.MagicNetwork("udp", global.SoMarkFromDae), Method: global.TcpCheckHttpMethod},
CheckDnsOptionRaw: dialer.CheckDnsOptionRaw{Raw: global.UdpCheckDns, ResolverNetwork: common.MagicNetwork("udp", global.SoMarkFromDae), Somark: global.SoMarkFromDae},
CheckInterval: global.CheckInterval,
CheckTolerance: global.CheckTolerance,
CheckDnsTcp: true,
AllowInsecure: global.AllowInsecure,
TlsImplementation: global.TlsImplementation,
UtlsImitate: global.UtlsImitate,
}
// Dial mode.
@ -255,9 +249,9 @@ func NewControlPlane(
}
disableKernelAliveCallback := dialMode != consts.DialMode_Ip
_direct, directProperty := dialer.NewDirectDialer(option, true)
direct := dialer.NewDialer(_direct, option, dialer.InstanceOption{CheckEnabled: false}, directProperty)
direct := dialer.NewDialer(_direct, option, dialer.InstanceOption{DisableCheck: true}, directProperty)
_block, blockProperty := dialer.NewBlockDialer(option, func() { /*Dialer Outbound*/ })
block := dialer.NewDialer(_block, option, dialer.InstanceOption{CheckEnabled: false}, blockProperty)
block := dialer.NewDialer(_block, option, dialer.InstanceOption{DisableCheck: true}, blockProperty)
outbounds := []*outbound.DialerGroup{
outbound.NewDialerGroup(option, consts.OutboundDirect.String(),
[]*dialer.Dialer{direct}, []*dialer.Annotation{{}},

5
go.mod
View File

@ -11,11 +11,11 @@ require (
github.com/cilium/ebpf v0.11.0
github.com/daeuniverse/dae-config-dist/go/dae_config v0.0.0-20230604120805-1c27619b592d
github.com/daeuniverse/outbound v0.0.0-20240423150318-97fdbb427e02
github.com/dlclark/regexp2 v1.11.0
github.com/json-iterator/go v1.1.12
github.com/miekg/dns v1.1.55
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
github.com/okzk/sdnotify v0.0.0-20180710141335-d9becc38acbd
github.com/refraction-networking/utls v1.6.4
github.com/safchain/ethtool v0.3.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
@ -33,7 +33,7 @@ require (
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 // indirect
github.com/gorilla/websocket v1.5.0
github.com/gorilla/websocket v1.5.0 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/onsi/ginkgo/v2 v2.11.0 // indirect
github.com/stretchr/testify v1.8.1 // indirect
@ -65,6 +65,7 @@ require (
github.com/mzz2017/disk-bloom v1.0.1 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
github.com/quic-go/qpack v0.4.0 // indirect
github.com/refraction-networking/utls v1.6.4 // indirect
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/vishvananda/netns v0.0.4 // indirect

2
go.sum
View File

@ -29,6 +29,8 @@ github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFP
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/dgryski/go-rc2 v0.0.0-20150621095337-8a9021637152 h1:ED31mPIxDJnrLt9W9dH5xgd/6KjzEACKHBVGQ33czc0=
github.com/dgryski/go-rc2 v0.0.0-20150621095337-8a9021637152/go.mod h1:I9fhc/EvSg88cDxmfQ47v35Ssz9rlFunL/KY0A1JAYI=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/ebfe/rc2 v0.0.0-20131011165748-24b9757f5521 h1:fBHFH+Y/GPGFGo7LIrErQc3p2MeAhoIQNgaxPWYsSxk=
github.com/ebfe/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:ucvhdsUCE3TH0LoLRb6ShHiJl8e39dGlx6A4g/ujlow=
github.com/eknkc/basex v1.0.1 h1:TcyAkqh4oJXgV3WYyL4KEfCMk9W8oJCpmx1bo+jVgKY=