feat: support http and socks5 upstream node uris
This commit is contained in:
@@ -349,6 +349,18 @@ func buildNodeOutbound(tag, rawURI string, skipCertVerify bool) (option.Outbound
|
|||||||
return option.Outbound{}, fmt.Errorf("parse uri: %w", err)
|
return option.Outbound{}, fmt.Errorf("parse uri: %w", err)
|
||||||
}
|
}
|
||||||
switch strings.ToLower(parsed.Scheme) {
|
switch strings.ToLower(parsed.Scheme) {
|
||||||
|
case "http":
|
||||||
|
opts, err := buildHTTPOptions(parsed, skipCertVerify)
|
||||||
|
if err != nil {
|
||||||
|
return option.Outbound{}, err
|
||||||
|
}
|
||||||
|
return option.Outbound{Type: C.TypeHTTP, Tag: tag, Options: &opts}, nil
|
||||||
|
case "socks5":
|
||||||
|
opts, err := buildSOCKSOptions(parsed)
|
||||||
|
if err != nil {
|
||||||
|
return option.Outbound{}, err
|
||||||
|
}
|
||||||
|
return option.Outbound{Type: C.TypeSOCKS, Tag: tag, Options: &opts}, nil
|
||||||
case "vless":
|
case "vless":
|
||||||
opts, err := buildVLESSOptions(parsed, skipCertVerify)
|
opts, err := buildVLESSOptions(parsed, skipCertVerify)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -390,6 +402,64 @@ func buildNodeOutbound(tag, rawURI string, skipCertVerify bool) (option.Outbound
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildHTTPOptions(u *url.URL, skipCertVerify bool) (option.HTTPOutboundOptions, error) {
|
||||||
|
query := u.Query()
|
||||||
|
defaultPort := 80
|
||||||
|
if strings.EqualFold(query.Get("security"), "tls") {
|
||||||
|
defaultPort = 443
|
||||||
|
}
|
||||||
|
server, port, err := hostPort(u, defaultPort)
|
||||||
|
if err != nil {
|
||||||
|
return option.HTTPOutboundOptions{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := option.HTTPOutboundOptions{
|
||||||
|
ServerOptions: option.ServerOptions{Server: server, ServerPort: uint16(port)},
|
||||||
|
}
|
||||||
|
if u.User != nil {
|
||||||
|
opts.Username = u.User.Username()
|
||||||
|
if password, ok := u.User.Password(); ok {
|
||||||
|
opts.Password = password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if path := u.EscapedPath(); path != "" {
|
||||||
|
opts.Path = path
|
||||||
|
}
|
||||||
|
if host := strings.TrimSpace(query.Get("host")); host != "" {
|
||||||
|
opts.Headers = badoption.HTTPHeader{"Host": {host}}
|
||||||
|
}
|
||||||
|
if tlsOptions, err := buildTLSOptions(query, skipCertVerify); err != nil {
|
||||||
|
return option.HTTPOutboundOptions{}, err
|
||||||
|
} else if tlsOptions != nil {
|
||||||
|
opts.OutboundTLSOptionsContainer = option.OutboundTLSOptionsContainer{TLS: tlsOptions}
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSOCKSOptions(u *url.URL) (option.SOCKSOutboundOptions, error) {
|
||||||
|
server, port, err := hostPort(u, 1080)
|
||||||
|
if err != nil {
|
||||||
|
return option.SOCKSOutboundOptions{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := option.SOCKSOutboundOptions{
|
||||||
|
ServerOptions: option.ServerOptions{Server: server, ServerPort: uint16(port)},
|
||||||
|
Version: "5",
|
||||||
|
}
|
||||||
|
if u.User != nil {
|
||||||
|
opts.Username = u.User.Username()
|
||||||
|
if password, ok := u.User.Password(); ok {
|
||||||
|
opts.Password = password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if network := strings.ToLower(strings.TrimSpace(u.Query().Get("network"))); network != "" {
|
||||||
|
opts.Network = option.NetworkList(network)
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts, nil
|
||||||
|
}
|
||||||
|
|
||||||
func buildAnyTLSOptions(u *url.URL, skipCertVerify bool) (option.AnyTLSOutboundOptions, error) {
|
func buildAnyTLSOptions(u *url.URL, skipCertVerify bool) (option.AnyTLSOutboundOptions, error) {
|
||||||
password := ""
|
password := ""
|
||||||
if u.User != nil {
|
if u.User != nil {
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package builder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildNodeOutboundSupportsSOCKS5(t *testing.T) {
|
||||||
|
outbound, err := buildNodeOutbound("socks-node", "socks5://demo:secret@99.144.123.135:30350", false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("buildNodeOutbound returned error: %v", err)
|
||||||
|
}
|
||||||
|
if outbound.Type != C.TypeSOCKS {
|
||||||
|
t.Fatalf("outbound type = %q, want %q", outbound.Type, C.TypeSOCKS)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts, ok := outbound.Options.(*option.SOCKSOutboundOptions)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("outbound options type = %T, want *option.SOCKSOutboundOptions", outbound.Options)
|
||||||
|
}
|
||||||
|
if opts.Server != "99.144.123.135" {
|
||||||
|
t.Fatalf("server = %q, want %q", opts.Server, "99.144.123.135")
|
||||||
|
}
|
||||||
|
if opts.ServerPort != 30350 {
|
||||||
|
t.Fatalf("server port = %d, want %d", opts.ServerPort, 30350)
|
||||||
|
}
|
||||||
|
if opts.Username != "demo" {
|
||||||
|
t.Fatalf("username = %q, want %q", opts.Username, "demo")
|
||||||
|
}
|
||||||
|
if opts.Password != "secret" {
|
||||||
|
t.Fatalf("password = %q, want %q", opts.Password, "secret")
|
||||||
|
}
|
||||||
|
if opts.Version != "5" {
|
||||||
|
t.Fatalf("version = %q, want %q", opts.Version, "5")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildNodeOutboundSupportsHTTP(t *testing.T) {
|
||||||
|
outbound, err := buildNodeOutbound("http-node", "http://alice:wonderland@example.com:8080/proxy", false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("buildNodeOutbound returned error: %v", err)
|
||||||
|
}
|
||||||
|
if outbound.Type != C.TypeHTTP {
|
||||||
|
t.Fatalf("outbound type = %q, want %q", outbound.Type, C.TypeHTTP)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts, ok := outbound.Options.(*option.HTTPOutboundOptions)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("outbound options type = %T, want *option.HTTPOutboundOptions", outbound.Options)
|
||||||
|
}
|
||||||
|
if opts.Server != "example.com" {
|
||||||
|
t.Fatalf("server = %q, want %q", opts.Server, "example.com")
|
||||||
|
}
|
||||||
|
if opts.ServerPort != 8080 {
|
||||||
|
t.Fatalf("server port = %d, want %d", opts.ServerPort, 8080)
|
||||||
|
}
|
||||||
|
if opts.Username != "alice" {
|
||||||
|
t.Fatalf("username = %q, want %q", opts.Username, "alice")
|
||||||
|
}
|
||||||
|
if opts.Password != "wonderland" {
|
||||||
|
t.Fatalf("password = %q, want %q", opts.Password, "wonderland")
|
||||||
|
}
|
||||||
|
if opts.Path != "/proxy" {
|
||||||
|
t.Fatalf("path = %q, want %q", opts.Path, "/proxy")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,10 +78,10 @@ type MultiPortConfig struct {
|
|||||||
|
|
||||||
// ManagementConfig controls the monitoring HTTP endpoint.
|
// ManagementConfig controls the monitoring HTTP endpoint.
|
||||||
type ManagementConfig struct {
|
type ManagementConfig struct {
|
||||||
Enabled *bool `yaml:"enabled"`
|
Enabled *bool `yaml:"enabled"`
|
||||||
Listen string `yaml:"listen"`
|
Listen string `yaml:"listen"`
|
||||||
ProbeTarget string `yaml:"probe_target"`
|
ProbeTarget string `yaml:"probe_target"`
|
||||||
Password string `yaml:"password"` // WebUI 访问密码,为空则不需要密码
|
Password string `yaml:"password"` // WebUI 访问密码,为空则不需要密码
|
||||||
HealthCheckInterval time.Duration `yaml:"health_check_interval"`
|
HealthCheckInterval time.Duration `yaml:"health_check_interval"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +111,20 @@ const (
|
|||||||
InboundProtocolMixed = "mixed"
|
InboundProtocolMixed = "mixed"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var supportedProxyURISchemes = []string{
|
||||||
|
"vmess://",
|
||||||
|
"vless://",
|
||||||
|
"trojan://",
|
||||||
|
"ss://",
|
||||||
|
"ssr://",
|
||||||
|
"hysteria://",
|
||||||
|
"hysteria2://",
|
||||||
|
"hy2://",
|
||||||
|
"anytls://",
|
||||||
|
"http://",
|
||||||
|
"socks5://",
|
||||||
|
}
|
||||||
|
|
||||||
// NormalizeInboundProtocol normalizes inbound protocol aliases and validates the value.
|
// NormalizeInboundProtocol normalizes inbound protocol aliases and validates the value.
|
||||||
func NormalizeInboundProtocol(value string) (string, error) {
|
func NormalizeInboundProtocol(value string) (string, error) {
|
||||||
protocol := strings.ToLower(strings.TrimSpace(value))
|
protocol := strings.ToLower(strings.TrimSpace(value))
|
||||||
@@ -683,9 +697,9 @@ func isBase64(s string) bool {
|
|||||||
|
|
||||||
// IsProxyURI checks if a string is a valid proxy URI
|
// IsProxyURI checks if a string is a valid proxy URI
|
||||||
func IsProxyURI(s string) bool {
|
func IsProxyURI(s string) bool {
|
||||||
schemes := []string{"vmess://", "vless://", "trojan://", "ss://", "ssr://", "hysteria://", "hysteria2://", "hy2://", "anytls://"}
|
lower := strings.ToLower(strings.TrimSpace(s))
|
||||||
for _, scheme := range schemes {
|
for _, scheme := range supportedProxyURISchemes {
|
||||||
if strings.HasPrefix(strings.ToLower(s), scheme) {
|
if strings.HasPrefix(lower, scheme) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestIsProxyURIRecognizesHTTPAndSOCKS5(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
uri string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "http", uri: "http://alice:secret@example.com:8080", want: true},
|
||||||
|
{name: "socks5", uri: "socks5://alice:secret@example.com:1080", want: true},
|
||||||
|
{name: "vmess", uri: "vmess://example", want: true},
|
||||||
|
{name: "invalid", uri: "ftp://example.com", want: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
if got := IsProxyURI(tt.uri); got != tt.want {
|
||||||
|
t.Fatalf("%s: IsProxyURI(%q) = %v, want %v", tt.name, tt.uri, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -495,14 +495,16 @@ func extractHostFromURI(uri string) string {
|
|||||||
// Handle different URI schemes
|
// Handle different URI schemes
|
||||||
lowerURI := strings.ToLower(uri)
|
lowerURI := strings.ToLower(uri)
|
||||||
|
|
||||||
// vmess:// vless:// trojan:// ss:// ssr:// hysteria:// hysteria2:// hy2:// anytls://
|
// Standard URL formats, including HTTP and SOCKS upstream proxies.
|
||||||
if strings.HasPrefix(lowerURI, "vmess://") ||
|
if strings.HasPrefix(lowerURI, "vmess://") ||
|
||||||
strings.HasPrefix(lowerURI, "vless://") ||
|
strings.HasPrefix(lowerURI, "vless://") ||
|
||||||
strings.HasPrefix(lowerURI, "trojan://") ||
|
strings.HasPrefix(lowerURI, "trojan://") ||
|
||||||
strings.HasPrefix(lowerURI, "hysteria://") ||
|
strings.HasPrefix(lowerURI, "hysteria://") ||
|
||||||
strings.HasPrefix(lowerURI, "hysteria2://") ||
|
strings.HasPrefix(lowerURI, "hysteria2://") ||
|
||||||
strings.HasPrefix(lowerURI, "hy2://") ||
|
strings.HasPrefix(lowerURI, "hy2://") ||
|
||||||
strings.HasPrefix(lowerURI, "anytls://") {
|
strings.HasPrefix(lowerURI, "anytls://") ||
|
||||||
|
strings.HasPrefix(lowerURI, "http://") ||
|
||||||
|
strings.HasPrefix(lowerURI, "socks5://") {
|
||||||
// Standard URL format: scheme://user@host:port?params#fragment
|
// Standard URL format: scheme://user@host:port?params#fragment
|
||||||
parsed, err := url.Parse(uri)
|
parsed, err := url.Parse(uri)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package geoip
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestExtractHostFromURISupportsHTTPAndSOCKS5(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
uri string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{name: "http", uri: "http://alice:secret@example.com:8080", want: "example.com"},
|
||||||
|
{name: "socks5", uri: "socks5://alice:secret@99.144.123.135:30350", want: "99.144.123.135"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
if got := extractHostFromURI(tt.uri); got != tt.want {
|
||||||
|
t.Fatalf("%s: extractHostFromURI(%q) = %q, want %q", tt.name, tt.uri, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package store
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"easy_proxies/internal/config"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -175,26 +176,13 @@ func loadNodesFromLegacyFile(path string) ([]string, error) {
|
|||||||
if line == "" || strings.HasPrefix(line, "#") {
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if isProxyURI(line) {
|
if config.IsProxyURI(line) {
|
||||||
uris = append(uris, line)
|
uris = append(uris, line)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return uris, nil
|
return uris, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// isProxyURI checks if a string looks like a proxy URI.
|
|
||||||
func isProxyURI(s string) bool {
|
|
||||||
schemes := []string{"vmess://", "vless://", "trojan://", "ss://", "ssr://",
|
|
||||||
"hysteria://", "hysteria2://", "hy2://", "anytls://", "http://", "socks5://"}
|
|
||||||
lower := strings.ToLower(s)
|
|
||||||
for _, scheme := range schemes {
|
|
||||||
if strings.HasPrefix(lower, scheme) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractNameFromURI extracts a name from the URI fragment (#name).
|
// extractNameFromURI extracts a name from the URI fragment (#name).
|
||||||
func extractNameFromURI(uri string) string {
|
func extractNameFromURI(uri string) string {
|
||||||
parsed, err := url.Parse(uri)
|
parsed, err := url.Parse(uri)
|
||||||
|
|||||||
@@ -651,24 +651,13 @@ func parseNodesFromContent(content string) ([]config.NodeConfig, error) {
|
|||||||
if line == "" || strings.HasPrefix(line, "#") {
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if isProxyURI(line) {
|
if config.IsProxyURI(line) {
|
||||||
nodes = append(nodes, config.NodeConfig{URI: line})
|
nodes = append(nodes, config.NodeConfig{URI: line})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nodes, nil
|
return nodes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func isProxyURI(s string) bool {
|
|
||||||
schemes := []string{"vmess://", "vless://", "trojan://", "ss://", "ssr://", "hysteria://", "hysteria2://", "hy2://", "anytls://"}
|
|
||||||
lower := strings.ToLower(s)
|
|
||||||
for _, scheme := range schemes {
|
|
||||||
if strings.HasPrefix(lower, scheme) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type defaultLogger struct{}
|
type defaultLogger struct{}
|
||||||
|
|
||||||
func (defaultLogger) Infof(format string, args ...any) {
|
func (defaultLogger) Infof(format string, args ...any) {
|
||||||
|
|||||||
Reference in New Issue
Block a user