Merge from remote branch

This commit is contained in:
yaling888 2021-07-28 22:14:20 +08:00
commit ac9e90c812
33 changed files with 1549 additions and 142 deletions

View file

@ -136,6 +136,8 @@ func (p *Proxy) URLTest(ctx context.Context, url string) (t uint16, err error) {
return http.ErrUseLastResponse
},
}
defer client.CloseIdleConnections()
resp, err := client.Do(req)
if err != nil {
return

View file

@ -14,9 +14,7 @@ type Direct struct {
// 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)
c, err := dialer.DialContext(ctx, "tcp", metadata.RemoteAddress())
if err != nil {
return nil, err
}

View file

@ -2,9 +2,9 @@ package provider
import (
"context"
"sync"
"time"
"github.com/Dreamacro/clash/common/batch"
C "github.com/Dreamacro/clash/constant"
"go.uber.org/atomic"
@ -59,20 +59,17 @@ func (hc *HealthCheck) touch() {
}
func (hc *HealthCheck) check() {
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
wg := &sync.WaitGroup{}
b, _ := batch.New(context.Background(), batch.WithConcurrencyNum(10))
for _, proxy := range hc.proxies {
wg.Add(1)
go func(p C.Proxy) {
p := proxy
b.Go(p.Name(), func() (interface{}, error) {
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
defer cancel()
p.URLTest(ctx, hc.url)
wg.Done()
}(proxy)
return nil, nil
})
}
wg.Wait()
cancel()
b.Wait()
}
func (hc *HealthCheck) close() {

105
common/batch/batch.go Normal file
View file

@ -0,0 +1,105 @@
package batch
import (
"context"
"sync"
)
type Option = func(b *Batch)
type Result struct {
Value interface{}
Err error
}
type Error struct {
Key string
Err error
}
func WithConcurrencyNum(n int) Option {
return func(b *Batch) {
q := make(chan struct{}, n)
for i := 0; i < n; i++ {
q <- struct{}{}
}
b.queue = q
}
}
// Batch similar to errgroup, but can control the maximum number of concurrent
type Batch struct {
result map[string]Result
queue chan struct{}
wg sync.WaitGroup
mux sync.Mutex
err *Error
once sync.Once
cancel func()
}
func (b *Batch) Go(key string, fn func() (interface{}, error)) {
b.wg.Add(1)
go func() {
defer b.wg.Done()
if b.queue != nil {
<-b.queue
defer func() {
b.queue <- struct{}{}
}()
}
value, err := fn()
if err != nil {
b.once.Do(func() {
b.err = &Error{key, err}
if b.cancel != nil {
b.cancel()
}
})
}
ret := Result{value, err}
b.mux.Lock()
defer b.mux.Unlock()
b.result[key] = ret
}()
}
func (b *Batch) Wait() *Error {
b.wg.Wait()
if b.cancel != nil {
b.cancel()
}
return b.err
}
func (b *Batch) WaitAndGetResult() (map[string]Result, *Error) {
err := b.Wait()
return b.Result(), err
}
func (b *Batch) Result() map[string]Result {
b.mux.Lock()
defer b.mux.Unlock()
copy := map[string]Result{}
for k, v := range b.result {
copy[k] = v
}
return copy
}
func New(ctx context.Context, opts ...Option) (*Batch, context.Context) {
ctx, cancel := context.WithCancel(ctx)
b := &Batch{
result: map[string]Result{},
}
for _, o := range opts {
o(b)
}
b.cancel = cancel
return b, ctx
}

View file

@ -0,0 +1,83 @@
package batch
import (
"context"
"errors"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestBatch(t *testing.T) {
b, _ := New(context.Background())
now := time.Now()
b.Go("foo", func() (interface{}, error) {
time.Sleep(time.Millisecond * 100)
return "foo", nil
})
b.Go("bar", func() (interface{}, error) {
time.Sleep(time.Millisecond * 150)
return "bar", nil
})
result, err := b.WaitAndGetResult()
assert.Nil(t, err)
duration := time.Since(now)
assert.Less(t, duration, time.Millisecond*200)
assert.Equal(t, 2, len(result))
for k, v := range result {
assert.NoError(t, v.Err)
assert.Equal(t, k, v.Value.(string))
}
}
func TestBatchWithConcurrencyNum(t *testing.T) {
b, _ := New(
context.Background(),
WithConcurrencyNum(3),
)
now := time.Now()
for i := 0; i < 7; i++ {
idx := i
b.Go(strconv.Itoa(idx), func() (interface{}, error) {
time.Sleep(time.Millisecond * 100)
return strconv.Itoa(idx), nil
})
}
result, _ := b.WaitAndGetResult()
duration := time.Since(now)
assert.Greater(t, duration, time.Millisecond*260)
assert.Equal(t, 7, len(result))
for k, v := range result {
assert.NoError(t, v.Err)
assert.Equal(t, k, v.Value.(string))
}
}
func TestBatchContext(t *testing.T) {
b, ctx := New(context.Background())
b.Go("error", func() (interface{}, error) {
time.Sleep(time.Millisecond * 100)
return nil, errors.New("test error")
})
b.Go("ctx", func() (interface{}, error) {
<-ctx.Done()
return nil, ctx.Err()
})
result, err := b.WaitAndGetResult()
assert.NotNil(t, err)
assert.Equal(t, "error", err.Key)
assert.Equal(t, ctx.Err(), result["ctx"].Err)
}

View file

@ -8,11 +8,7 @@ import (
"sync"
)
var defaultAllocator *Allocator
func init() {
defaultAllocator = NewAllocator()
}
var defaultAllocator = NewAllocator()
// Allocator for incoming frames, optimized to prevent overwriting after zeroing
type Allocator struct {

View file

@ -16,14 +16,14 @@ import (
)
// from https://github.com/vishvananda/netlink/blob/bca67dfc8220b44ef582c9da4e9172bf1c9ec973/nl/nl_linux.go#L52-L62
func init() {
var nativeEndian = func() binary.ByteOrder {
var x uint32 = 0x01020304
if *(*byte)(unsafe.Pointer(&x)) == 0x01 {
nativeEndian = binary.BigEndian
} else {
nativeEndian = binary.LittleEndian
return binary.BigEndian
}
}
return binary.LittleEndian
}()
type SocketResolver func(network string, ip net.IP, srcPort int) (inode, uid int, err error)
type ProcessNameResolver func(inode, uid int) (name string, err error)
@ -40,8 +40,6 @@ const (
pathProc = "/proc"
)
var nativeEndian binary.ByteOrder = binary.LittleEndian
func findProcessName(network string, ip net.IP, srcPort int) (string, error) {
inode, uid, err := DefaultSocketResolver(network, ip, srcPort)
if err != nil {

View file

@ -18,7 +18,8 @@ const (
HTTP Type = iota
HTTPCONNECT
SOCKS
SOCKS4
SOCKS5
REDIR
TPROXY
TUN
@ -47,7 +48,9 @@ func (t Type) String() string {
return "HTTP"
case HTTPCONNECT:
return "HTTP Connect"
case SOCKS:
case SOCKS4:
return "Socks4"
case SOCKS5:
return "Socks5"
case REDIR:
return "Redir"

View file

@ -9,21 +9,19 @@ import (
const Name = "clash"
// Path is used to get the configuration path
var Path *path
type path struct {
homeDir string
configFile string
}
func init() {
var Path = func() *path {
homeDir, err := os.UserHomeDir()
if err != nil {
homeDir, _ = os.Getwd()
}
homeDir = P.Join(homeDir, ".config", Name)
Path = &path{homeDir: homeDir, configFile: "config.yaml"}
return &path{homeDir: homeDir, configFile: "config.yaml"}
}()
type path struct {
homeDir string
configFile string
}
// SetHomeDir is used to set the configuration path

View file

@ -76,16 +76,14 @@ func ApplyConfig(cfg *config.Config, force bool) {
defer mux.Unlock()
updateUsers(cfg.Users)
updateDNS(cfg.DNS, cfg.General)
updateGeneral(cfg.General, force)
log.SetLevel(log.DEBUG)
updateProxies(cfg.Proxies, cfg.Providers)
updateRules(cfg.Rules)
updateHosts(cfg.Hosts)
updateExperimental(cfg)
updateProfile(cfg)
updateIPTables(cfg.DNS, cfg.General)
log.SetLevel(cfg.General.LogLevel)
updateDNS(cfg.DNS, cfg.General)
updateGeneral(cfg.General, force)
updateExperimental(cfg)
}
func GetGeneral() *config.General {
@ -178,7 +176,6 @@ func updateRules(rules []C.Rule) {
}
func updateGeneral(general *config.General, force bool) {
log.SetLevel(log.DEBUG)
tunnel.SetMode(general.Mode)
resolver.DisableIPv6 = !general.IPv6
@ -223,7 +220,7 @@ func updateGeneral(general *config.General, force bool) {
}
if err := P.ReCreateSocks(general.SocksPort, tcpIn, udpIn); err != nil {
log.Errorln("Start SOCKS5 server error: %s", err.Error())
log.Errorln("Start SOCKS server error: %s", err.Error())
}
if err := P.ReCreateRedir(general.RedirPort, tcpIn, udpIn); err != nil {
@ -235,7 +232,7 @@ func updateGeneral(general *config.General, force bool) {
}
if err := P.ReCreateMixed(general.MixedPort, tcpIn, udpIn); err != nil {
log.Errorln("Start Mixed(http and socks5) server error: %s", err.Error())
log.Errorln("Start Mixed(http and socks) server error: %s", err.Error())
}
if err := P.ReCreateTun(general.Tun, tcpIn, udpIn); err != nil {

View file

@ -3,6 +3,7 @@ package route
import (
"bytes"
"encoding/json"
"net"
"net/http"
"strings"
"time"
@ -81,10 +82,15 @@ func Start(addr string, secret string) {
})
}
log.Infoln("RESTful API listening at: %s", addr)
err := http.ListenAndServe(addr, r)
l, err := net.Listen("tcp", addr)
if err != nil {
log.Errorln("External controller error: %s", err.Error())
log.Errorln("External controller listen error: %s", err)
return
}
serverAddr = l.Addr().String()
log.Infoln("RESTful API listening at: %s", serverAddr)
if err = http.Serve(l, r); err != nil {
log.Errorln("External controller serve error: %s", err)
}
}

View file

@ -45,6 +45,7 @@ func HandleConn(c net.Conn, in chan<- C.ConnContext, cache *cache.Cache) {
if request.Method == http.MethodConnect {
resp = responseWith(200)
resp.Status = "Connection established"
resp.ContentLength = -1
if resp.Write(conn) != nil {
break // close connection

View file

@ -10,7 +10,6 @@ import (
type Listener struct {
listener net.Listener
address string
closed bool
}
@ -31,7 +30,6 @@ func NewWithAuthenticate(addr string, in chan<- C.ConnContext, authenticate bool
hl := &Listener{
listener: l,
address: addr,
}
go func() {
for {
@ -55,5 +53,5 @@ func (l *Listener) Close() {
}
func (l *Listener) Address() string {
return l.address
return l.listener.Addr().String()
}

View file

@ -157,7 +157,7 @@ func ReCreateSocks(port int, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.P
socksListener = tcpListener
socksUDPListener = udpListener
log.Infoln("SOCKS5 proxy listening at: %s", socksListener.Address())
log.Infoln("SOCKS proxy listening at: %s", socksListener.Address())
return nil
}
@ -289,7 +289,7 @@ func ReCreateMixed(port int, tcpIn chan<- C.ConnContext, udpIn chan<- *inbound.P
return err
}
log.Infoln("Mixed(http+socks5) proxy listening at: %s", mixedListener.Address())
log.Infoln("Mixed(http+socks) proxy listening at: %s", mixedListener.Address())
return nil
}

View file

@ -9,12 +9,12 @@ import (
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/listener/http"
"github.com/Dreamacro/clash/listener/socks"
"github.com/Dreamacro/clash/transport/socks4"
"github.com/Dreamacro/clash/transport/socks5"
)
type Listener struct {
listener net.Listener
address string
closed bool
cache *cache.Cache
}
@ -25,7 +25,10 @@ func New(addr string, in chan<- C.ConnContext) (*Listener, error) {
return nil, err
}
ml := &Listener{l, addr, false, cache.New(30 * time.Second)}
ml := &Listener{
listener: l,
cache: cache.New(30 * time.Second),
}
go func() {
for {
c, err := ml.listener.Accept()
@ -48,7 +51,7 @@ func (l *Listener) Close() {
}
func (l *Listener) Address() string {
return l.address
return l.listener.Addr().String()
}
func handleConn(conn net.Conn, in chan<- C.ConnContext, cache *cache.Cache) {
@ -58,10 +61,12 @@ func handleConn(conn net.Conn, in chan<- C.ConnContext, cache *cache.Cache) {
return
}
if head[0] == socks5.Version {
socks.HandleSocks(bufConn, in)
return
switch head[0] {
case socks4.Version:
socks.HandleSocks4(bufConn, in)
case socks5.Version:
socks.HandleSocks5(bufConn, in)
default:
http.HandleConn(bufConn, in, cache)
}
http.HandleConn(bufConn, in, cache)
}

View file

@ -9,7 +9,6 @@ import (
type Listener struct {
listener net.Listener
address string
closed bool
}
@ -18,7 +17,9 @@ func New(addr string, in chan<- C.ConnContext) (*Listener, error) {
if err != nil {
return nil, err
}
rl := &Listener{l, addr, false}
rl := &Listener{
listener: l,
}
go func() {
for {
@ -42,7 +43,7 @@ func (l *Listener) Close() {
}
func (l *Listener) Address() string {
return l.address
return l.listener.Addr().String()
}
func handleRedir(conn net.Conn, in chan<- C.ConnContext) {

View file

@ -6,14 +6,15 @@ import (
"net"
"github.com/Dreamacro/clash/adapter/inbound"
N "github.com/Dreamacro/clash/common/net"
C "github.com/Dreamacro/clash/constant"
authStore "github.com/Dreamacro/clash/listener/auth"
"github.com/Dreamacro/clash/transport/socks4"
"github.com/Dreamacro/clash/transport/socks5"
)
type Listener struct {
listener net.Listener
address string
closed bool
}
@ -23,7 +24,9 @@ func New(addr string, in chan<- C.ConnContext) (*Listener, error) {
return nil, err
}
sl := &Listener{l, addr, false}
sl := &Listener{
listener: l,
}
go func() {
for {
c, err := l.Accept()
@ -33,7 +36,7 @@ func New(addr string, in chan<- C.ConnContext) (*Listener, error) {
}
continue
}
go HandleSocks(c, in)
go handleSocks(c, in)
}
}()
@ -46,10 +49,40 @@ func (l *Listener) Close() {
}
func (l *Listener) Address() string {
return l.address
return l.listener.Addr().String()
}
func HandleSocks(conn net.Conn, in chan<- C.ConnContext) {
func handleSocks(conn net.Conn, in chan<- C.ConnContext) {
bufConn := N.NewBufferedConn(conn)
head, err := bufConn.Peek(1)
if err != nil {
conn.Close()
return
}
switch head[0] {
case socks4.Version:
HandleSocks4(bufConn, in)
case socks5.Version:
HandleSocks5(bufConn, in)
default:
conn.Close()
}
}
func HandleSocks4(conn net.Conn, in chan<- C.ConnContext) {
addr, _, err := socks4.ServerHandshake(conn, authStore.Authenticator())
if err != nil {
conn.Close()
return
}
if c, ok := conn.(*net.TCPConn); ok {
c.SetKeepAlive(true)
}
in <- inbound.NewSocket(socks5.ParseAddr(addr), conn, C.SOCKS4)
}
func HandleSocks5(conn net.Conn, in chan<- C.ConnContext) {
target, command, err := socks5.ServerHandshake(conn, authStore.Authenticator())
if err != nil {
conn.Close()
@ -63,5 +96,5 @@ func HandleSocks(conn net.Conn, in chan<- C.ConnContext) {
io.Copy(ioutil.Discard, conn)
return
}
in <- inbound.NewSocket(target, conn, C.SOCKS)
in <- inbound.NewSocket(target, conn, C.SOCKS5)
}

View file

@ -13,7 +13,6 @@ import (
type UDPListener struct {
packetConn net.PacketConn
address string
closed bool
}
@ -27,7 +26,9 @@ func NewUDP(addr string, in chan<- *inbound.PacketAdapter) (*UDPListener, error)
log.Warnln("Failed to Reuse UDP Address: %s", err)
}
sl := &UDPListener{l, addr, false}
sl := &UDPListener{
packetConn: l,
}
go func() {
for {
buf := pool.Get(pool.RelayBufferSize)
@ -52,7 +53,7 @@ func (l *UDPListener) Close() error {
}
func (l *UDPListener) Address() string {
return l.address
return l.packetConn.LocalAddr().String()
}
func handleSocksUDP(pc net.PacketConn, in chan<- *inbound.PacketAdapter, buf []byte, addr net.Addr) {
@ -69,7 +70,7 @@ func handleSocksUDP(pc net.PacketConn, in chan<- *inbound.PacketAdapter, buf []b
bufRef: buf,
}
select {
case in <- inbound.NewPacket(target, packet, C.TPROXY):
case in <- inbound.NewPacket(target, packet, C.SOCKS5):
default:
}
}

View file

@ -10,7 +10,6 @@ import (
type Listener struct {
listener net.Listener
address string
closed bool
}
@ -33,7 +32,6 @@ func New(addr string, in chan<- C.ConnContext) (*Listener, error) {
rl := &Listener{
listener: l,
address: addr,
}
go func() {
@ -58,7 +56,7 @@ func (l *Listener) Close() {
}
func (l *Listener) Address() string {
return l.address
return l.listener.Addr().String()
}
func (l *Listener) handleTProxy(conn net.Conn, in chan<- C.ConnContext) {

View file

@ -11,7 +11,6 @@ import (
type UDPListener struct {
packetConn net.PacketConn
address string
closed bool
}
@ -21,7 +20,9 @@ func NewUDP(addr string, in chan<- *inbound.PacketAdapter) (*UDPListener, error)
return nil, err
}
rl := &UDPListener{l, addr, false}
rl := &UDPListener{
packetConn: l,
}
c := l.(*net.UDPConn)
@ -65,7 +66,7 @@ func (l *UDPListener) Close() error {
}
func (l *UDPListener) Address() string {
return l.address
return l.packetConn.LocalAddr().String()
}
func handlePacketConn(pc net.PacketConn, in chan<- *inbound.PacketAdapter, buf []byte, lAddr *net.UDPAddr, rAddr *net.UDPAddr) {

View file

@ -47,3 +47,13 @@ Prerequisite
```
$ go test -p 1 -v
```
benchmark (Linux)
> Cannot represent the throughput of the protocol on your machine
> but you can compare the corresponding throughput of the protocol on clash
> (change chunkSize to measure the maximum throughput of clash on your machine)
```
$ go test -benchmem -run=^$ -bench .
```

View file

@ -15,6 +15,7 @@ import (
"testing"
"time"
"github.com/Dreamacro/clash/adapter/outbound"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/hub/executor"
"github.com/Dreamacro/clash/transport/socks5"
@ -70,7 +71,7 @@ func init() {
}
}
c, err := client.NewClientWithOpts(client.FromEnv)
c, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
panic(err)
}
@ -619,6 +620,42 @@ func testSuit(t *testing.T, proxy C.ProxyAdapter) {
assert.NoError(t, testPacketConnTimeout(t, pc))
}
func benchmarkProxy(b *testing.B, proxy C.ProxyAdapter) {
l, err := net.Listen("tcp", ":10001")
if err != nil {
assert.FailNow(b, err.Error())
}
go func() {
c, err := l.Accept()
if err != nil {
assert.FailNow(b, err.Error())
}
io.Copy(io.Discard, c)
c.Close()
}()
chunkSize := int64(16 * 1024)
chunk := make([]byte, chunkSize)
conn, err := proxy.DialContext(context.Background(), &C.Metadata{
Host: localIP.String(),
DstPort: "10001",
AddrType: socks5.AtypDomainName,
})
if err != nil {
assert.FailNow(b, err.Error())
}
b.SetBytes(chunkSize)
b.ResetTimer()
for i := 0; i < b.N; i++ {
if _, err := conn.Write(chunk); err != nil {
assert.FailNow(b, err.Error())
}
}
}
func TestClash_Basic(t *testing.T) {
basic := `
mixed-port: 10000
@ -633,3 +670,8 @@ log-level: silent
time.Sleep(waitTime)
testPingPongWithSocksPort(t, 10000)
}
func Benchmark_Direct(b *testing.B) {
proxy := outbound.NewDirect()
benchmarkProxy(b, proxy)
}

View file

@ -11,7 +11,7 @@ import (
var isDarwin = false
func startContainer(cfg *container.Config, hostCfg *container.HostConfig, name string) (string, error) {
c, err := client.NewClientWithOpts(client.FromEnv)
c, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return "", err
}
@ -34,7 +34,7 @@ func startContainer(cfg *container.Config, hostCfg *container.HostConfig, name s
}
func cleanContainer(id string) error {
c, err := client.NewClientWithOpts(client.FromEnv)
c, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return err
}

View file

@ -3,24 +3,17 @@ module clash-test
go 1.16
require (
github.com/Dreamacro/clash v1.6.1-0.20210516120541-06fdd3abe0ab
github.com/Microsoft/go-winio v0.4.16 // indirect
github.com/containerd/containerd v1.4.4 // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v20.10.6+incompatible
github.com/Dreamacro/clash v1.6.5
github.com/Microsoft/go-winio v0.5.0 // indirect
github.com/containerd/containerd v1.5.3 // indirect
github.com/docker/docker v20.10.7+incompatible
github.com/docker/go-connections v0.4.0
github.com/docker/go-units v0.4.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/go-cmp v0.5.5 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/miekg/dns v1.1.42
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
github.com/miekg/dns v1.1.43
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/stretchr/testify v1.7.0
golang.org/x/net v0.0.0-20210510120150-4163338589ed
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
google.golang.org/grpc v1.36.0 // indirect
gotest.tools/v3 v3.0.3 // indirect
golang.org/x/net v0.0.0-20210614182718-04defd469f4e
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 // indirect
google.golang.org/grpc v1.39.0 // indirect
)

File diff suppressed because it is too large Load diff

View file

@ -119,3 +119,40 @@ func TestClash_Snell(t *testing.T) {
time.Sleep(waitTime)
testSuit(t, proxy)
}
func Benchmark_Snell(b *testing.B) {
cfg := &container.Config{
Image: ImageSnell,
ExposedPorts: defaultExposedPorts,
Cmd: []string{"-c", "/config.conf"},
}
hostCfg := &container.HostConfig{
PortBindings: defaultPortBindings,
Binds: []string{fmt.Sprintf("%s:/config.conf", C.Path.Resolve("snell-http.conf"))},
}
id, err := startContainer(cfg, hostCfg, "snell-http")
if err != nil {
assert.FailNow(b, err.Error())
}
b.Cleanup(func() {
cleanContainer(id)
})
proxy, err := outbound.NewSnell(outbound.SnellOption{
Name: "snell",
Server: localIP.String(),
Port: 10002,
Psk: "password",
ObfsOpts: map[string]interface{}{
"mode": "http",
},
})
if err != nil {
assert.FailNow(b, err.Error())
}
time.Sleep(waitTime)
benchmarkProxy(b, proxy)
}

View file

@ -170,3 +170,38 @@ func TestClash_ShadowsocksV2RayPlugin(t *testing.T) {
time.Sleep(waitTime)
testSuit(t, proxy)
}
func Benchmark_Shadowsocks(b *testing.B) {
cfg := &container.Config{
Image: ImageShadowsocks,
Env: []string{"SS_MODULE=ss-server", "SS_CONFIG=-s 0.0.0.0 -u -v -p 10002 -m aes-256-gcm -k FzcLbKs2dY9mhL"},
ExposedPorts: defaultExposedPorts,
}
hostCfg := &container.HostConfig{
PortBindings: defaultPortBindings,
}
id, err := startContainer(cfg, hostCfg, "ss")
if err != nil {
assert.FailNow(b, err.Error())
}
b.Cleanup(func() {
cleanContainer(id)
})
proxy, err := outbound.NewShadowSocks(outbound.ShadowSocksOption{
Name: "ss",
Server: localIP.String(),
Port: 10002,
Password: "FzcLbKs2dY9mhL",
Cipher: "aes-256-gcm",
UDP: true,
})
if err != nil {
assert.FailNow(b, err.Error())
}
time.Sleep(waitTime)
benchmarkProxy(b, proxy)
}

View file

@ -92,3 +92,43 @@ func TestClash_TrojanGrpc(t *testing.T) {
time.Sleep(waitTime)
testSuit(t, proxy)
}
func Benchmark_Trojan(b *testing.B) {
cfg := &container.Config{
Image: ImageTrojan,
ExposedPorts: defaultExposedPorts,
}
hostCfg := &container.HostConfig{
PortBindings: defaultPortBindings,
Binds: []string{
fmt.Sprintf("%s:/config/config.json", C.Path.Resolve("trojan.json")),
fmt.Sprintf("%s:/path/to/certificate.crt", C.Path.Resolve("example.org.pem")),
fmt.Sprintf("%s:/path/to/private.key", C.Path.Resolve("example.org-key.pem")),
},
}
id, err := startContainer(cfg, hostCfg, "trojan")
if err != nil {
assert.FailNow(b, err.Error())
}
b.Cleanup(func() {
cleanContainer(id)
})
proxy, err := outbound.NewTrojan(outbound.TrojanOption{
Name: "trojan",
Server: localIP.String(),
Port: 10002,
Password: "password",
SNI: "example.org",
SkipCertVerify: true,
UDP: true,
})
if err != nil {
assert.FailNow(b, err.Error())
}
time.Sleep(waitTime)
benchmarkProxy(b, proxy)
}

12
test/util_other_test.go Normal file
View file

@ -0,0 +1,12 @@
// +build !darwin
package main
import (
"errors"
"net"
)
func defaultRouteIP() (net.IP, error) {
return nil, errors.New("not supported")
}

View file

@ -345,3 +345,41 @@ func TestClash_VmessGrpc(t *testing.T) {
time.Sleep(waitTime)
testSuit(t, proxy)
}
func Benchmark_Vmess(b *testing.B) {
configPath := C.Path.Resolve("vmess-aead.json")
cfg := &container.Config{
Image: ImageVmess,
ExposedPorts: defaultExposedPorts,
}
hostCfg := &container.HostConfig{
PortBindings: defaultPortBindings,
Binds: []string{fmt.Sprintf("%s:/etc/v2ray/config.json", configPath)},
}
id, err := startContainer(cfg, hostCfg, "vmess-aead")
if err != nil {
assert.FailNow(b, err.Error())
}
b.Cleanup(func() {
cleanContainer(id)
})
proxy, err := outbound.NewVmess(outbound.VmessOption{
Name: "vmess",
Server: localIP.String(),
Port: 10002,
UUID: "b831381d-6324-4d53-ad4f-8cda48b30811",
Cipher: "auto",
AlterID: 0,
UDP: true,
})
if err != nil {
assert.FailNow(b, err.Error())
}
time.Sleep(waitTime)
benchmarkProxy(b, proxy)
}

195
transport/socks4/socks4.go Normal file
View file

@ -0,0 +1,195 @@
package socks4
import (
"bytes"
"encoding/binary"
"errors"
"io"
"net"
"strconv"
"github.com/Dreamacro/clash/component/auth"
)
const Version = 0x04
type Command = uint8
const (
CmdConnect Command = 0x01
CmdBind Command = 0x02
)
type Code = uint8
const (
RequestGranted Code = 90
RequestRejected Code = 91
RequestIdentdFailed Code = 92
RequestIdentdMismatched Code = 93
)
var (
errVersionMismatched = errors.New("version code mismatched")
errCommandNotSupported = errors.New("command not supported")
errIPv6NotSupported = errors.New("IPv6 not supported")
ErrRequestRejected = errors.New("request rejected or failed")
ErrRequestIdentdFailed = errors.New("request rejected because SOCKS server cannot connect to identd on the client")
ErrRequestIdentdMismatched = errors.New("request rejected because the client program and identd report different user-ids")
ErrRequestUnknownCode = errors.New("request failed with unknown code")
)
func ServerHandshake(rw io.ReadWriter, authenticator auth.Authenticator) (addr string, command Command, err error) {
var req [8]byte
if _, err = io.ReadFull(rw, req[:]); err != nil {
return
}
if req[0] != Version {
err = errVersionMismatched
return
}
if command = req[1]; command != CmdConnect {
err = errCommandNotSupported
return
}
var (
dstIP = req[4:8] // [4]byte
dstPort = req[2:4] // [2]byte
)
var (
host string
port string
code uint8
userID []byte
)
if userID, err = readUntilNull(rw); err != nil {
return
}
if isReservedIP(dstIP) {
var target []byte
if target, err = readUntilNull(rw); err != nil {
return
}
host = string(target)
}
port = strconv.Itoa(int(binary.BigEndian.Uint16(dstPort)))
if host != "" {
addr = net.JoinHostPort(host, port)
} else {
addr = net.JoinHostPort(net.IP(dstIP).String(), port)
}
// SOCKS4 only support USERID auth.
if authenticator == nil || authenticator.Verify(string(userID), "") {
code = RequestGranted
} else {
code = RequestIdentdMismatched
}
var reply [8]byte
reply[0] = 0x00 // reply code
reply[1] = code // result code
copy(reply[4:8], dstIP)
copy(reply[2:4], dstPort)
_, err = rw.Write(reply[:])
return
}
func ClientHandshake(rw io.ReadWriter, addr string, command Command, userID string) (err error) {
host, portStr, err := net.SplitHostPort(addr)
if err != nil {
return err
}
port, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
return err
}
ip := net.ParseIP(host)
if ip == nil /* HOST */ {
ip = net.IPv4(0, 0, 0, 1).To4()
} else if ip.To4() == nil /* IPv6 */ {
return errIPv6NotSupported
}
dstIP := ip.To4()
req := &bytes.Buffer{}
req.WriteByte(Version)
req.WriteByte(command)
binary.Write(req, binary.BigEndian, uint16(port))
req.Write(dstIP)
req.WriteString(userID)
req.WriteByte(0) /* NULL */
if isReservedIP(dstIP) /* SOCKS4A */ {
req.WriteString(host)
req.WriteByte(0) /* NULL */
}
if _, err = rw.Write(req.Bytes()); err != nil {
return err
}
var resp [8]byte
if _, err = io.ReadFull(rw, resp[:]); err != nil {
return err
}
if resp[0] != 0x00 {
return errVersionMismatched
}
switch resp[1] {
case RequestGranted:
return nil
case RequestRejected:
return ErrRequestRejected
case RequestIdentdFailed:
return ErrRequestIdentdFailed
case RequestIdentdMismatched:
return ErrRequestIdentdMismatched
default:
return ErrRequestUnknownCode
}
}
// For version 4A, if the client cannot resolve the destination host's
// domain name to find its IP address, it should set the first three bytes
// of DSTIP to NULL and the last byte to a non-zero value. (This corresponds
// to IP address 0.0.0.x, with x nonzero. As decreed by IANA -- The
// Internet Assigned Numbers Authority -- such an address is inadmissible
// as a destination IP address and thus should never occur if the client
// can resolve the domain name.)
func isReservedIP(ip net.IP) bool {
subnet := net.IPNet{
IP: net.IPv4zero,
Mask: net.IPv4Mask(0xff, 0xff, 0xff, 0x00),
}
return !ip.IsUnspecified() && subnet.Contains(ip)
}
func readUntilNull(r io.Reader) ([]byte, error) {
var buf = &bytes.Buffer{}
var data [1]byte
for {
if _, err := r.Read(data[:]); err != nil {
return nil, err
}
if data[0] == 0 {
return buf.Bytes(), nil
}
buf.WriteByte(data[0])
}
}

View file

@ -224,9 +224,9 @@ func handleUDPConn(packet *inbound.PacketAdapter) {
rawPc, err := proxy.DialUDP(metadata)
if err != nil {
if rule == nil {
log.Warnln("[UDP] dial %s to %s error: %s", proxy.Name(), metadata.String(), err.Error())
log.Warnln("[UDP] dial %s to %s error: %s", proxy.Name(), metadata.RemoteAddress(), err.Error())
} else {
log.Warnln("[UDP] dial %s (match %s/%s) to %s error: %s", proxy.Name(), rule.RuleType().String(), rule.Payload(), metadata.String(), err.Error())
log.Warnln("[UDP] dial %s (match %s/%s) to %s error: %s", proxy.Name(), rule.RuleType().String(), rule.Payload(), metadata.RemoteAddress(), err.Error())
}
return
}
@ -235,13 +235,13 @@ func handleUDPConn(packet *inbound.PacketAdapter) {
switch true {
case rule != nil:
log.Infoln("[UDP] %s(%s) --> %s:%s match %s(%s) %s using %s", metadata.SourceAddress(), metadata.Process, metadata.String(), metadata.DstPort, rule.RuleType().String(), rule.Payload(), rule.NetWork().String(), rawPc.Chains().String())
log.Infoln("[UDP] %s(%s) --> %s:%s match %s(%s) %s using %s", metadata.SourceAddress(), metadata.Process, metadata.RemoteAddress(), metadata.DstPort, rule.RuleType().String(), rule.Payload(), rule.NetWork().String(), rawPc.Chains().String())
case mode == Global:
log.Infoln("[UDP] %s(%s) --> %s using GLOBAL", metadata.SourceAddress(), metadata.Process, metadata.String())
log.Infoln("[UDP] %s(%s) --> %s using GLOBAL", metadata.SourceAddress(), metadata.Process, metadata.RemoteAddress())
case mode == Direct:
log.Infoln("[UDP] %s(%s) --> %s using DIRECT", metadata.SourceAddress(), metadata.Process, metadata.String())
log.Infoln("[UDP] %s(%s) --> %s using DIRECT", metadata.SourceAddress(), metadata.Process, metadata.RemoteAddress())
default:
log.Infoln("[UDP] %s(%s) --> %s doesn't match any rule using DIRECT", metadata.SourceAddress(), metadata.Process, metadata.String())
log.Infoln("[UDP] %s(%s) --> %s doesn't match any rule using DIRECT", metadata.SourceAddress(), metadata.Process, metadata.RemoteAddress())
}
go handleUDPToLocal(packet.UDPPacket, pc, key, fAddr)
@ -274,9 +274,9 @@ func handleTCPConn(ctx C.ConnContext) {
remoteConn, err := proxy.Dial(metadata)
if err != nil {
if rule == nil {
log.Warnln("[TCP] dial %s to %s error: %s", proxy.Name(), metadata.String(), err.Error())
log.Warnln("[TCP] dial %s to %s error: %s", proxy.Name(), metadata.RemoteAddress(), err.Error())
} else {
log.Warnln("[TCP] dial %s (match %s/%s) to %s error: %s", proxy.Name(), rule.RuleType().String(), rule.Payload(), metadata.String(), err.Error())
log.Warnln("[TCP] dial %s (match %s/%s) to %s error: %s", proxy.Name(), rule.RuleType().String(), rule.Payload(), metadata.RemoteAddress(), err.Error())
}
return
}
@ -285,13 +285,13 @@ func handleTCPConn(ctx C.ConnContext) {
switch true {
case rule != nil:
log.Infoln("[TCP] %s(%s) --> %s:%s match %s(%s) %s using %s", metadata.SourceAddress(), metadata.Process, metadata.String(), metadata.DstPort, rule.RuleType().String(), rule.Payload(), rule.NetWork().String(), remoteConn.Chains().String())
log.Infoln("[TCP] %s(%s) --> %s:%s match %s(%s) %s using %s", metadata.SourceAddress(), metadata.Process, metadata.RemoteAddress(), metadata.DstPort, rule.RuleType().String(), rule.Payload(), rule.NetWork().String(), remoteConn.Chains().String())
case mode == Global:
log.Infoln("[TCP] %s(%s) --> %s using GLOBAL", metadata.SourceAddress(), metadata.Process, metadata.String())
log.Infoln("[TCP] %s(%s) --> %s using GLOBAL", metadata.SourceAddress(), metadata.Process, metadata.RemoteAddress())
case mode == Direct:
log.Infoln("[TCP] %s(%s) --> %s using DIRECT", metadata.SourceAddress(), metadata.Process, metadata.String())
log.Infoln("[TCP] %s(%s) --> %s using DIRECT", metadata.SourceAddress(), metadata.Process, metadata.RemoteAddress())
default:
log.Infoln("[TCP] %s(%s) --> %s doesn't match any rule using DIRECT", metadata.SourceAddress(), metadata.Process, metadata.String())
log.Infoln("[TCP] %s(%s) --> %s doesn't match any rule using DIRECT", metadata.SourceAddress(), metadata.Process, metadata.RemoteAddress())
}
handleSocket(ctx, remoteConn)