Style: code style
This commit is contained in:
177
adapter/adapter.go
Normal file
177
adapter/adapter.go
Normal file
@ -0,0 +1,177 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/common/queue"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
type Proxy struct {
|
||||
C.ProxyAdapter
|
||||
history *queue.Queue
|
||||
alive *atomic.Bool
|
||||
}
|
||||
|
||||
// Alive implements C.Proxy
|
||||
func (p *Proxy) Alive() bool {
|
||||
return p.alive.Load()
|
||||
}
|
||||
|
||||
// Dial implements C.Proxy
|
||||
func (p *Proxy) Dial(metadata *C.Metadata) (C.Conn, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
|
||||
defer cancel()
|
||||
return p.DialContext(ctx, metadata)
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (p *Proxy) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
conn, err := p.ProxyAdapter.DialContext(ctx, metadata)
|
||||
if err != nil {
|
||||
p.alive.Store(false)
|
||||
}
|
||||
return conn, err
|
||||
}
|
||||
|
||||
// DelayHistory implements C.Proxy
|
||||
func (p *Proxy) DelayHistory() []C.DelayHistory {
|
||||
queue := p.history.Copy()
|
||||
histories := []C.DelayHistory{}
|
||||
for _, item := range queue {
|
||||
histories = append(histories, item.(C.DelayHistory))
|
||||
}
|
||||
return histories
|
||||
}
|
||||
|
||||
// LastDelay return last history record. if proxy is not alive, return the max value of uint16.
|
||||
// implements C.Proxy
|
||||
func (p *Proxy) LastDelay() (delay uint16) {
|
||||
var max uint16 = 0xffff
|
||||
if !p.alive.Load() {
|
||||
return max
|
||||
}
|
||||
|
||||
last := p.history.Last()
|
||||
if last == nil {
|
||||
return max
|
||||
}
|
||||
history := last.(C.DelayHistory)
|
||||
if history.Delay == 0 {
|
||||
return max
|
||||
}
|
||||
return history.Delay
|
||||
}
|
||||
|
||||
// MarshalJSON implements C.ProxyAdapter
|
||||
func (p *Proxy) MarshalJSON() ([]byte, error) {
|
||||
inner, err := p.ProxyAdapter.MarshalJSON()
|
||||
if err != nil {
|
||||
return inner, err
|
||||
}
|
||||
|
||||
mapping := map[string]interface{}{}
|
||||
json.Unmarshal(inner, &mapping)
|
||||
mapping["history"] = p.DelayHistory()
|
||||
mapping["name"] = p.Name()
|
||||
return json.Marshal(mapping)
|
||||
}
|
||||
|
||||
// URLTest get the delay for the specified URL
|
||||
// implements C.Proxy
|
||||
func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
|
||||
defer func() {
|
||||
p.alive.Store(err == nil)
|
||||
record := C.DelayHistory{Time: time.Now()}
|
||||
if err == nil {
|
||||
record.Delay = t
|
||||
}
|
||||
p.history.Put(record)
|
||||
if p.history.Len() > 10 {
|
||||
p.history.Pop()
|
||||
}
|
||||
}()
|
||||
|
||||
addr, err := urlToMetadata(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
instance, err := p.DialContext(ctx, &addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer instance.Close()
|
||||
|
||||
req, err := http.NewRequest(http.MethodHead, url, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
transport := &http.Transport{
|
||||
Dial: func(string, string) (net.Conn, error) {
|
||||
return instance, nil
|
||||
},
|
||||
// from http.DefaultTransport
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
|
||||
client := http.Client{
|
||||
Transport: transport,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
t = uint16(time.Since(start) / time.Millisecond)
|
||||
return
|
||||
}
|
||||
|
||||
func NewProxy(adapter C.ProxyAdapter) *Proxy {
|
||||
return &Proxy{adapter, queue.New(10), atomic.NewBool(true)}
|
||||
}
|
||||
|
||||
func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
port := u.Port()
|
||||
if port == "" {
|
||||
switch u.Scheme {
|
||||
case "https":
|
||||
port = "443"
|
||||
case "http":
|
||||
port = "80"
|
||||
default:
|
||||
err = fmt.Errorf("%s scheme not Support", rawURL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
addr = C.Metadata{
|
||||
AddrType: C.AtypDomainName,
|
||||
Host: u.Hostname(),
|
||||
DstIP: nil,
|
||||
DstPort: port,
|
||||
}
|
||||
return
|
||||
}
|
61
adapter/inbound/http.go
Normal file
61
adapter/inbound/http.go
Normal file
@ -0,0 +1,61 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/context"
|
||||
)
|
||||
|
||||
// NewHTTP receive normal http request and return HTTPContext
|
||||
func NewHTTP(request *http.Request, conn net.Conn) *context.HTTPContext {
|
||||
metadata := parseHTTPAddr(request)
|
||||
metadata.Type = C.HTTP
|
||||
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
|
||||
metadata.SrcIP = ip
|
||||
metadata.SrcPort = port
|
||||
}
|
||||
return context.NewHTTPContext(conn, request, metadata)
|
||||
}
|
||||
|
||||
// RemoveHopByHopHeaders remove hop-by-hop header
|
||||
func RemoveHopByHopHeaders(header http.Header) {
|
||||
// Strip hop-by-hop header based on RFC:
|
||||
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.5.1
|
||||
// https://www.mnot.net/blog/2011/07/11/what_proxies_must_do
|
||||
|
||||
header.Del("Proxy-Connection")
|
||||
header.Del("Proxy-Authenticate")
|
||||
header.Del("Proxy-Authorization")
|
||||
header.Del("TE")
|
||||
header.Del("Trailers")
|
||||
header.Del("Transfer-Encoding")
|
||||
header.Del("Upgrade")
|
||||
|
||||
connections := header.Get("Connection")
|
||||
header.Del("Connection")
|
||||
if len(connections) == 0 {
|
||||
return
|
||||
}
|
||||
for _, h := range strings.Split(connections, ",") {
|
||||
header.Del(strings.TrimSpace(h))
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveExtraHTTPHostPort remove extra host port (example.com:80 --> example.com)
|
||||
// It resolves the behavior of some HTTP servers that do not handle host:80 (e.g. baidu.com)
|
||||
func RemoveExtraHTTPHostPort(req *http.Request) {
|
||||
host := req.Host
|
||||
if host == "" {
|
||||
host = req.URL.Host
|
||||
}
|
||||
|
||||
if pHost, port, err := net.SplitHostPort(host); err == nil && port == "80" {
|
||||
host = pHost
|
||||
}
|
||||
|
||||
req.Host = host
|
||||
req.URL.Host = host
|
||||
}
|
20
adapter/inbound/https.go
Normal file
20
adapter/inbound/https.go
Normal file
@ -0,0 +1,20 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/context"
|
||||
)
|
||||
|
||||
// NewHTTPS receive CONNECT request and return ConnContext
|
||||
func NewHTTPS(request *http.Request, conn net.Conn) *context.ConnContext {
|
||||
metadata := parseHTTPAddr(request)
|
||||
metadata.Type = C.HTTPCONNECT
|
||||
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
|
||||
metadata.SrcIP = ip
|
||||
metadata.SrcPort = port
|
||||
}
|
||||
return context.NewConnContext(conn, metadata)
|
||||
}
|
33
adapter/inbound/packet.go
Normal file
33
adapter/inbound/packet.go
Normal file
@ -0,0 +1,33 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/transport/socks5"
|
||||
)
|
||||
|
||||
// PacketAdapter is a UDP Packet adapter for socks/redir/tun
|
||||
type PacketAdapter struct {
|
||||
C.UDPPacket
|
||||
metadata *C.Metadata
|
||||
}
|
||||
|
||||
// Metadata returns destination metadata
|
||||
func (s *PacketAdapter) Metadata() *C.Metadata {
|
||||
return s.metadata
|
||||
}
|
||||
|
||||
// NewPacket is PacketAdapter generator
|
||||
func NewPacket(target socks5.Addr, packet C.UDPPacket, source C.Type) *PacketAdapter {
|
||||
metadata := parseSocksAddr(target)
|
||||
metadata.NetWork = C.UDP
|
||||
metadata.Type = source
|
||||
if ip, port, err := parseAddr(packet.LocalAddr().String()); err == nil {
|
||||
metadata.SrcIP = ip
|
||||
metadata.SrcPort = port
|
||||
}
|
||||
|
||||
return &PacketAdapter{
|
||||
UDPPacket: packet,
|
||||
metadata: metadata,
|
||||
}
|
||||
}
|
22
adapter/inbound/socket.go
Normal file
22
adapter/inbound/socket.go
Normal file
@ -0,0 +1,22 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/context"
|
||||
"github.com/Dreamacro/clash/transport/socks5"
|
||||
)
|
||||
|
||||
// NewSocket receive TCP inbound and return ConnContext
|
||||
func NewSocket(target socks5.Addr, conn net.Conn, source C.Type) *context.ConnContext {
|
||||
metadata := parseSocksAddr(target)
|
||||
metadata.NetWork = C.TCP
|
||||
metadata.Type = source
|
||||
if ip, port, err := parseAddr(conn.RemoteAddr().String()); err == nil {
|
||||
metadata.SrcIP = ip
|
||||
metadata.SrcPort = port
|
||||
}
|
||||
|
||||
return context.NewConnContext(conn, metadata)
|
||||
}
|
76
adapter/inbound/util.go
Normal file
76
adapter/inbound/util.go
Normal file
@ -0,0 +1,76 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/transport/socks5"
|
||||
)
|
||||
|
||||
func parseSocksAddr(target socks5.Addr) *C.Metadata {
|
||||
metadata := &C.Metadata{
|
||||
AddrType: int(target[0]),
|
||||
}
|
||||
|
||||
switch target[0] {
|
||||
case socks5.AtypDomainName:
|
||||
// trim for FQDN
|
||||
metadata.Host = strings.TrimRight(string(target[2:2+target[1]]), ".")
|
||||
metadata.DstPort = strconv.Itoa((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
|
||||
case socks5.AtypIPv4:
|
||||
ip := net.IP(target[1 : 1+net.IPv4len])
|
||||
metadata.DstIP = ip
|
||||
metadata.DstPort = strconv.Itoa((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1]))
|
||||
case socks5.AtypIPv6:
|
||||
ip := net.IP(target[1 : 1+net.IPv6len])
|
||||
metadata.DstIP = ip
|
||||
metadata.DstPort = strconv.Itoa((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1]))
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
func parseHTTPAddr(request *http.Request) *C.Metadata {
|
||||
host := request.URL.Hostname()
|
||||
port := request.URL.Port()
|
||||
if port == "" {
|
||||
port = "80"
|
||||
}
|
||||
|
||||
// trim FQDN (#737)
|
||||
host = strings.TrimRight(host, ".")
|
||||
|
||||
metadata := &C.Metadata{
|
||||
NetWork: C.TCP,
|
||||
AddrType: C.AtypDomainName,
|
||||
Host: host,
|
||||
DstIP: nil,
|
||||
DstPort: port,
|
||||
}
|
||||
|
||||
ip := net.ParseIP(host)
|
||||
if ip != nil {
|
||||
switch {
|
||||
case ip.To4() == nil:
|
||||
metadata.AddrType = C.AtypIPv6
|
||||
default:
|
||||
metadata.AddrType = C.AtypIPv4
|
||||
}
|
||||
metadata.DstIP = ip
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
func parseAddr(addr string) (net.IP, string, error) {
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
ip := net.ParseIP(host)
|
||||
return ip, port, nil
|
||||
}
|
100
adapter/outbound/base.go
Normal file
100
adapter/outbound/base.go
Normal file
@ -0,0 +1,100 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type Base struct {
|
||||
name string
|
||||
addr string
|
||||
tp C.AdapterType
|
||||
udp bool
|
||||
}
|
||||
|
||||
// Name implements C.ProxyAdapter
|
||||
func (b *Base) Name() string {
|
||||
return b.name
|
||||
}
|
||||
|
||||
// Type implements C.ProxyAdapter
|
||||
func (b *Base) Type() C.AdapterType {
|
||||
return b.tp
|
||||
}
|
||||
|
||||
// StreamConn implements C.ProxyAdapter
|
||||
func (b *Base) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||
return c, errors.New("no support")
|
||||
}
|
||||
|
||||
// DialUDP implements C.ProxyAdapter
|
||||
func (b *Base) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||
return nil, errors.New("no support")
|
||||
}
|
||||
|
||||
// SupportUDP implements C.ProxyAdapter
|
||||
func (b *Base) SupportUDP() bool {
|
||||
return b.udp
|
||||
}
|
||||
|
||||
// MarshalJSON implements C.ProxyAdapter
|
||||
func (b *Base) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]string{
|
||||
"type": b.Type().String(),
|
||||
})
|
||||
}
|
||||
|
||||
// Addr implements C.ProxyAdapter
|
||||
func (b *Base) Addr() string {
|
||||
return b.addr
|
||||
}
|
||||
|
||||
// Unwrap implements C.ProxyAdapter
|
||||
func (b *Base) Unwrap(metadata *C.Metadata) C.Proxy {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewBase(name string, addr string, tp C.AdapterType, udp bool) *Base {
|
||||
return &Base{name, addr, tp, udp}
|
||||
}
|
||||
|
||||
type conn struct {
|
||||
net.Conn
|
||||
chain C.Chain
|
||||
}
|
||||
|
||||
// Chains implements C.Connection
|
||||
func (c *conn) Chains() C.Chain {
|
||||
return c.chain
|
||||
}
|
||||
|
||||
// AppendToChains implements C.Connection
|
||||
func (c *conn) AppendToChains(a C.ProxyAdapter) {
|
||||
c.chain = append(c.chain, a.Name())
|
||||
}
|
||||
|
||||
func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
|
||||
return &conn{c, []string{a.Name()}}
|
||||
}
|
||||
|
||||
type packetConn struct {
|
||||
net.PacketConn
|
||||
chain C.Chain
|
||||
}
|
||||
|
||||
// Chains implements C.Connection
|
||||
func (c *packetConn) Chains() C.Chain {
|
||||
return c.chain
|
||||
}
|
||||
|
||||
// AppendToChains implements C.Connection
|
||||
func (c *packetConn) AppendToChains(a C.ProxyAdapter) {
|
||||
c.chain = append(c.chain, a.Name())
|
||||
}
|
||||
|
||||
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
|
||||
return &packetConn{pc, []string{a.Name()}}
|
||||
}
|
48
adapter/outbound/direct.go
Normal file
48
adapter/outbound/direct.go
Normal file
@ -0,0 +1,48 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type Direct struct {
|
||||
*Base
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (d *Direct) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
address := net.JoinHostPort(metadata.String(), metadata.DstPort)
|
||||
|
||||
c, err := dialer.DialContext(ctx, "tcp", address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
return NewConn(c, d), nil
|
||||
}
|
||||
|
||||
// DialUDP implements C.ProxyAdapter
|
||||
func (d *Direct) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||
pc, err := dialer.ListenPacket("udp", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newPacketConn(&directPacketConn{pc}, d), nil
|
||||
}
|
||||
|
||||
type directPacketConn struct {
|
||||
net.PacketConn
|
||||
}
|
||||
|
||||
func NewDirect() *Direct {
|
||||
return &Direct{
|
||||
Base: &Base{
|
||||
name: "DIRECT",
|
||||
tp: C.Direct,
|
||||
udp: true,
|
||||
},
|
||||
}
|
||||
}
|
143
adapter/outbound/http.go
Normal file
143
adapter/outbound/http.go
Normal file
@ -0,0 +1,143 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type Http struct {
|
||||
*Base
|
||||
user string
|
||||
pass string
|
||||
tlsConfig *tls.Config
|
||||
}
|
||||
|
||||
type HttpOption struct {
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
UserName string `proxy:"username,omitempty"`
|
||||
Password string `proxy:"password,omitempty"`
|
||||
TLS bool `proxy:"tls,omitempty"`
|
||||
SNI string `proxy:"sni,omitempty"`
|
||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||
}
|
||||
|
||||
// StreamConn implements C.ProxyAdapter
|
||||
func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||
if h.tlsConfig != nil {
|
||||
cc := tls.Client(c, h.tlsConfig)
|
||||
err := cc.Handshake()
|
||||
c = cc
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.shakeHand(metadata, c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||
c, err := dialer.DialContext(ctx, "tcp", h.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
|
||||
defer safeConnClose(c, err)
|
||||
|
||||
c, err = h.StreamConn(c, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewConn(c, h), nil
|
||||
}
|
||||
|
||||
func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
|
||||
addr := metadata.RemoteAddress()
|
||||
req := &http.Request{
|
||||
Method: http.MethodConnect,
|
||||
URL: &url.URL{
|
||||
Host: addr,
|
||||
},
|
||||
Host: addr,
|
||||
Header: http.Header{
|
||||
"Proxy-Connection": []string{"Keep-Alive"},
|
||||
},
|
||||
}
|
||||
|
||||
if h.user != "" && h.pass != "" {
|
||||
auth := h.user + ":" + h.pass
|
||||
req.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
|
||||
}
|
||||
|
||||
if err := req.Write(rw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := http.ReadResponse(bufio.NewReader(rw), req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusProxyAuthRequired {
|
||||
return errors.New("HTTP need auth")
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusMethodNotAllowed {
|
||||
return errors.New("CONNECT method not allowed by proxy")
|
||||
}
|
||||
|
||||
if resp.StatusCode >= http.StatusInternalServerError {
|
||||
return errors.New(resp.Status)
|
||||
}
|
||||
|
||||
return fmt.Errorf("can not connect remote err code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
func NewHttp(option HttpOption) *Http {
|
||||
var tlsConfig *tls.Config
|
||||
if option.TLS {
|
||||
sni := option.Server
|
||||
if option.SNI != "" {
|
||||
sni = option.SNI
|
||||
}
|
||||
tlsConfig = &tls.Config{
|
||||
InsecureSkipVerify: option.SkipCertVerify,
|
||||
ClientSessionCache: getClientSessionCache(),
|
||||
ServerName: sni,
|
||||
}
|
||||
}
|
||||
|
||||
return &Http{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
||||
tp: C.Http,
|
||||
},
|
||||
user: option.UserName,
|
||||
pass: option.Password,
|
||||
tlsConfig: tlsConfig,
|
||||
}
|
||||
}
|
63
adapter/outbound/reject.go
Normal file
63
adapter/outbound/reject.go
Normal file
@ -0,0 +1,63 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type Reject struct {
|
||||
*Base
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
return NewConn(&NopConn{}, r), nil
|
||||
}
|
||||
|
||||
// DialUDP implements C.ProxyAdapter
|
||||
func (r *Reject) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||
return nil, errors.New("match reject rule")
|
||||
}
|
||||
|
||||
func NewReject() *Reject {
|
||||
return &Reject{
|
||||
Base: &Base{
|
||||
name: "REJECT",
|
||||
tp: C.Reject,
|
||||
udp: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type NopConn struct{}
|
||||
|
||||
func (rw *NopConn) Read(b []byte) (int, error) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
func (rw *NopConn) Write(b []byte) (int, error) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
// Close is fake function for net.Conn
|
||||
func (rw *NopConn) Close() error { return nil }
|
||||
|
||||
// LocalAddr is fake function for net.Conn
|
||||
func (rw *NopConn) LocalAddr() net.Addr { return nil }
|
||||
|
||||
// RemoteAddr is fake function for net.Conn
|
||||
func (rw *NopConn) RemoteAddr() net.Addr { return nil }
|
||||
|
||||
// SetDeadline is fake function for net.Conn
|
||||
func (rw *NopConn) SetDeadline(time.Time) error { return nil }
|
||||
|
||||
// SetReadDeadline is fake function for net.Conn
|
||||
func (rw *NopConn) SetReadDeadline(time.Time) error { return nil }
|
||||
|
||||
// SetWriteDeadline is fake function for net.Conn
|
||||
func (rw *NopConn) SetWriteDeadline(time.Time) error { return nil }
|
202
adapter/outbound/shadowsocks.go
Normal file
202
adapter/outbound/shadowsocks.go
Normal file
@ -0,0 +1,202 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/Dreamacro/clash/common/structure"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
obfs "github.com/Dreamacro/clash/transport/simple-obfs"
|
||||
"github.com/Dreamacro/clash/transport/socks5"
|
||||
v2rayObfs "github.com/Dreamacro/clash/transport/v2ray-plugin"
|
||||
|
||||
"github.com/Dreamacro/go-shadowsocks2/core"
|
||||
)
|
||||
|
||||
type ShadowSocks struct {
|
||||
*Base
|
||||
cipher core.Cipher
|
||||
|
||||
// obfs
|
||||
obfsMode string
|
||||
obfsOption *simpleObfsOption
|
||||
v2rayOption *v2rayObfs.Option
|
||||
}
|
||||
|
||||
type ShadowSocksOption struct {
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
Password string `proxy:"password"`
|
||||
Cipher string `proxy:"cipher"`
|
||||
UDP bool `proxy:"udp,omitempty"`
|
||||
Plugin string `proxy:"plugin,omitempty"`
|
||||
PluginOpts map[string]interface{} `proxy:"plugin-opts,omitempty"`
|
||||
}
|
||||
|
||||
type simpleObfsOption struct {
|
||||
Mode string `obfs:"mode,omitempty"`
|
||||
Host string `obfs:"host,omitempty"`
|
||||
}
|
||||
|
||||
type v2rayObfsOption struct {
|
||||
Mode string `obfs:"mode"`
|
||||
Host string `obfs:"host,omitempty"`
|
||||
Path string `obfs:"path,omitempty"`
|
||||
TLS bool `obfs:"tls,omitempty"`
|
||||
Headers map[string]string `obfs:"headers,omitempty"`
|
||||
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
|
||||
Mux bool `obfs:"mux,omitempty"`
|
||||
}
|
||||
|
||||
// StreamConn implements C.ProxyAdapter
|
||||
func (ss *ShadowSocks) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||
switch ss.obfsMode {
|
||||
case "tls":
|
||||
c = obfs.NewTLSObfs(c, ss.obfsOption.Host)
|
||||
case "http":
|
||||
_, port, _ := net.SplitHostPort(ss.addr)
|
||||
c = obfs.NewHTTPObfs(c, ss.obfsOption.Host, port)
|
||||
case "websocket":
|
||||
var err error
|
||||
c, err = v2rayObfs.NewV2rayObfs(c, ss.v2rayOption)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||
}
|
||||
}
|
||||
c = ss.cipher.StreamConn(c)
|
||||
_, err := c.Write(serializesSocksAddr(metadata))
|
||||
return c, err
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (ss *ShadowSocks) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
|
||||
defer safeConnClose(c, err)
|
||||
|
||||
c, err = ss.StreamConn(c, metadata)
|
||||
return NewConn(c, ss), err
|
||||
}
|
||||
|
||||
// DialUDP implements C.ProxyAdapter
|
||||
func (ss *ShadowSocks) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||
pc, err := dialer.ListenPacket("udp", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addr, err := resolveUDPAddr("udp", ss.addr)
|
||||
if err != nil {
|
||||
pc.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pc = ss.cipher.PacketConn(pc)
|
||||
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ss), nil
|
||||
}
|
||||
|
||||
func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||
cipher := option.Cipher
|
||||
password := option.Password
|
||||
ciph, err := core.PickCipher(cipher, nil, password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ss %s initialize error: %w", addr, err)
|
||||
}
|
||||
|
||||
var v2rayOption *v2rayObfs.Option
|
||||
var obfsOption *simpleObfsOption
|
||||
obfsMode := ""
|
||||
|
||||
decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true})
|
||||
if option.Plugin == "obfs" {
|
||||
opts := simpleObfsOption{Host: "bing.com"}
|
||||
if err := decoder.Decode(option.PluginOpts, &opts); err != nil {
|
||||
return nil, fmt.Errorf("ss %s initialize obfs error: %w", addr, err)
|
||||
}
|
||||
|
||||
if opts.Mode != "tls" && opts.Mode != "http" {
|
||||
return nil, fmt.Errorf("ss %s obfs mode error: %s", addr, opts.Mode)
|
||||
}
|
||||
obfsMode = opts.Mode
|
||||
obfsOption = &opts
|
||||
} else if option.Plugin == "v2ray-plugin" {
|
||||
opts := v2rayObfsOption{Host: "bing.com", Mux: true}
|
||||
if err := decoder.Decode(option.PluginOpts, &opts); err != nil {
|
||||
return nil, fmt.Errorf("ss %s initialize v2ray-plugin error: %w", addr, err)
|
||||
}
|
||||
|
||||
if opts.Mode != "websocket" {
|
||||
return nil, fmt.Errorf("ss %s obfs mode error: %s", addr, opts.Mode)
|
||||
}
|
||||
obfsMode = opts.Mode
|
||||
v2rayOption = &v2rayObfs.Option{
|
||||
Host: opts.Host,
|
||||
Path: opts.Path,
|
||||
Headers: opts.Headers,
|
||||
Mux: opts.Mux,
|
||||
}
|
||||
|
||||
if opts.TLS {
|
||||
v2rayOption.TLS = true
|
||||
v2rayOption.SkipCertVerify = opts.SkipCertVerify
|
||||
v2rayOption.SessionCache = getClientSessionCache()
|
||||
}
|
||||
}
|
||||
|
||||
return &ShadowSocks{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
addr: addr,
|
||||
tp: C.Shadowsocks,
|
||||
udp: option.UDP,
|
||||
},
|
||||
cipher: ciph,
|
||||
|
||||
obfsMode: obfsMode,
|
||||
v2rayOption: v2rayOption,
|
||||
obfsOption: obfsOption,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type ssPacketConn struct {
|
||||
net.PacketConn
|
||||
rAddr net.Addr
|
||||
}
|
||||
|
||||
func (spc *ssPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
|
||||
packet, err := socks5.EncodeUDPPacket(socks5.ParseAddrToSocksAddr(addr), b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return spc.PacketConn.WriteTo(packet[3:], spc.rAddr)
|
||||
}
|
||||
|
||||
func (spc *ssPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
||||
n, _, e := spc.PacketConn.ReadFrom(b)
|
||||
if e != nil {
|
||||
return 0, nil, e
|
||||
}
|
||||
|
||||
addr := socks5.SplitAddr(b[:n])
|
||||
if addr == nil {
|
||||
return 0, nil, errors.New("parse addr error")
|
||||
}
|
||||
|
||||
udpAddr := addr.UDPAddr()
|
||||
if udpAddr == nil {
|
||||
return 0, nil, errors.New("parse addr error")
|
||||
}
|
||||
|
||||
copy(b, b[len(addr):])
|
||||
return n - len(addr), udpAddr, e
|
||||
}
|
148
adapter/outbound/shadowsocksr.go
Normal file
148
adapter/outbound/shadowsocksr.go
Normal file
@ -0,0 +1,148 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/transport/ssr/obfs"
|
||||
"github.com/Dreamacro/clash/transport/ssr/protocol"
|
||||
|
||||
"github.com/Dreamacro/go-shadowsocks2/core"
|
||||
"github.com/Dreamacro/go-shadowsocks2/shadowaead"
|
||||
"github.com/Dreamacro/go-shadowsocks2/shadowstream"
|
||||
)
|
||||
|
||||
type ShadowSocksR struct {
|
||||
*Base
|
||||
cipher core.Cipher
|
||||
obfs obfs.Obfs
|
||||
protocol protocol.Protocol
|
||||
}
|
||||
|
||||
type ShadowSocksROption struct {
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
Password string `proxy:"password"`
|
||||
Cipher string `proxy:"cipher"`
|
||||
Obfs string `proxy:"obfs"`
|
||||
ObfsParam string `proxy:"obfs-param,omitempty"`
|
||||
Protocol string `proxy:"protocol"`
|
||||
ProtocolParam string `proxy:"protocol-param,omitempty"`
|
||||
UDP bool `proxy:"udp,omitempty"`
|
||||
}
|
||||
|
||||
// StreamConn implements C.ProxyAdapter
|
||||
func (ssr *ShadowSocksR) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||
c = ssr.obfs.StreamConn(c)
|
||||
c = ssr.cipher.StreamConn(c)
|
||||
var (
|
||||
iv []byte
|
||||
err error
|
||||
)
|
||||
switch conn := c.(type) {
|
||||
case *shadowstream.Conn:
|
||||
iv, err = conn.ObtainWriteIV()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case *shadowaead.Conn:
|
||||
return nil, fmt.Errorf("invalid connection type")
|
||||
}
|
||||
c = ssr.protocol.StreamConn(c, iv)
|
||||
_, err = c.Write(serializesSocksAddr(metadata))
|
||||
return c, err
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (ssr *ShadowSocksR) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||
c, err := dialer.DialContext(ctx, "tcp", ssr.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", ssr.addr, err)
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
|
||||
defer safeConnClose(c, err)
|
||||
|
||||
c, err = ssr.StreamConn(c, metadata)
|
||||
return NewConn(c, ssr), err
|
||||
}
|
||||
|
||||
// DialUDP implements C.ProxyAdapter
|
||||
func (ssr *ShadowSocksR) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||
pc, err := dialer.ListenPacket("udp", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addr, err := resolveUDPAddr("udp", ssr.addr)
|
||||
if err != nil {
|
||||
pc.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pc = ssr.cipher.PacketConn(pc)
|
||||
pc = ssr.protocol.PacketConn(pc)
|
||||
return newPacketConn(&ssPacketConn{PacketConn: pc, rAddr: addr}, ssr), nil
|
||||
}
|
||||
|
||||
func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) {
|
||||
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||
cipher := option.Cipher
|
||||
password := option.Password
|
||||
coreCiph, err := core.PickCipher(cipher, nil, password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ssr %s initialize error: %w", addr, err)
|
||||
}
|
||||
var (
|
||||
ivSize int
|
||||
key []byte
|
||||
)
|
||||
if option.Cipher == "dummy" {
|
||||
ivSize = 0
|
||||
key = core.Kdf(option.Password, 16)
|
||||
} else {
|
||||
ciph, ok := coreCiph.(*core.StreamCipher)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s is not dummy or a supported stream cipher in ssr", cipher)
|
||||
}
|
||||
ivSize = ciph.IVSize()
|
||||
key = ciph.Key
|
||||
}
|
||||
|
||||
obfs, obfsOverhead, err := obfs.PickObfs(option.Obfs, &obfs.Base{
|
||||
Host: option.Server,
|
||||
Port: option.Port,
|
||||
Key: key,
|
||||
IVSize: ivSize,
|
||||
Param: option.ObfsParam,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ssr %s initialize obfs error: %w", addr, err)
|
||||
}
|
||||
|
||||
protocol, err := protocol.PickProtocol(option.Protocol, &protocol.Base{
|
||||
Key: key,
|
||||
Overhead: obfsOverhead,
|
||||
Param: option.ProtocolParam,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ssr %s initialize protocol error: %w", addr, err)
|
||||
}
|
||||
|
||||
return &ShadowSocksR{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
addr: addr,
|
||||
tp: C.ShadowsocksR,
|
||||
udp: option.UDP,
|
||||
},
|
||||
cipher: coreCiph,
|
||||
obfs: obfs,
|
||||
protocol: protocol,
|
||||
}, nil
|
||||
}
|
135
adapter/outbound/snell.go
Normal file
135
adapter/outbound/snell.go
Normal file
@ -0,0 +1,135 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/Dreamacro/clash/common/structure"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
obfs "github.com/Dreamacro/clash/transport/simple-obfs"
|
||||
"github.com/Dreamacro/clash/transport/snell"
|
||||
)
|
||||
|
||||
type Snell struct {
|
||||
*Base
|
||||
psk []byte
|
||||
pool *snell.Pool
|
||||
obfsOption *simpleObfsOption
|
||||
version int
|
||||
}
|
||||
|
||||
type SnellOption struct {
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
Psk string `proxy:"psk"`
|
||||
Version int `proxy:"version,omitempty"`
|
||||
ObfsOpts map[string]interface{} `proxy:"obfs-opts,omitempty"`
|
||||
}
|
||||
|
||||
type streamOption struct {
|
||||
psk []byte
|
||||
version int
|
||||
addr string
|
||||
obfsOption *simpleObfsOption
|
||||
}
|
||||
|
||||
func streamConn(c net.Conn, option streamOption) *snell.Snell {
|
||||
switch option.obfsOption.Mode {
|
||||
case "tls":
|
||||
c = obfs.NewTLSObfs(c, option.obfsOption.Host)
|
||||
case "http":
|
||||
_, port, _ := net.SplitHostPort(option.addr)
|
||||
c = obfs.NewHTTPObfs(c, option.obfsOption.Host, port)
|
||||
}
|
||||
return snell.StreamConn(c, option.psk, option.version)
|
||||
}
|
||||
|
||||
// StreamConn implements C.ProxyAdapter
|
||||
func (s *Snell) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
|
||||
port, _ := strconv.Atoi(metadata.DstPort)
|
||||
err := snell.WriteHeader(c, metadata.String(), uint(port), s.version)
|
||||
return c, err
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||
if s.version == snell.Version2 {
|
||||
c, err := s.pool.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
port, _ := strconv.Atoi(metadata.DstPort)
|
||||
if err = snell.WriteHeader(c, metadata.String(), uint(port), s.version); err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
return NewConn(c, s), err
|
||||
}
|
||||
|
||||
c, err := dialer.DialContext(ctx, "tcp", s.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", s.addr, err)
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
|
||||
defer safeConnClose(c, err)
|
||||
|
||||
c, err = s.StreamConn(c, metadata)
|
||||
return NewConn(c, s), err
|
||||
}
|
||||
|
||||
func NewSnell(option SnellOption) (*Snell, error) {
|
||||
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||
psk := []byte(option.Psk)
|
||||
|
||||
decoder := structure.NewDecoder(structure.Option{TagName: "obfs", WeaklyTypedInput: true})
|
||||
obfsOption := &simpleObfsOption{Host: "bing.com"}
|
||||
if err := decoder.Decode(option.ObfsOpts, obfsOption); err != nil {
|
||||
return nil, fmt.Errorf("snell %s initialize obfs error: %w", addr, err)
|
||||
}
|
||||
|
||||
switch obfsOption.Mode {
|
||||
case "tls", "http", "":
|
||||
break
|
||||
default:
|
||||
return nil, fmt.Errorf("snell %s obfs mode error: %s", addr, obfsOption.Mode)
|
||||
}
|
||||
|
||||
// backward compatible
|
||||
if option.Version == 0 {
|
||||
option.Version = snell.DefaultSnellVersion
|
||||
}
|
||||
if option.Version != snell.Version1 && option.Version != snell.Version2 {
|
||||
return nil, fmt.Errorf("snell version error: %d", option.Version)
|
||||
}
|
||||
|
||||
s := &Snell{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
addr: addr,
|
||||
tp: C.Snell,
|
||||
},
|
||||
psk: psk,
|
||||
obfsOption: obfsOption,
|
||||
version: option.Version,
|
||||
}
|
||||
|
||||
if option.Version == snell.Version2 {
|
||||
s.pool = snell.NewPool(func(ctx context.Context) (*snell.Snell, error) {
|
||||
c, err := dialer.DialContext(ctx, "tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tcpKeepAlive(c)
|
||||
return streamConn(c, streamOption{psk, option.Version, addr, obfsOption}), nil
|
||||
})
|
||||
}
|
||||
return s, nil
|
||||
}
|
205
adapter/outbound/socks5.go
Normal file
205
adapter/outbound/socks5.go
Normal file
@ -0,0 +1,205 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/transport/socks5"
|
||||
)
|
||||
|
||||
type Socks5 struct {
|
||||
*Base
|
||||
user string
|
||||
pass string
|
||||
tls bool
|
||||
skipCertVerify bool
|
||||
tlsConfig *tls.Config
|
||||
}
|
||||
|
||||
type Socks5Option struct {
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
UserName string `proxy:"username,omitempty"`
|
||||
Password string `proxy:"password,omitempty"`
|
||||
TLS bool `proxy:"tls,omitempty"`
|
||||
UDP bool `proxy:"udp,omitempty"`
|
||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||
}
|
||||
|
||||
// StreamConn implements C.ProxyAdapter
|
||||
func (ss *Socks5) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||
if ss.tls {
|
||||
cc := tls.Client(c, ss.tlsConfig)
|
||||
err := cc.Handshake()
|
||||
c = cc
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||
}
|
||||
}
|
||||
|
||||
var user *socks5.User
|
||||
if ss.user != "" {
|
||||
user = &socks5.User{
|
||||
Username: ss.user,
|
||||
Password: ss.pass,
|
||||
}
|
||||
}
|
||||
if _, err := socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdConnect, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (ss *Socks5) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
|
||||
defer safeConnClose(c, err)
|
||||
|
||||
c, err = ss.StreamConn(c, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewConn(c, ss), nil
|
||||
}
|
||||
|
||||
// DialUDP implements C.ProxyAdapter
|
||||
func (ss *Socks5) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
|
||||
defer cancel()
|
||||
c, err := dialer.DialContext(ctx, "tcp", ss.addr)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||
return
|
||||
}
|
||||
|
||||
if ss.tls {
|
||||
cc := tls.Client(c, ss.tlsConfig)
|
||||
err = cc.Handshake()
|
||||
c = cc
|
||||
}
|
||||
|
||||
defer safeConnClose(c, err)
|
||||
|
||||
tcpKeepAlive(c)
|
||||
var user *socks5.User
|
||||
if ss.user != "" {
|
||||
user = &socks5.User{
|
||||
Username: ss.user,
|
||||
Password: ss.pass,
|
||||
}
|
||||
}
|
||||
|
||||
bindAddr, err := socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdUDPAssociate, user)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("client hanshake error: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
pc, err := dialer.ListenPacket("udp", "")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
io.Copy(ioutil.Discard, c)
|
||||
c.Close()
|
||||
// A UDP association terminates when the TCP connection that the UDP
|
||||
// ASSOCIATE request arrived on terminates. RFC1928
|
||||
pc.Close()
|
||||
}()
|
||||
|
||||
// Support unspecified UDP bind address.
|
||||
bindUDPAddr := bindAddr.UDPAddr()
|
||||
if bindUDPAddr == nil {
|
||||
err = errors.New("invalid UDP bind address")
|
||||
return
|
||||
} else if bindUDPAddr.IP.IsUnspecified() {
|
||||
serverAddr, err := resolveUDPAddr("udp", ss.Addr())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bindUDPAddr.IP = serverAddr.IP
|
||||
}
|
||||
|
||||
return newPacketConn(&socksPacketConn{PacketConn: pc, rAddr: bindUDPAddr, tcpConn: c}, ss), nil
|
||||
}
|
||||
|
||||
func NewSocks5(option Socks5Option) *Socks5 {
|
||||
var tlsConfig *tls.Config
|
||||
if option.TLS {
|
||||
tlsConfig = &tls.Config{
|
||||
InsecureSkipVerify: option.SkipCertVerify,
|
||||
ClientSessionCache: getClientSessionCache(),
|
||||
ServerName: option.Server,
|
||||
}
|
||||
}
|
||||
|
||||
return &Socks5{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
||||
tp: C.Socks5,
|
||||
udp: option.UDP,
|
||||
},
|
||||
user: option.UserName,
|
||||
pass: option.Password,
|
||||
tls: option.TLS,
|
||||
skipCertVerify: option.SkipCertVerify,
|
||||
tlsConfig: tlsConfig,
|
||||
}
|
||||
}
|
||||
|
||||
type socksPacketConn struct {
|
||||
net.PacketConn
|
||||
rAddr net.Addr
|
||||
tcpConn net.Conn
|
||||
}
|
||||
|
||||
func (uc *socksPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
|
||||
packet, err := socks5.EncodeUDPPacket(socks5.ParseAddrToSocksAddr(addr), b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return uc.PacketConn.WriteTo(packet, uc.rAddr)
|
||||
}
|
||||
|
||||
func (uc *socksPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
||||
n, _, e := uc.PacketConn.ReadFrom(b)
|
||||
if e != nil {
|
||||
return 0, nil, e
|
||||
}
|
||||
addr, payload, err := socks5.DecodeUDPPacket(b)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
udpAddr := addr.UDPAddr()
|
||||
if udpAddr == nil {
|
||||
return 0, nil, errors.New("parse udp addr error")
|
||||
}
|
||||
|
||||
// due to DecodeUDPPacket is mutable, record addr length
|
||||
copy(b, payload)
|
||||
return n - len(addr) - 3, udpAddr, nil
|
||||
}
|
||||
|
||||
func (uc *socksPacketConn) Close() error {
|
||||
uc.tcpConn.Close()
|
||||
return uc.PacketConn.Close()
|
||||
}
|
178
adapter/outbound/trojan.go
Normal file
178
adapter/outbound/trojan.go
Normal file
@ -0,0 +1,178 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/transport/gun"
|
||||
"github.com/Dreamacro/clash/transport/trojan"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
type Trojan struct {
|
||||
*Base
|
||||
instance *trojan.Trojan
|
||||
|
||||
// for gun mux
|
||||
gunTLSConfig *tls.Config
|
||||
gunConfig *gun.Config
|
||||
transport *http2.Transport
|
||||
}
|
||||
|
||||
type TrojanOption struct {
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
Password string `proxy:"password"`
|
||||
ALPN []string `proxy:"alpn,omitempty"`
|
||||
SNI string `proxy:"sni,omitempty"`
|
||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||
UDP bool `proxy:"udp,omitempty"`
|
||||
Network string `proxy:"network,omitempty"`
|
||||
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
|
||||
}
|
||||
|
||||
// StreamConn implements C.ProxyAdapter
|
||||
func (t *Trojan) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||
var err error
|
||||
if t.transport != nil {
|
||||
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig)
|
||||
} else {
|
||||
c, err = t.instance.StreamConn(c)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||
}
|
||||
|
||||
err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata))
|
||||
return c, err
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||
// gun transport
|
||||
if t.transport != nil {
|
||||
c, err := gun.StreamGunWithTransport(t.transport, t.gunConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)); err != nil {
|
||||
c.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewConn(c, t), nil
|
||||
}
|
||||
|
||||
c, err := dialer.DialContext(ctx, "tcp", t.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
|
||||
defer safeConnClose(c, err)
|
||||
|
||||
c, err = t.StreamConn(c, metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewConn(c, t), err
|
||||
}
|
||||
|
||||
// DialUDP implements C.ProxyAdapter
|
||||
func (t *Trojan) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||
var c net.Conn
|
||||
|
||||
// grpc transport
|
||||
if t.transport != nil {
|
||||
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||
}
|
||||
defer safeConnClose(c, err)
|
||||
} else {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
|
||||
defer cancel()
|
||||
c, err = dialer.DialContext(ctx, "tcp", t.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||
}
|
||||
defer safeConnClose(c, err)
|
||||
tcpKeepAlive(c)
|
||||
c, err = t.instance.StreamConn(c)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||
}
|
||||
}
|
||||
|
||||
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pc := t.instance.PacketConn(c)
|
||||
return newPacketConn(pc, t), err
|
||||
}
|
||||
|
||||
func NewTrojan(option TrojanOption) (*Trojan, error) {
|
||||
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||
|
||||
tOption := &trojan.Option{
|
||||
Password: option.Password,
|
||||
ALPN: option.ALPN,
|
||||
ServerName: option.Server,
|
||||
SkipCertVerify: option.SkipCertVerify,
|
||||
ClientSessionCache: getClientSessionCache(),
|
||||
}
|
||||
|
||||
if option.SNI != "" {
|
||||
tOption.ServerName = option.SNI
|
||||
}
|
||||
|
||||
t := &Trojan{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
addr: addr,
|
||||
tp: C.Trojan,
|
||||
udp: option.UDP,
|
||||
},
|
||||
instance: trojan.New(tOption),
|
||||
}
|
||||
|
||||
if option.Network == "grpc" {
|
||||
dialFn := func(network, addr string) (net.Conn, error) {
|
||||
c, err := dialer.DialContext(context.Background(), "tcp", t.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error())
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
NextProtos: option.ALPN,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
InsecureSkipVerify: tOption.SkipCertVerify,
|
||||
ServerName: tOption.ServerName,
|
||||
ClientSessionCache: getClientSessionCache(),
|
||||
}
|
||||
|
||||
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
|
||||
t.gunTLSConfig = tlsConfig
|
||||
t.gunConfig = &gun.Config{
|
||||
ServiceName: option.GrpcOpts.GrpcServiceName,
|
||||
Host: tOption.ServerName,
|
||||
}
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
72
adapter/outbound/util.go
Normal file
72
adapter/outbound/util.go
Normal file
@ -0,0 +1,72 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/component/resolver"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/transport/socks5"
|
||||
)
|
||||
|
||||
var (
|
||||
globalClientSessionCache tls.ClientSessionCache
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
func tcpKeepAlive(c net.Conn) {
|
||||
if tcp, ok := c.(*net.TCPConn); ok {
|
||||
tcp.SetKeepAlive(true)
|
||||
tcp.SetKeepAlivePeriod(30 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func getClientSessionCache() tls.ClientSessionCache {
|
||||
once.Do(func() {
|
||||
globalClientSessionCache = tls.NewLRUClientSessionCache(128)
|
||||
})
|
||||
return globalClientSessionCache
|
||||
}
|
||||
|
||||
func serializesSocksAddr(metadata *C.Metadata) []byte {
|
||||
var buf [][]byte
|
||||
aType := uint8(metadata.AddrType)
|
||||
p, _ := strconv.Atoi(metadata.DstPort)
|
||||
port := []byte{uint8(p >> 8), uint8(p & 0xff)}
|
||||
switch metadata.AddrType {
|
||||
case socks5.AtypDomainName:
|
||||
len := uint8(len(metadata.Host))
|
||||
host := []byte(metadata.Host)
|
||||
buf = [][]byte{{aType, len}, host, port}
|
||||
case socks5.AtypIPv4:
|
||||
host := metadata.DstIP.To4()
|
||||
buf = [][]byte{{aType}, host, port}
|
||||
case socks5.AtypIPv6:
|
||||
host := metadata.DstIP.To16()
|
||||
buf = [][]byte{{aType}, host, port}
|
||||
}
|
||||
return bytes.Join(buf, nil)
|
||||
}
|
||||
|
||||
func resolveUDPAddr(network, address string) (*net.UDPAddr, error) {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ip, err := resolver.ResolveIP(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port))
|
||||
}
|
||||
|
||||
func safeConnClose(c net.Conn, err error) {
|
||||
if err != nil {
|
||||
c.Close()
|
||||
}
|
||||
}
|
354
adapter/outbound/vmess.go
Normal file
354
adapter/outbound/vmess.go
Normal file
@ -0,0 +1,354 @@
|
||||
package outbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
"github.com/Dreamacro/clash/component/resolver"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/transport/gun"
|
||||
"github.com/Dreamacro/clash/transport/vmess"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
type Vmess struct {
|
||||
*Base
|
||||
client *vmess.Client
|
||||
option *VmessOption
|
||||
|
||||
// for gun mux
|
||||
gunTLSConfig *tls.Config
|
||||
gunConfig *gun.Config
|
||||
transport *http2.Transport
|
||||
}
|
||||
|
||||
type VmessOption struct {
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
UUID string `proxy:"uuid"`
|
||||
AlterID int `proxy:"alterId"`
|
||||
Cipher string `proxy:"cipher"`
|
||||
TLS bool `proxy:"tls,omitempty"`
|
||||
UDP bool `proxy:"udp,omitempty"`
|
||||
Network string `proxy:"network,omitempty"`
|
||||
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
|
||||
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
|
||||
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
|
||||
WSPath string `proxy:"ws-path,omitempty"`
|
||||
WSHeaders map[string]string `proxy:"ws-headers,omitempty"`
|
||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||
ServerName string `proxy:"servername,omitempty"`
|
||||
}
|
||||
|
||||
type HTTPOptions struct {
|
||||
Method string `proxy:"method,omitempty"`
|
||||
Path []string `proxy:"path,omitempty"`
|
||||
Headers map[string][]string `proxy:"headers,omitempty"`
|
||||
}
|
||||
|
||||
type HTTP2Options struct {
|
||||
Host []string `proxy:"host,omitempty"`
|
||||
Path string `proxy:"path,omitempty"`
|
||||
}
|
||||
|
||||
type GrpcOptions struct {
|
||||
GrpcServiceName string `proxy:"grpc-service-name,omitempty"`
|
||||
}
|
||||
|
||||
// StreamConn implements C.ProxyAdapter
|
||||
func (v *Vmess) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) {
|
||||
var err error
|
||||
switch v.option.Network {
|
||||
case "ws":
|
||||
host, port, _ := net.SplitHostPort(v.addr)
|
||||
wsOpts := &vmess.WebsocketConfig{
|
||||
Host: host,
|
||||
Port: port,
|
||||
Path: v.option.WSPath,
|
||||
}
|
||||
|
||||
if len(v.option.WSHeaders) != 0 {
|
||||
header := http.Header{}
|
||||
for key, value := range v.option.WSHeaders {
|
||||
header.Add(key, value)
|
||||
}
|
||||
wsOpts.Headers = header
|
||||
}
|
||||
|
||||
if v.option.TLS {
|
||||
wsOpts.TLS = true
|
||||
wsOpts.SessionCache = getClientSessionCache()
|
||||
wsOpts.SkipCertVerify = v.option.SkipCertVerify
|
||||
wsOpts.ServerName = v.option.ServerName
|
||||
}
|
||||
c, err = vmess.StreamWebsocketConn(c, wsOpts)
|
||||
case "http":
|
||||
// readability first, so just copy default TLS logic
|
||||
if v.option.TLS {
|
||||
host, _, _ := net.SplitHostPort(v.addr)
|
||||
tlsOpts := &vmess.TLSConfig{
|
||||
Host: host,
|
||||
SkipCertVerify: v.option.SkipCertVerify,
|
||||
SessionCache: getClientSessionCache(),
|
||||
}
|
||||
|
||||
if v.option.ServerName != "" {
|
||||
tlsOpts.Host = v.option.ServerName
|
||||
}
|
||||
|
||||
c, err = vmess.StreamTLSConn(c, tlsOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
host, _, _ := net.SplitHostPort(v.addr)
|
||||
httpOpts := &vmess.HTTPConfig{
|
||||
Host: host,
|
||||
Method: v.option.HTTPOpts.Method,
|
||||
Path: v.option.HTTPOpts.Path,
|
||||
Headers: v.option.HTTPOpts.Headers,
|
||||
}
|
||||
|
||||
c = vmess.StreamHTTPConn(c, httpOpts)
|
||||
case "h2":
|
||||
host, _, _ := net.SplitHostPort(v.addr)
|
||||
tlsOpts := vmess.TLSConfig{
|
||||
Host: host,
|
||||
SkipCertVerify: v.option.SkipCertVerify,
|
||||
SessionCache: getClientSessionCache(),
|
||||
NextProtos: []string{"h2"},
|
||||
}
|
||||
|
||||
if v.option.ServerName != "" {
|
||||
tlsOpts.Host = v.option.ServerName
|
||||
}
|
||||
|
||||
c, err = vmess.StreamTLSConn(c, &tlsOpts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h2Opts := &vmess.H2Config{
|
||||
Hosts: v.option.HTTP2Opts.Host,
|
||||
Path: v.option.HTTP2Opts.Path,
|
||||
}
|
||||
|
||||
c, err = vmess.StreamH2Conn(c, h2Opts)
|
||||
case "grpc":
|
||||
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig)
|
||||
default:
|
||||
// handle TLS
|
||||
if v.option.TLS {
|
||||
host, _, _ := net.SplitHostPort(v.addr)
|
||||
tlsOpts := &vmess.TLSConfig{
|
||||
Host: host,
|
||||
SkipCertVerify: v.option.SkipCertVerify,
|
||||
SessionCache: getClientSessionCache(),
|
||||
}
|
||||
|
||||
if v.option.ServerName != "" {
|
||||
tlsOpts.Host = v.option.ServerName
|
||||
}
|
||||
|
||||
c, err = vmess.StreamTLSConn(c, tlsOpts)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return v.client.StreamConn(c, parseVmessAddr(metadata))
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||
// gun transport
|
||||
if v.transport != nil {
|
||||
c, err := gun.StreamGunWithTransport(v.transport, v.gunConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer safeConnClose(c, err)
|
||||
|
||||
c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewConn(c, v), nil
|
||||
}
|
||||
|
||||
c, err := dialer.DialContext(ctx, "tcp", v.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
defer safeConnClose(c, err)
|
||||
|
||||
c, err = v.StreamConn(c, metadata)
|
||||
return NewConn(c, v), err
|
||||
}
|
||||
|
||||
// DialUDP implements C.ProxyAdapter
|
||||
func (v *Vmess) DialUDP(metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||
// vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr
|
||||
if !metadata.Resolved() {
|
||||
ip, err := resolver.ResolveIP(metadata.Host)
|
||||
if err != nil {
|
||||
return nil, errors.New("can't resolve ip")
|
||||
}
|
||||
metadata.DstIP = ip
|
||||
}
|
||||
|
||||
var c net.Conn
|
||||
// gun transport
|
||||
if v.transport != nil {
|
||||
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer safeConnClose(c, err)
|
||||
|
||||
c, err = v.client.StreamConn(c, parseVmessAddr(metadata))
|
||||
} else {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), C.DefaultTCPTimeout)
|
||||
defer cancel()
|
||||
c, err = dialer.DialContext(ctx, "tcp", v.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
defer safeConnClose(c, err)
|
||||
|
||||
c, err = v.StreamConn(c, metadata)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("new vmess client error: %v", err)
|
||||
}
|
||||
|
||||
return newPacketConn(&vmessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil
|
||||
}
|
||||
|
||||
func NewVmess(option VmessOption) (*Vmess, error) {
|
||||
security := strings.ToLower(option.Cipher)
|
||||
client, err := vmess.NewClient(vmess.Config{
|
||||
UUID: option.UUID,
|
||||
AlterID: uint16(option.AlterID),
|
||||
Security: security,
|
||||
HostName: option.Server,
|
||||
Port: strconv.Itoa(option.Port),
|
||||
IsAead: option.AlterID == 0,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch option.Network {
|
||||
case "h2", "grpc":
|
||||
if !option.TLS {
|
||||
return nil, fmt.Errorf("TLS must be true with h2/grpc network")
|
||||
}
|
||||
}
|
||||
|
||||
v := &Vmess{
|
||||
Base: &Base{
|
||||
name: option.Name,
|
||||
addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
|
||||
tp: C.Vmess,
|
||||
udp: option.UDP,
|
||||
},
|
||||
client: client,
|
||||
option: &option,
|
||||
}
|
||||
|
||||
switch option.Network {
|
||||
case "h2":
|
||||
if len(option.HTTP2Opts.Host) == 0 {
|
||||
option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com")
|
||||
}
|
||||
case "grpc":
|
||||
dialFn := func(network, addr string) (net.Conn, error) {
|
||||
c, err := dialer.DialContext(context.Background(), "tcp", v.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
gunConfig := &gun.Config{
|
||||
ServiceName: v.option.GrpcOpts.GrpcServiceName,
|
||||
Host: v.option.ServerName,
|
||||
}
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: v.option.SkipCertVerify,
|
||||
ServerName: v.option.ServerName,
|
||||
}
|
||||
|
||||
if v.option.ServerName == "" {
|
||||
host, _, _ := net.SplitHostPort(v.addr)
|
||||
tlsConfig.ServerName = host
|
||||
gunConfig.Host = host
|
||||
}
|
||||
|
||||
v.gunTLSConfig = tlsConfig
|
||||
v.gunConfig = gunConfig
|
||||
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig)
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func parseVmessAddr(metadata *C.Metadata) *vmess.DstAddr {
|
||||
var addrType byte
|
||||
var addr []byte
|
||||
switch metadata.AddrType {
|
||||
case C.AtypIPv4:
|
||||
addrType = byte(vmess.AtypIPv4)
|
||||
addr = make([]byte, net.IPv4len)
|
||||
copy(addr[:], metadata.DstIP.To4())
|
||||
case C.AtypIPv6:
|
||||
addrType = byte(vmess.AtypIPv6)
|
||||
addr = make([]byte, net.IPv6len)
|
||||
copy(addr[:], metadata.DstIP.To16())
|
||||
case C.AtypDomainName:
|
||||
addrType = byte(vmess.AtypDomainName)
|
||||
addr = make([]byte, len(metadata.Host)+1)
|
||||
addr[0] = byte(len(metadata.Host))
|
||||
copy(addr[1:], []byte(metadata.Host))
|
||||
}
|
||||
|
||||
port, _ := strconv.Atoi(metadata.DstPort)
|
||||
return &vmess.DstAddr{
|
||||
UDP: metadata.NetWork == C.UDP,
|
||||
AddrType: addrType,
|
||||
Addr: addr,
|
||||
Port: uint(port),
|
||||
}
|
||||
}
|
||||
|
||||
type vmessPacketConn struct {
|
||||
net.Conn
|
||||
rAddr net.Addr
|
||||
}
|
||||
|
||||
func (uc *vmessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
|
||||
return uc.Conn.Write(b)
|
||||
}
|
||||
|
||||
func (uc *vmessPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
||||
n, err := uc.Conn.Read(b)
|
||||
return n, uc.rAddr, err
|
||||
}
|
24
adapter/outboundgroup/common.go
Normal file
24
adapter/outboundgroup/common.go
Normal file
@ -0,0 +1,24 @@
|
||||
package outboundgroup
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/adapter/provider"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultGetProxiesDuration = time.Second * 5
|
||||
)
|
||||
|
||||
func getProvidersProxies(providers []provider.ProxyProvider, touch bool) []C.Proxy {
|
||||
proxies := []C.Proxy{}
|
||||
for _, provider := range providers {
|
||||
if touch {
|
||||
proxies = append(proxies, provider.ProxiesWithTouch()...)
|
||||
} else {
|
||||
proxies = append(proxies, provider.Proxies()...)
|
||||
}
|
||||
}
|
||||
return proxies
|
||||
}
|
100
adapter/outboundgroup/fallback.go
Normal file
100
adapter/outboundgroup/fallback.go
Normal file
@ -0,0 +1,100 @@
|
||||
package outboundgroup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/Dreamacro/clash/adapter/outbound"
|
||||
"github.com/Dreamacro/clash/adapter/provider"
|
||||
"github.com/Dreamacro/clash/common/singledo"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type Fallback struct {
|
||||
*outbound.Base
|
||||
disableUDP bool
|
||||
single *singledo.Single
|
||||
providers []provider.ProxyProvider
|
||||
}
|
||||
|
||||
func (f *Fallback) Now() string {
|
||||
proxy := f.findAliveProxy(false)
|
||||
return proxy.Name()
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
proxy := f.findAliveProxy(true)
|
||||
c, err := proxy.DialContext(ctx, metadata)
|
||||
if err == nil {
|
||||
c.AppendToChains(f)
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
// DialUDP implements C.ProxyAdapter
|
||||
func (f *Fallback) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||
proxy := f.findAliveProxy(true)
|
||||
pc, err := proxy.DialUDP(metadata)
|
||||
if err == nil {
|
||||
pc.AppendToChains(f)
|
||||
}
|
||||
return pc, err
|
||||
}
|
||||
|
||||
// SupportUDP implements C.ProxyAdapter
|
||||
func (f *Fallback) SupportUDP() bool {
|
||||
if f.disableUDP {
|
||||
return false
|
||||
}
|
||||
|
||||
proxy := f.findAliveProxy(false)
|
||||
return proxy.SupportUDP()
|
||||
}
|
||||
|
||||
// MarshalJSON implements C.ProxyAdapter
|
||||
func (f *Fallback) MarshalJSON() ([]byte, error) {
|
||||
var all []string
|
||||
for _, proxy := range f.proxies(false) {
|
||||
all = append(all, proxy.Name())
|
||||
}
|
||||
return json.Marshal(map[string]interface{}{
|
||||
"type": f.Type().String(),
|
||||
"now": f.Now(),
|
||||
"all": all,
|
||||
})
|
||||
}
|
||||
|
||||
// Unwrap implements C.ProxyAdapter
|
||||
func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy {
|
||||
proxy := f.findAliveProxy(true)
|
||||
return proxy
|
||||
}
|
||||
|
||||
func (f *Fallback) proxies(touch bool) []C.Proxy {
|
||||
elm, _, _ := f.single.Do(func() (interface{}, error) {
|
||||
return getProvidersProxies(f.providers, touch), nil
|
||||
})
|
||||
|
||||
return elm.([]C.Proxy)
|
||||
}
|
||||
|
||||
func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
|
||||
proxies := f.proxies(touch)
|
||||
for _, proxy := range proxies {
|
||||
if proxy.Alive() {
|
||||
return proxy
|
||||
}
|
||||
}
|
||||
|
||||
return proxies[0]
|
||||
}
|
||||
|
||||
func NewFallback(options *GroupCommonOption, providers []provider.ProxyProvider) *Fallback {
|
||||
return &Fallback{
|
||||
Base: outbound.NewBase(options.Name, "", C.Fallback, false),
|
||||
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||
providers: providers,
|
||||
disableUDP: options.DisableUDP,
|
||||
}
|
||||
}
|
179
adapter/outboundgroup/loadbalance.go
Normal file
179
adapter/outboundgroup/loadbalance.go
Normal file
@ -0,0 +1,179 @@
|
||||
package outboundgroup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/Dreamacro/clash/adapter/outbound"
|
||||
"github.com/Dreamacro/clash/adapter/provider"
|
||||
"github.com/Dreamacro/clash/common/murmur3"
|
||||
"github.com/Dreamacro/clash/common/singledo"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
|
||||
"golang.org/x/net/publicsuffix"
|
||||
)
|
||||
|
||||
type strategyFn = func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy
|
||||
|
||||
type LoadBalance struct {
|
||||
*outbound.Base
|
||||
disableUDP bool
|
||||
single *singledo.Single
|
||||
providers []provider.ProxyProvider
|
||||
strategyFn strategyFn
|
||||
}
|
||||
|
||||
var errStrategy = errors.New("unsupported strategy")
|
||||
|
||||
func parseStrategy(config map[string]interface{}) string {
|
||||
if elm, ok := config["strategy"]; ok {
|
||||
if strategy, ok := elm.(string); ok {
|
||||
return strategy
|
||||
}
|
||||
}
|
||||
return "consistent-hashing"
|
||||
}
|
||||
|
||||
func getKey(metadata *C.Metadata) string {
|
||||
if metadata.Host != "" {
|
||||
// ip host
|
||||
if ip := net.ParseIP(metadata.Host); ip != nil {
|
||||
return metadata.Host
|
||||
}
|
||||
|
||||
if etld, err := publicsuffix.EffectiveTLDPlusOne(metadata.Host); err == nil {
|
||||
return etld
|
||||
}
|
||||
}
|
||||
|
||||
if metadata.DstIP == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return metadata.DstIP.String()
|
||||
}
|
||||
|
||||
func jumpHash(key uint64, buckets int32) int32 {
|
||||
var b, j int64
|
||||
|
||||
for j < int64(buckets) {
|
||||
b = j
|
||||
key = key*2862933555777941757 + 1
|
||||
j = int64(float64(b+1) * (float64(int64(1)<<31) / float64((key>>33)+1)))
|
||||
}
|
||||
|
||||
return int32(b)
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (lb *LoadBalance) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
|
||||
defer func() {
|
||||
if err == nil {
|
||||
c.AppendToChains(lb)
|
||||
}
|
||||
}()
|
||||
|
||||
proxy := lb.Unwrap(metadata)
|
||||
|
||||
c, err = proxy.DialContext(ctx, metadata)
|
||||
return
|
||||
}
|
||||
|
||||
// DialUDP implements C.ProxyAdapter
|
||||
func (lb *LoadBalance) DialUDP(metadata *C.Metadata) (pc C.PacketConn, err error) {
|
||||
defer func() {
|
||||
if err == nil {
|
||||
pc.AppendToChains(lb)
|
||||
}
|
||||
}()
|
||||
|
||||
proxy := lb.Unwrap(metadata)
|
||||
|
||||
return proxy.DialUDP(metadata)
|
||||
}
|
||||
|
||||
// SupportUDP implements C.ProxyAdapter
|
||||
func (lb *LoadBalance) SupportUDP() bool {
|
||||
return !lb.disableUDP
|
||||
}
|
||||
|
||||
func strategyRoundRobin() strategyFn {
|
||||
idx := 0
|
||||
return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy {
|
||||
length := len(proxies)
|
||||
for i := 0; i < length; i++ {
|
||||
idx = (idx + 1) % length
|
||||
proxy := proxies[idx]
|
||||
if proxy.Alive() {
|
||||
return proxy
|
||||
}
|
||||
}
|
||||
|
||||
return proxies[0]
|
||||
}
|
||||
}
|
||||
|
||||
func strategyConsistentHashing() strategyFn {
|
||||
maxRetry := 5
|
||||
return func(proxies []C.Proxy, metadata *C.Metadata) C.Proxy {
|
||||
key := uint64(murmur3.Sum32([]byte(getKey(metadata))))
|
||||
buckets := int32(len(proxies))
|
||||
for i := 0; i < maxRetry; i, key = i+1, key+1 {
|
||||
idx := jumpHash(key, buckets)
|
||||
proxy := proxies[idx]
|
||||
if proxy.Alive() {
|
||||
return proxy
|
||||
}
|
||||
}
|
||||
|
||||
return proxies[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Unwrap implements C.ProxyAdapter
|
||||
func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy {
|
||||
proxies := lb.proxies(true)
|
||||
return lb.strategyFn(proxies, metadata)
|
||||
}
|
||||
|
||||
func (lb *LoadBalance) proxies(touch bool) []C.Proxy {
|
||||
elm, _, _ := lb.single.Do(func() (interface{}, error) {
|
||||
return getProvidersProxies(lb.providers, touch), nil
|
||||
})
|
||||
|
||||
return elm.([]C.Proxy)
|
||||
}
|
||||
|
||||
// MarshalJSON implements C.ProxyAdapter
|
||||
func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
|
||||
var all []string
|
||||
for _, proxy := range lb.proxies(false) {
|
||||
all = append(all, proxy.Name())
|
||||
}
|
||||
return json.Marshal(map[string]interface{}{
|
||||
"type": lb.Type().String(),
|
||||
"all": all,
|
||||
})
|
||||
}
|
||||
|
||||
func NewLoadBalance(options *GroupCommonOption, providers []provider.ProxyProvider, strategy string) (lb *LoadBalance, err error) {
|
||||
var strategyFn strategyFn
|
||||
switch strategy {
|
||||
case "consistent-hashing":
|
||||
strategyFn = strategyConsistentHashing()
|
||||
case "round-robin":
|
||||
strategyFn = strategyRoundRobin()
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: %s", errStrategy, strategy)
|
||||
}
|
||||
return &LoadBalance{
|
||||
Base: outbound.NewBase(options.Name, "", C.LoadBalance, false),
|
||||
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||
providers: providers,
|
||||
strategyFn: strategyFn,
|
||||
disableUDP: options.DisableUDP,
|
||||
}, nil
|
||||
}
|
155
adapter/outboundgroup/parser.go
Normal file
155
adapter/outboundgroup/parser.go
Normal file
@ -0,0 +1,155 @@
|
||||
package outboundgroup
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/Dreamacro/clash/adapter/provider"
|
||||
"github.com/Dreamacro/clash/common/structure"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
var (
|
||||
errFormat = errors.New("format error")
|
||||
errType = errors.New("unsupport type")
|
||||
errMissProxy = errors.New("`use` or `proxies` missing")
|
||||
errMissHealthCheck = errors.New("`url` or `interval` missing")
|
||||
errDuplicateProvider = errors.New("`duplicate provider name")
|
||||
)
|
||||
|
||||
type GroupCommonOption struct {
|
||||
Name string `group:"name"`
|
||||
Type string `group:"type"`
|
||||
Proxies []string `group:"proxies,omitempty"`
|
||||
Use []string `group:"use,omitempty"`
|
||||
URL string `group:"url,omitempty"`
|
||||
Interval int `group:"interval,omitempty"`
|
||||
Lazy bool `group:"lazy,omitempty"`
|
||||
DisableUDP bool `group:"disable-udp,omitempty"`
|
||||
}
|
||||
|
||||
func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy, providersMap map[string]provider.ProxyProvider) (C.ProxyAdapter, error) {
|
||||
decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true})
|
||||
|
||||
groupOption := &GroupCommonOption{
|
||||
Lazy: true,
|
||||
}
|
||||
if err := decoder.Decode(config, groupOption); err != nil {
|
||||
return nil, errFormat
|
||||
}
|
||||
|
||||
if groupOption.Type == "" || groupOption.Name == "" {
|
||||
return nil, errFormat
|
||||
}
|
||||
|
||||
groupName := groupOption.Name
|
||||
|
||||
providers := []provider.ProxyProvider{}
|
||||
|
||||
if len(groupOption.Proxies) == 0 && len(groupOption.Use) == 0 {
|
||||
return nil, errMissProxy
|
||||
}
|
||||
|
||||
if len(groupOption.Proxies) != 0 {
|
||||
ps, err := getProxies(proxyMap, groupOption.Proxies)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if Use not empty, drop health check options
|
||||
if len(groupOption.Use) != 0 {
|
||||
hc := provider.NewHealthCheck(ps, "", 0, true)
|
||||
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
providers = append(providers, pd)
|
||||
} else {
|
||||
if _, ok := providersMap[groupName]; ok {
|
||||
return nil, errDuplicateProvider
|
||||
}
|
||||
|
||||
// select don't need health check
|
||||
if groupOption.Type == "select" || groupOption.Type == "relay" {
|
||||
hc := provider.NewHealthCheck(ps, "", 0, true)
|
||||
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
providers = append(providers, pd)
|
||||
providersMap[groupName] = pd
|
||||
} else {
|
||||
if groupOption.URL == "" || groupOption.Interval == 0 {
|
||||
return nil, errMissHealthCheck
|
||||
}
|
||||
|
||||
hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy)
|
||||
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
providers = append(providers, pd)
|
||||
providersMap[groupName] = pd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(groupOption.Use) != 0 {
|
||||
list, err := getProviders(providersMap, groupOption.Use)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
providers = append(providers, list...)
|
||||
}
|
||||
|
||||
var group C.ProxyAdapter
|
||||
switch groupOption.Type {
|
||||
case "url-test":
|
||||
opts := parseURLTestOption(config)
|
||||
group = NewURLTest(groupOption, providers, opts...)
|
||||
case "select":
|
||||
group = NewSelector(groupOption, providers)
|
||||
case "fallback":
|
||||
group = NewFallback(groupOption, providers)
|
||||
case "load-balance":
|
||||
strategy := parseStrategy(config)
|
||||
return NewLoadBalance(groupOption, providers, strategy)
|
||||
case "relay":
|
||||
group = NewRelay(groupOption, providers)
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: %s", errType, groupOption.Type)
|
||||
}
|
||||
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func getProxies(mapping map[string]C.Proxy, list []string) ([]C.Proxy, error) {
|
||||
var ps []C.Proxy
|
||||
for _, name := range list {
|
||||
p, ok := mapping[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("'%s' not found", name)
|
||||
}
|
||||
ps = append(ps, p)
|
||||
}
|
||||
return ps, nil
|
||||
}
|
||||
|
||||
func getProviders(mapping map[string]provider.ProxyProvider, list []string) ([]provider.ProxyProvider, error) {
|
||||
var ps []provider.ProxyProvider
|
||||
for _, name := range list {
|
||||
p, ok := mapping[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("'%s' not found", name)
|
||||
}
|
||||
|
||||
if p.VehicleType() == provider.Compatible {
|
||||
return nil, fmt.Errorf("proxy group %s can't contains in `use`", name)
|
||||
}
|
||||
ps = append(ps, p)
|
||||
}
|
||||
return ps, nil
|
||||
}
|
100
adapter/outboundgroup/relay.go
Normal file
100
adapter/outboundgroup/relay.go
Normal file
@ -0,0 +1,100 @@
|
||||
package outboundgroup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/Dreamacro/clash/adapter/outbound"
|
||||
"github.com/Dreamacro/clash/adapter/provider"
|
||||
"github.com/Dreamacro/clash/common/singledo"
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type Relay struct {
|
||||
*outbound.Base
|
||||
single *singledo.Single
|
||||
providers []provider.ProxyProvider
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
proxies := r.proxies(metadata, true)
|
||||
if len(proxies) == 0 {
|
||||
return nil, errors.New("proxy does not exist")
|
||||
}
|
||||
first := proxies[0]
|
||||
last := proxies[len(proxies)-1]
|
||||
|
||||
c, err := dialer.DialContext(ctx, "tcp", first.Addr())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
|
||||
}
|
||||
tcpKeepAlive(c)
|
||||
|
||||
var currentMeta *C.Metadata
|
||||
for _, proxy := range proxies[1:] {
|
||||
currentMeta, err = addrToMetadata(proxy.Addr())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c, err = first.StreamConn(c, currentMeta)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", first.Addr(), err)
|
||||
}
|
||||
|
||||
first = proxy
|
||||
}
|
||||
|
||||
c, err = last.StreamConn(c, metadata)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", last.Addr(), err)
|
||||
}
|
||||
|
||||
return outbound.NewConn(c, r), nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements C.ProxyAdapter
|
||||
func (r *Relay) MarshalJSON() ([]byte, error) {
|
||||
var all []string
|
||||
for _, proxy := range r.rawProxies(false) {
|
||||
all = append(all, proxy.Name())
|
||||
}
|
||||
return json.Marshal(map[string]interface{}{
|
||||
"type": r.Type().String(),
|
||||
"all": all,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Relay) rawProxies(touch bool) []C.Proxy {
|
||||
elm, _, _ := r.single.Do(func() (interface{}, error) {
|
||||
return getProvidersProxies(r.providers, touch), nil
|
||||
})
|
||||
|
||||
return elm.([]C.Proxy)
|
||||
}
|
||||
|
||||
func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy {
|
||||
proxies := r.rawProxies(touch)
|
||||
|
||||
for n, proxy := range proxies {
|
||||
subproxy := proxy.Unwrap(metadata)
|
||||
for subproxy != nil {
|
||||
proxies[n] = subproxy
|
||||
subproxy = subproxy.Unwrap(metadata)
|
||||
}
|
||||
}
|
||||
|
||||
return proxies
|
||||
}
|
||||
|
||||
func NewRelay(options *GroupCommonOption, providers []provider.ProxyProvider) *Relay {
|
||||
return &Relay{
|
||||
Base: outbound.NewBase(options.Name, "", C.Relay, false),
|
||||
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||
providers: providers,
|
||||
}
|
||||
}
|
108
adapter/outboundgroup/selector.go
Normal file
108
adapter/outboundgroup/selector.go
Normal file
@ -0,0 +1,108 @@
|
||||
package outboundgroup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
||||
"github.com/Dreamacro/clash/adapter/outbound"
|
||||
"github.com/Dreamacro/clash/adapter/provider"
|
||||
"github.com/Dreamacro/clash/common/singledo"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type Selector struct {
|
||||
*outbound.Base
|
||||
disableUDP bool
|
||||
single *singledo.Single
|
||||
selected string
|
||||
providers []provider.ProxyProvider
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
|
||||
c, err := s.selectedProxy(true).DialContext(ctx, metadata)
|
||||
if err == nil {
|
||||
c.AppendToChains(s)
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
// DialUDP implements C.ProxyAdapter
|
||||
func (s *Selector) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||
pc, err := s.selectedProxy(true).DialUDP(metadata)
|
||||
if err == nil {
|
||||
pc.AppendToChains(s)
|
||||
}
|
||||
return pc, err
|
||||
}
|
||||
|
||||
// SupportUDP implements C.ProxyAdapter
|
||||
func (s *Selector) SupportUDP() bool {
|
||||
if s.disableUDP {
|
||||
return false
|
||||
}
|
||||
|
||||
return s.selectedProxy(false).SupportUDP()
|
||||
}
|
||||
|
||||
// MarshalJSON implements C.ProxyAdapter
|
||||
func (s *Selector) MarshalJSON() ([]byte, error) {
|
||||
var all []string
|
||||
for _, proxy := range getProvidersProxies(s.providers, false) {
|
||||
all = append(all, proxy.Name())
|
||||
}
|
||||
|
||||
return json.Marshal(map[string]interface{}{
|
||||
"type": s.Type().String(),
|
||||
"now": s.Now(),
|
||||
"all": all,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Selector) Now() string {
|
||||
return s.selectedProxy(false).Name()
|
||||
}
|
||||
|
||||
func (s *Selector) Set(name string) error {
|
||||
for _, proxy := range getProvidersProxies(s.providers, false) {
|
||||
if proxy.Name() == name {
|
||||
s.selected = name
|
||||
s.single.Reset()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("proxy not exist")
|
||||
}
|
||||
|
||||
// Unwrap implements C.ProxyAdapter
|
||||
func (s *Selector) Unwrap(metadata *C.Metadata) C.Proxy {
|
||||
return s.selectedProxy(true)
|
||||
}
|
||||
|
||||
func (s *Selector) selectedProxy(touch bool) C.Proxy {
|
||||
elm, _, _ := s.single.Do(func() (interface{}, error) {
|
||||
proxies := getProvidersProxies(s.providers, touch)
|
||||
for _, proxy := range proxies {
|
||||
if proxy.Name() == s.selected {
|
||||
return proxy, nil
|
||||
}
|
||||
}
|
||||
|
||||
return proxies[0], nil
|
||||
})
|
||||
|
||||
return elm.(C.Proxy)
|
||||
}
|
||||
|
||||
func NewSelector(options *GroupCommonOption, providers []provider.ProxyProvider) *Selector {
|
||||
selected := providers[0].Proxies()[0].Name()
|
||||
return &Selector{
|
||||
Base: outbound.NewBase(options.Name, "", C.Selector, false),
|
||||
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||
providers: providers,
|
||||
selected: selected,
|
||||
disableUDP: options.DisableUDP,
|
||||
}
|
||||
}
|
150
adapter/outboundgroup/urltest.go
Normal file
150
adapter/outboundgroup/urltest.go
Normal file
@ -0,0 +1,150 @@
|
||||
package outboundgroup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/adapter/outbound"
|
||||
"github.com/Dreamacro/clash/adapter/provider"
|
||||
"github.com/Dreamacro/clash/common/singledo"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
type urlTestOption func(*URLTest)
|
||||
|
||||
func urlTestWithTolerance(tolerance uint16) urlTestOption {
|
||||
return func(u *URLTest) {
|
||||
u.tolerance = tolerance
|
||||
}
|
||||
}
|
||||
|
||||
type URLTest struct {
|
||||
*outbound.Base
|
||||
tolerance uint16
|
||||
disableUDP bool
|
||||
fastNode C.Proxy
|
||||
single *singledo.Single
|
||||
fastSingle *singledo.Single
|
||||
providers []provider.ProxyProvider
|
||||
}
|
||||
|
||||
func (u *URLTest) Now() string {
|
||||
return u.fast(false).Name()
|
||||
}
|
||||
|
||||
// DialContext implements C.ProxyAdapter
|
||||
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
|
||||
c, err = u.fast(true).DialContext(ctx, metadata)
|
||||
if err == nil {
|
||||
c.AppendToChains(u)
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
// DialUDP implements C.ProxyAdapter
|
||||
func (u *URLTest) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
|
||||
pc, err := u.fast(true).DialUDP(metadata)
|
||||
if err == nil {
|
||||
pc.AppendToChains(u)
|
||||
}
|
||||
return pc, err
|
||||
}
|
||||
|
||||
// Unwrap implements C.ProxyAdapter
|
||||
func (u *URLTest) Unwrap(metadata *C.Metadata) C.Proxy {
|
||||
return u.fast(true)
|
||||
}
|
||||
|
||||
func (u *URLTest) proxies(touch bool) []C.Proxy {
|
||||
elm, _, _ := u.single.Do(func() (interface{}, error) {
|
||||
return getProvidersProxies(u.providers, touch), nil
|
||||
})
|
||||
|
||||
return elm.([]C.Proxy)
|
||||
}
|
||||
|
||||
func (u *URLTest) fast(touch bool) C.Proxy {
|
||||
elm, _, _ := u.fastSingle.Do(func() (interface{}, error) {
|
||||
proxies := u.proxies(touch)
|
||||
fast := proxies[0]
|
||||
min := fast.LastDelay()
|
||||
fastNotExist := true
|
||||
|
||||
for _, proxy := range proxies[1:] {
|
||||
if u.fastNode != nil && proxy.Name() == u.fastNode.Name() {
|
||||
fastNotExist = false
|
||||
}
|
||||
|
||||
if !proxy.Alive() {
|
||||
continue
|
||||
}
|
||||
|
||||
delay := proxy.LastDelay()
|
||||
if delay < min {
|
||||
fast = proxy
|
||||
min = delay
|
||||
}
|
||||
}
|
||||
|
||||
// tolerance
|
||||
if u.fastNode == nil || fastNotExist || !u.fastNode.Alive() || u.fastNode.LastDelay() > fast.LastDelay()+u.tolerance {
|
||||
u.fastNode = fast
|
||||
}
|
||||
|
||||
return u.fastNode, nil
|
||||
})
|
||||
|
||||
return elm.(C.Proxy)
|
||||
}
|
||||
|
||||
// SupportUDP implements C.ProxyAdapter
|
||||
func (u *URLTest) SupportUDP() bool {
|
||||
if u.disableUDP {
|
||||
return false
|
||||
}
|
||||
|
||||
return u.fast(false).SupportUDP()
|
||||
}
|
||||
|
||||
// MarshalJSON implements C.ProxyAdapter
|
||||
func (u *URLTest) MarshalJSON() ([]byte, error) {
|
||||
var all []string
|
||||
for _, proxy := range u.proxies(false) {
|
||||
all = append(all, proxy.Name())
|
||||
}
|
||||
return json.Marshal(map[string]interface{}{
|
||||
"type": u.Type().String(),
|
||||
"now": u.Now(),
|
||||
"all": all,
|
||||
})
|
||||
}
|
||||
|
||||
func parseURLTestOption(config map[string]interface{}) []urlTestOption {
|
||||
opts := []urlTestOption{}
|
||||
|
||||
// tolerance
|
||||
if elm, ok := config["tolerance"]; ok {
|
||||
if tolerance, ok := elm.(int); ok {
|
||||
opts = append(opts, urlTestWithTolerance(uint16(tolerance)))
|
||||
}
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
func NewURLTest(commonOptions *GroupCommonOption, providers []provider.ProxyProvider, options ...urlTestOption) *URLTest {
|
||||
urlTest := &URLTest{
|
||||
Base: outbound.NewBase(commonOptions.Name, "", C.URLTest, false),
|
||||
single: singledo.NewSingle(defaultGetProxiesDuration),
|
||||
fastSingle: singledo.NewSingle(time.Second * 10),
|
||||
providers: providers,
|
||||
disableUDP: commonOptions.DisableUDP,
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
option(urlTest)
|
||||
}
|
||||
|
||||
return urlTest
|
||||
}
|
51
adapter/outboundgroup/util.go
Normal file
51
adapter/outboundgroup/util.go
Normal file
@ -0,0 +1,51 @@
|
||||
package outboundgroup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
func addrToMetadata(rawAddress string) (addr *C.Metadata, err error) {
|
||||
host, port, err := net.SplitHostPort(rawAddress)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("addrToMetadata failed: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil {
|
||||
addr = &C.Metadata{
|
||||
AddrType: C.AtypDomainName,
|
||||
Host: host,
|
||||
DstIP: nil,
|
||||
DstPort: port,
|
||||
}
|
||||
return
|
||||
} else if ip4 := ip.To4(); ip4 != nil {
|
||||
addr = &C.Metadata{
|
||||
AddrType: C.AtypIPv4,
|
||||
Host: "",
|
||||
DstIP: ip4,
|
||||
DstPort: port,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
addr = &C.Metadata{
|
||||
AddrType: C.AtypIPv6,
|
||||
Host: "",
|
||||
DstIP: ip,
|
||||
DstPort: port,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func tcpKeepAlive(c net.Conn) {
|
||||
if tcp, ok := c.(*net.TCPConn); ok {
|
||||
tcp.SetKeepAlive(true)
|
||||
tcp.SetKeepAlivePeriod(30 * time.Second)
|
||||
}
|
||||
}
|
86
adapter/parser.go
Normal file
86
adapter/parser.go
Normal file
@ -0,0 +1,86 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/Dreamacro/clash/adapter/outbound"
|
||||
"github.com/Dreamacro/clash/common/structure"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
func ParseProxy(mapping map[string]interface{}) (C.Proxy, error) {
|
||||
decoder := structure.NewDecoder(structure.Option{TagName: "proxy", WeaklyTypedInput: true})
|
||||
proxyType, existType := mapping["type"].(string)
|
||||
if !existType {
|
||||
return nil, fmt.Errorf("missing type")
|
||||
}
|
||||
|
||||
var (
|
||||
proxy C.ProxyAdapter
|
||||
err error
|
||||
)
|
||||
switch proxyType {
|
||||
case "ss":
|
||||
ssOption := &outbound.ShadowSocksOption{}
|
||||
err = decoder.Decode(mapping, ssOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy, err = outbound.NewShadowSocks(*ssOption)
|
||||
case "ssr":
|
||||
ssrOption := &outbound.ShadowSocksROption{}
|
||||
err = decoder.Decode(mapping, ssrOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy, err = outbound.NewShadowSocksR(*ssrOption)
|
||||
case "socks5":
|
||||
socksOption := &outbound.Socks5Option{}
|
||||
err = decoder.Decode(mapping, socksOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy = outbound.NewSocks5(*socksOption)
|
||||
case "http":
|
||||
httpOption := &outbound.HttpOption{}
|
||||
err = decoder.Decode(mapping, httpOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy = outbound.NewHttp(*httpOption)
|
||||
case "vmess":
|
||||
vmessOption := &outbound.VmessOption{
|
||||
HTTPOpts: outbound.HTTPOptions{
|
||||
Method: "GET",
|
||||
Path: []string{"/"},
|
||||
},
|
||||
}
|
||||
err = decoder.Decode(mapping, vmessOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy, err = outbound.NewVmess(*vmessOption)
|
||||
case "snell":
|
||||
snellOption := &outbound.SnellOption{}
|
||||
err = decoder.Decode(mapping, snellOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy, err = outbound.NewSnell(*snellOption)
|
||||
case "trojan":
|
||||
trojanOption := &outbound.TrojanOption{}
|
||||
err = decoder.Decode(mapping, trojanOption)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
proxy, err = outbound.NewTrojan(*trojanOption)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewProxy(proxy), nil
|
||||
}
|
184
adapter/provider/fetcher.go
Normal file
184
adapter/provider/fetcher.go
Normal file
@ -0,0 +1,184 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/log"
|
||||
)
|
||||
|
||||
var (
|
||||
fileMode os.FileMode = 0666
|
||||
dirMode os.FileMode = 0755
|
||||
)
|
||||
|
||||
type parser = func([]byte) (interface{}, error)
|
||||
|
||||
type fetcher struct {
|
||||
name string
|
||||
vehicle Vehicle
|
||||
updatedAt *time.Time
|
||||
ticker *time.Ticker
|
||||
done chan struct{}
|
||||
hash [16]byte
|
||||
parser parser
|
||||
onUpdate func(interface{})
|
||||
}
|
||||
|
||||
func (f *fetcher) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
func (f *fetcher) VehicleType() VehicleType {
|
||||
return f.vehicle.Type()
|
||||
}
|
||||
|
||||
func (f *fetcher) Initial() (interface{}, error) {
|
||||
var (
|
||||
buf []byte
|
||||
err error
|
||||
isLocal bool
|
||||
)
|
||||
if stat, fErr := os.Stat(f.vehicle.Path()); fErr == nil {
|
||||
buf, err = ioutil.ReadFile(f.vehicle.Path())
|
||||
modTime := stat.ModTime()
|
||||
f.updatedAt = &modTime
|
||||
isLocal = true
|
||||
} else {
|
||||
buf, err = f.vehicle.Read()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxies, err := f.parser(buf)
|
||||
if err != nil {
|
||||
if !isLocal {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// parse local file error, fallback to remote
|
||||
buf, err = f.vehicle.Read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxies, err = f.parser(buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isLocal = false
|
||||
}
|
||||
|
||||
if f.vehicle.Type() != File && !isLocal {
|
||||
if err := safeWrite(f.vehicle.Path(), buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
f.hash = md5.Sum(buf)
|
||||
|
||||
// pull proxies automatically
|
||||
if f.ticker != nil {
|
||||
go f.pullLoop()
|
||||
}
|
||||
|
||||
return proxies, nil
|
||||
}
|
||||
|
||||
func (f *fetcher) Update() (interface{}, bool, error) {
|
||||
buf, err := f.vehicle.Read()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
hash := md5.Sum(buf)
|
||||
if bytes.Equal(f.hash[:], hash[:]) {
|
||||
f.updatedAt = &now
|
||||
return nil, true, nil
|
||||
}
|
||||
|
||||
proxies, err := f.parser(buf)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if f.vehicle.Type() != File {
|
||||
if err := safeWrite(f.vehicle.Path(), buf); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
f.updatedAt = &now
|
||||
f.hash = hash
|
||||
|
||||
return proxies, false, nil
|
||||
}
|
||||
|
||||
func (f *fetcher) Destroy() error {
|
||||
if f.ticker != nil {
|
||||
f.done <- struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fetcher) pullLoop() {
|
||||
for {
|
||||
select {
|
||||
case <-f.ticker.C:
|
||||
elm, same, err := f.Update()
|
||||
if err != nil {
|
||||
log.Warnln("[Provider] %s pull error: %s", f.Name(), err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
if same {
|
||||
log.Debugln("[Provider] %s's proxies doesn't change", f.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
log.Infoln("[Provider] %s's proxies update", f.Name())
|
||||
if f.onUpdate != nil {
|
||||
f.onUpdate(elm)
|
||||
}
|
||||
case <-f.done:
|
||||
f.ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func safeWrite(path string, buf []byte) error {
|
||||
dir := filepath.Dir(path)
|
||||
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(dir, dirMode); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(path, buf, fileMode)
|
||||
}
|
||||
|
||||
func newFetcher(name string, interval time.Duration, vehicle Vehicle, parser parser, onUpdate func(interface{})) *fetcher {
|
||||
var ticker *time.Ticker
|
||||
if interval != 0 {
|
||||
ticker = time.NewTicker(interval)
|
||||
}
|
||||
|
||||
return &fetcher{
|
||||
name: name,
|
||||
ticker: ticker,
|
||||
vehicle: vehicle,
|
||||
parser: parser,
|
||||
done: make(chan struct{}, 1),
|
||||
onUpdate: onUpdate,
|
||||
}
|
||||
}
|
91
adapter/provider/healthcheck.go
Normal file
91
adapter/provider/healthcheck.go
Normal file
@ -0,0 +1,91 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultURLTestTimeout = time.Second * 5
|
||||
)
|
||||
|
||||
type HealthCheckOption struct {
|
||||
URL string
|
||||
Interval uint
|
||||
}
|
||||
|
||||
type HealthCheck struct {
|
||||
url string
|
||||
proxies []C.Proxy
|
||||
interval uint
|
||||
lazy bool
|
||||
lastTouch *atomic.Int64
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func (hc *HealthCheck) process() {
|
||||
ticker := time.NewTicker(time.Duration(hc.interval) * time.Second)
|
||||
|
||||
go hc.check()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
now := time.Now().Unix()
|
||||
if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) {
|
||||
hc.check()
|
||||
}
|
||||
case <-hc.done:
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (hc *HealthCheck) setProxy(proxies []C.Proxy) {
|
||||
hc.proxies = proxies
|
||||
}
|
||||
|
||||
func (hc *HealthCheck) auto() bool {
|
||||
return hc.interval != 0
|
||||
}
|
||||
|
||||
func (hc *HealthCheck) touch() {
|
||||
hc.lastTouch.Store(time.Now().Unix())
|
||||
}
|
||||
|
||||
func (hc *HealthCheck) check() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
|
||||
wg := &sync.WaitGroup{}
|
||||
|
||||
for _, proxy := range hc.proxies {
|
||||
wg.Add(1)
|
||||
|
||||
go func(p C.Proxy) {
|
||||
p.URLTest(ctx, hc.url)
|
||||
wg.Done()
|
||||
}(proxy)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
cancel()
|
||||
}
|
||||
|
||||
func (hc *HealthCheck) close() {
|
||||
hc.done <- struct{}{}
|
||||
}
|
||||
|
||||
func NewHealthCheck(proxies []C.Proxy, url string, interval uint, lazy bool) *HealthCheck {
|
||||
return &HealthCheck{
|
||||
proxies: proxies,
|
||||
url: url,
|
||||
interval: interval,
|
||||
lazy: lazy,
|
||||
lastTouch: atomic.NewInt64(0),
|
||||
done: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
63
adapter/provider/parser.go
Normal file
63
adapter/provider/parser.go
Normal file
@ -0,0 +1,63 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/common/structure"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
var (
|
||||
errVehicleType = errors.New("unsupport vehicle type")
|
||||
)
|
||||
|
||||
type healthCheckSchema struct {
|
||||
Enable bool `provider:"enable"`
|
||||
URL string `provider:"url"`
|
||||
Interval int `provider:"interval"`
|
||||
Lazy bool `provider:"lazy,omitempty"`
|
||||
}
|
||||
|
||||
type proxyProviderSchema struct {
|
||||
Type string `provider:"type"`
|
||||
Path string `provider:"path"`
|
||||
URL string `provider:"url,omitempty"`
|
||||
Interval int `provider:"interval,omitempty"`
|
||||
HealthCheck healthCheckSchema `provider:"health-check,omitempty"`
|
||||
}
|
||||
|
||||
func ParseProxyProvider(name string, mapping map[string]interface{}) (ProxyProvider, error) {
|
||||
decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
|
||||
|
||||
schema := &proxyProviderSchema{
|
||||
HealthCheck: healthCheckSchema{
|
||||
Lazy: true,
|
||||
},
|
||||
}
|
||||
if err := decoder.Decode(mapping, schema); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var hcInterval uint
|
||||
if schema.HealthCheck.Enable {
|
||||
hcInterval = uint(schema.HealthCheck.Interval)
|
||||
}
|
||||
hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, hcInterval, schema.HealthCheck.Lazy)
|
||||
|
||||
path := C.Path.Resolve(schema.Path)
|
||||
|
||||
var vehicle Vehicle
|
||||
switch schema.Type {
|
||||
case "file":
|
||||
vehicle = NewFileVehicle(path)
|
||||
case "http":
|
||||
vehicle = NewHTTPVehicle(schema.URL, path)
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: %s", errVehicleType, schema.Type)
|
||||
}
|
||||
|
||||
interval := time.Duration(uint(schema.Interval)) * time.Second
|
||||
return NewProxySetProvider(name, interval, vehicle, hc), nil
|
||||
}
|
261
adapter/provider/provider.go
Normal file
261
adapter/provider/provider.go
Normal file
@ -0,0 +1,261 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/adapter"
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
ReservedName = "default"
|
||||
)
|
||||
|
||||
// Provider Type
|
||||
const (
|
||||
Proxy ProviderType = iota
|
||||
Rule
|
||||
)
|
||||
|
||||
// ProviderType defined
|
||||
type ProviderType int
|
||||
|
||||
func (pt ProviderType) String() string {
|
||||
switch pt {
|
||||
case Proxy:
|
||||
return "Proxy"
|
||||
case Rule:
|
||||
return "Rule"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// Provider interface
|
||||
type Provider interface {
|
||||
Name() string
|
||||
VehicleType() VehicleType
|
||||
Type() ProviderType
|
||||
Initial() error
|
||||
Update() error
|
||||
}
|
||||
|
||||
// ProxyProvider interface
|
||||
type ProxyProvider interface {
|
||||
Provider
|
||||
Proxies() []C.Proxy
|
||||
// ProxiesWithTouch is used to inform the provider that the proxy is actually being used while getting the list of proxies.
|
||||
// Commonly used in Dial and DialUDP
|
||||
ProxiesWithTouch() []C.Proxy
|
||||
HealthCheck()
|
||||
}
|
||||
|
||||
type ProxySchema struct {
|
||||
Proxies []map[string]interface{} `yaml:"proxies"`
|
||||
}
|
||||
|
||||
// for auto gc
|
||||
type ProxySetProvider struct {
|
||||
*proxySetProvider
|
||||
}
|
||||
|
||||
type proxySetProvider struct {
|
||||
*fetcher
|
||||
proxies []C.Proxy
|
||||
healthCheck *HealthCheck
|
||||
}
|
||||
|
||||
func (pp *proxySetProvider) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]interface{}{
|
||||
"name": pp.Name(),
|
||||
"type": pp.Type().String(),
|
||||
"vehicleType": pp.VehicleType().String(),
|
||||
"proxies": pp.Proxies(),
|
||||
"updatedAt": pp.updatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
func (pp *proxySetProvider) Name() string {
|
||||
return pp.name
|
||||
}
|
||||
|
||||
func (pp *proxySetProvider) HealthCheck() {
|
||||
pp.healthCheck.check()
|
||||
}
|
||||
|
||||
func (pp *proxySetProvider) Update() error {
|
||||
elm, same, err := pp.fetcher.Update()
|
||||
if err == nil && !same {
|
||||
pp.onUpdate(elm)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (pp *proxySetProvider) Initial() error {
|
||||
elm, err := pp.fetcher.Initial()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pp.onUpdate(elm)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pp *proxySetProvider) Type() ProviderType {
|
||||
return Proxy
|
||||
}
|
||||
|
||||
func (pp *proxySetProvider) Proxies() []C.Proxy {
|
||||
return pp.proxies
|
||||
}
|
||||
|
||||
func (pp *proxySetProvider) ProxiesWithTouch() []C.Proxy {
|
||||
pp.healthCheck.touch()
|
||||
return pp.Proxies()
|
||||
}
|
||||
|
||||
func proxiesParse(buf []byte) (interface{}, error) {
|
||||
schema := &ProxySchema{}
|
||||
|
||||
if err := yaml.Unmarshal(buf, schema); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if schema.Proxies == nil {
|
||||
return nil, errors.New("file must have a `proxies` field")
|
||||
}
|
||||
|
||||
proxies := []C.Proxy{}
|
||||
for idx, mapping := range schema.Proxies {
|
||||
proxy, err := adapter.ParseProxy(mapping)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("proxy %d error: %w", idx, err)
|
||||
}
|
||||
proxies = append(proxies, proxy)
|
||||
}
|
||||
|
||||
if len(proxies) == 0 {
|
||||
return nil, errors.New("file doesn't have any valid proxy")
|
||||
}
|
||||
|
||||
return proxies, nil
|
||||
}
|
||||
|
||||
func (pp *proxySetProvider) setProxies(proxies []C.Proxy) {
|
||||
pp.proxies = proxies
|
||||
pp.healthCheck.setProxy(proxies)
|
||||
if pp.healthCheck.auto() {
|
||||
go pp.healthCheck.check()
|
||||
}
|
||||
}
|
||||
|
||||
func stopProxyProvider(pd *ProxySetProvider) {
|
||||
pd.healthCheck.close()
|
||||
pd.fetcher.Destroy()
|
||||
}
|
||||
|
||||
func NewProxySetProvider(name string, interval time.Duration, vehicle Vehicle, hc *HealthCheck) *ProxySetProvider {
|
||||
if hc.auto() {
|
||||
go hc.process()
|
||||
}
|
||||
|
||||
pd := &proxySetProvider{
|
||||
proxies: []C.Proxy{},
|
||||
healthCheck: hc,
|
||||
}
|
||||
|
||||
onUpdate := func(elm interface{}) {
|
||||
ret := elm.([]C.Proxy)
|
||||
pd.setProxies(ret)
|
||||
}
|
||||
|
||||
fetcher := newFetcher(name, interval, vehicle, proxiesParse, onUpdate)
|
||||
pd.fetcher = fetcher
|
||||
|
||||
wrapper := &ProxySetProvider{pd}
|
||||
runtime.SetFinalizer(wrapper, stopProxyProvider)
|
||||
return wrapper
|
||||
}
|
||||
|
||||
// for auto gc
|
||||
type CompatibleProvider struct {
|
||||
*compatibleProvider
|
||||
}
|
||||
|
||||
type compatibleProvider struct {
|
||||
name string
|
||||
healthCheck *HealthCheck
|
||||
proxies []C.Proxy
|
||||
}
|
||||
|
||||
func (cp *compatibleProvider) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]interface{}{
|
||||
"name": cp.Name(),
|
||||
"type": cp.Type().String(),
|
||||
"vehicleType": cp.VehicleType().String(),
|
||||
"proxies": cp.Proxies(),
|
||||
})
|
||||
}
|
||||
|
||||
func (cp *compatibleProvider) Name() string {
|
||||
return cp.name
|
||||
}
|
||||
|
||||
func (cp *compatibleProvider) HealthCheck() {
|
||||
cp.healthCheck.check()
|
||||
}
|
||||
|
||||
func (cp *compatibleProvider) Update() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cp *compatibleProvider) Initial() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cp *compatibleProvider) VehicleType() VehicleType {
|
||||
return Compatible
|
||||
}
|
||||
|
||||
func (cp *compatibleProvider) Type() ProviderType {
|
||||
return Proxy
|
||||
}
|
||||
|
||||
func (cp *compatibleProvider) Proxies() []C.Proxy {
|
||||
return cp.proxies
|
||||
}
|
||||
|
||||
func (cp *compatibleProvider) ProxiesWithTouch() []C.Proxy {
|
||||
cp.healthCheck.touch()
|
||||
return cp.Proxies()
|
||||
}
|
||||
|
||||
func stopCompatibleProvider(pd *CompatibleProvider) {
|
||||
pd.healthCheck.close()
|
||||
}
|
||||
|
||||
func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*CompatibleProvider, error) {
|
||||
if len(proxies) == 0 {
|
||||
return nil, errors.New("Provider need one proxy at least")
|
||||
}
|
||||
|
||||
if hc.auto() {
|
||||
go hc.process()
|
||||
}
|
||||
|
||||
pd := &compatibleProvider{
|
||||
name: name,
|
||||
proxies: proxies,
|
||||
healthCheck: hc,
|
||||
}
|
||||
|
||||
wrapper := &CompatibleProvider{pd}
|
||||
runtime.SetFinalizer(wrapper, stopCompatibleProvider)
|
||||
return wrapper, nil
|
||||
}
|
122
adapter/provider/vehicle.go
Normal file
122
adapter/provider/vehicle.go
Normal file
@ -0,0 +1,122 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/Dreamacro/clash/component/dialer"
|
||||
)
|
||||
|
||||
// Vehicle Type
|
||||
const (
|
||||
File VehicleType = iota
|
||||
HTTP
|
||||
Compatible
|
||||
)
|
||||
|
||||
// VehicleType defined
|
||||
type VehicleType int
|
||||
|
||||
func (v VehicleType) String() string {
|
||||
switch v {
|
||||
case File:
|
||||
return "File"
|
||||
case HTTP:
|
||||
return "HTTP"
|
||||
case Compatible:
|
||||
return "Compatible"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
type Vehicle interface {
|
||||
Read() ([]byte, error)
|
||||
Path() string
|
||||
Type() VehicleType
|
||||
}
|
||||
|
||||
type FileVehicle struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func (f *FileVehicle) Type() VehicleType {
|
||||
return File
|
||||
}
|
||||
|
||||
func (f *FileVehicle) Path() string {
|
||||
return f.path
|
||||
}
|
||||
|
||||
func (f *FileVehicle) Read() ([]byte, error) {
|
||||
return ioutil.ReadFile(f.path)
|
||||
}
|
||||
|
||||
func NewFileVehicle(path string) *FileVehicle {
|
||||
return &FileVehicle{path: path}
|
||||
}
|
||||
|
||||
type HTTPVehicle struct {
|
||||
url string
|
||||
path string
|
||||
}
|
||||
|
||||
func (h *HTTPVehicle) Type() VehicleType {
|
||||
return HTTP
|
||||
}
|
||||
|
||||
func (h *HTTPVehicle) Path() string {
|
||||
return h.path
|
||||
}
|
||||
|
||||
func (h *HTTPVehicle) Read() ([]byte, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
|
||||
defer cancel()
|
||||
|
||||
uri, err := url.Parse(h.url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, uri.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if user := uri.User; user != nil {
|
||||
password, _ := user.Password()
|
||||
req.SetBasicAuth(user.Username(), password)
|
||||
}
|
||||
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
transport := &http.Transport{
|
||||
// from http.DefaultTransport
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
DialContext: dialer.DialContext,
|
||||
}
|
||||
|
||||
client := http.Client{Transport: transport}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
buf, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func NewHTTPVehicle(url string, path string) *HTTPVehicle {
|
||||
return &HTTPVehicle{url, path}
|
||||
}
|
Reference in New Issue
Block a user