Skip to content

Commit 8c3f3d2

Browse files
authored
Merge pull request #102 from Snawoot/srv_select
Automatic server selection feature
2 parents 7bfaeb9 + 389231d commit 8c3f3d2

File tree

5 files changed

+286
-74
lines changed

5 files changed

+286
-74
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ eu3.sec-tunnel.com,77.111.244.22,443
109109
| proxy | String | sets base proxy to use for all dial-outs. Format: `<http\|https\|socks5\|socks5h>://[login:password@]host[:port]` Examples: `http://user:[email protected]:3128`, `socks5://10.0.0.1:1080` |
110110
| refresh | Duration | login refresh interval (default 4h0m0s) |
111111
| refresh-retry | Duration | login refresh retry interval (default 5s) |
112+
| server-selection | Enum | server selection policy (first/random/fastest) (default fastest) |
113+
| server-selection-dl-limit | Number | restrict amount of downloaded data per connection by fastest server selection |
114+
| server-selection-test-url | String | URL used for download benchmark by fastest server selection policy (default `https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js`) |
115+
| server-selection-timeout | Duration | timeout given for server selection function to produce result (default 30s) |
112116
| timeout | Duration | timeout for network operations (default 10s) |
113117
| verbosity | Number | logging verbosity (10 - debug, 20 - info, 30 - warning, 40 - error, 50 - critical) (default 20) |
114118
| version | - | show program version and exit |

dialer/selection.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package dialer
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"math/rand/v2"
9+
"net/http"
10+
"strings"
11+
"time"
12+
13+
"github.com/hashicorp/go-multierror"
14+
)
15+
16+
type ServerSelection int
17+
18+
const (
19+
_ = iota
20+
ServerSelectionFirst
21+
ServerSelectionRandom
22+
ServerSelectionFastest
23+
)
24+
25+
func (ss ServerSelection) String() string {
26+
switch ss {
27+
case ServerSelectionFirst:
28+
return "first"
29+
case ServerSelectionRandom:
30+
return "random"
31+
case ServerSelectionFastest:
32+
return "fastest"
33+
default:
34+
return fmt.Sprintf("ServerSelection(%d)", int(ss))
35+
}
36+
}
37+
38+
func ParseServerSelection(s string) (ServerSelection, error) {
39+
switch strings.ToLower(s) {
40+
case "first":
41+
return ServerSelectionFirst, nil
42+
case "random":
43+
return ServerSelectionRandom, nil
44+
case "fastest":
45+
return ServerSelectionFastest, nil
46+
}
47+
return 0, errors.New("unknown server selection strategy")
48+
}
49+
50+
type SelectionFunc = func(ctx context.Context, dialers []ContextDialer) (ContextDialer, error)
51+
52+
func SelectFirst(_ context.Context, dialers []ContextDialer) (ContextDialer, error) {
53+
if len(dialers) == 0 {
54+
return nil, errors.New("empty dialers list")
55+
}
56+
return dialers[0], nil
57+
}
58+
59+
func SelectRandom(_ context.Context, dialers []ContextDialer) (ContextDialer, error) {
60+
if len(dialers) == 0 {
61+
return nil, errors.New("empty dialers list")
62+
}
63+
return dialers[rand.IntN(len(dialers))], nil
64+
}
65+
66+
func probeDialer(ctx context.Context, dialer ContextDialer, url string, dlLimit int64) error {
67+
httpClient := http.Client{
68+
Transport: &http.Transport{
69+
MaxIdleConns: 100,
70+
IdleConnTimeout: 90 * time.Second,
71+
TLSHandshakeTimeout: 10 * time.Second,
72+
ExpectContinueTimeout: 1 * time.Second,
73+
DialContext: dialer.DialContext,
74+
},
75+
}
76+
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
77+
if err != nil {
78+
return err
79+
}
80+
resp, err := httpClient.Do(req)
81+
if err != nil {
82+
return err
83+
}
84+
defer resp.Body.Close()
85+
if resp.StatusCode != http.StatusOK {
86+
return fmt.Errorf("bad status code %d for URL %q", resp.StatusCode, url)
87+
}
88+
var rd io.Reader = resp.Body
89+
if dlLimit > 0 {
90+
rd = io.LimitReader(rd, dlLimit)
91+
}
92+
_, err = io.Copy(io.Discard, rd)
93+
return err
94+
}
95+
96+
func NewFastestServerSelectionFunc(url string, dlLimit int64) SelectionFunc {
97+
return func(ctx context.Context, dialers []ContextDialer) (ContextDialer, error) {
98+
var resErr error
99+
masterNotInterested := make(chan struct{})
100+
defer close(masterNotInterested)
101+
errors := make(chan error)
102+
success := make(chan ContextDialer)
103+
for _, dialer := range dialers {
104+
go func(dialer ContextDialer) {
105+
err := probeDialer(ctx, dialer, url, dlLimit)
106+
if err == nil {
107+
select {
108+
case success <- dialer:
109+
case <-masterNotInterested:
110+
}
111+
} else {
112+
select {
113+
case errors <- err:
114+
case <-masterNotInterested:
115+
}
116+
}
117+
}(dialer)
118+
}
119+
for _ = range dialers {
120+
select {
121+
case <-ctx.Done():
122+
return nil, ctx.Err()
123+
case d := <-success:
124+
return d, nil
125+
case err := <-errors:
126+
resErr = multierror.Append(resErr, err)
127+
}
128+
}
129+
return nil, resErr
130+
}
131+
}

dialer/upstream.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,10 @@ func (d *ProxyDialer) Dial(network, address string) (net.Conn, error) {
221221
return d.DialContext(context.Background(), network, address)
222222
}
223223

224+
func (d *ProxyDialer) Address() (string, error) {
225+
return d.address()
226+
}
227+
224228
func readResponse(r io.Reader, req *http.Request) (*http.Response, error) {
225229
endOfResponse := []byte("\r\n\r\n")
226230
buf := &bytes.Buffer{}

0 commit comments

Comments
 (0)