Skip to content

Commit 5526ee4

Browse files
authored
feat: proxy to lb (#283)
1 parent 886fafb commit 5526ee4

File tree

7 files changed

+240
-6
lines changed

7 files changed

+240
-6
lines changed

internal/constant/constant.go

+1
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ const WelcomeHTML = `
111111
<ul>
112112
{{range .SubConfigs}}
113113
<li><a href="/clash_proxy_provider/?sub_name={{.Name}}">{{.Name}}</a></li>
114+
<li><a href="/clash_proxy_provider/?sub_name={{.Name}}&grouped=true">{{.Name}}-lb</a></li>
114115
{{end}}
115116
</ul>
116117
{{end}}

internal/relay/conf/cfg.go

+11
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,14 @@ func (r *Config) Validate() error {
5454
}
5555
return nil
5656
}
57+
58+
func (r *Config) Clone() *Config {
59+
return &Config{
60+
Listen: r.Listen,
61+
ListenType: r.ListenType,
62+
TransportType: r.TransportType,
63+
TCPRemotes: r.TCPRemotes,
64+
UDPRemotes: r.UDPRemotes,
65+
Label: r.Label,
66+
}
67+
}

internal/web/handlers.go

+16-1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ func (s *Server) HandleClashProxyProvider(w http.ResponseWriter, r *http.Request
5151
writerBadRequestMsg(w, msg)
5252
return
5353
}
54+
grouped := r.URL.Query().Get("grouped")
55+
if grouped == "true" {
56+
handleClashProxyProvider(s, w, r, subName, true)
57+
} else {
58+
handleClashProxyProvider(s, w, r, subName, false)
59+
}
60+
}
61+
62+
func handleClashProxyProvider(s *Server, w http.ResponseWriter, r *http.Request, subName string, grouped bool) {
5463
if s.relayServerReloader != nil {
5564
if err := s.relayServerReloader.Reload(); err != nil {
5665
writerBadRequestMsg(w, err.Error())
@@ -66,7 +75,13 @@ func (s *Server) HandleClashProxyProvider(w http.ResponseWriter, r *http.Request
6675
}
6776
for _, clashSub := range clashSubList {
6877
if clashSub.Name == subName {
69-
clashCfgBuf, err := clashSub.ToClashConfigYaml()
78+
var clashCfgBuf []byte
79+
var err error
80+
if grouped {
81+
clashCfgBuf, err = clashSub.ToGroupedClashConfigYaml()
82+
} else {
83+
clashCfgBuf, err = clashSub.ToClashConfigYaml()
84+
}
7085
if err != nil {
7186
writerBadRequestMsg(w, err.Error())
7287
return

pkg/sub/clash.go

+60-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package sub
22

33
import (
44
"fmt"
5+
"net"
6+
"sort"
7+
"strings"
58

69
relay_cfg "github.com/Ehco1996/ehco/internal/relay/conf"
710
"gopkg.in/yaml.v3"
@@ -36,6 +39,24 @@ func (c *ClashSub) ToClashConfigYaml() ([]byte, error) {
3639
return yaml.Marshal(c.cCfg)
3740
}
3841

42+
func (c *ClashSub) ToGroupedClashConfigYaml() ([]byte, error) {
43+
groupProxy := c.cCfg.groupByLongestCommonPrefix()
44+
ps := []*Proxies{}
45+
groupNameList := []string{}
46+
for groupName := range groupProxy {
47+
groupNameList = append(groupNameList, groupName)
48+
}
49+
sort.Strings(groupNameList)
50+
for _, groupName := range groupNameList {
51+
proxies := groupProxy[groupName]
52+
// only use first proxy will be show in proxy provider, other will be merged into load balance in relay
53+
p := proxies[0].getOrCreateGroupLeader()
54+
ps = append(ps, p)
55+
}
56+
groupedCfg := &clashConfig{&ps}
57+
return yaml.Marshal(groupedCfg)
58+
}
59+
3960
func (c *ClashSub) Refresh() error {
4061
// get new clash sub by url
4162
newSub, err := NewClashSubByURL(c.URL, c.Name)
@@ -82,13 +103,51 @@ func (c *ClashSub) Refresh() error {
82103

83104
func (c *ClashSub) ToRelayConfigs(listenHost string) ([]*relay_cfg.Config, error) {
84105
relayConfigs := []*relay_cfg.Config{}
106+
// generate relay config for each proxy
85107
for _, proxy := range *c.cCfg.Proxies {
86-
newName := fmt.Sprintf("%s-%s", c.Name, proxy.Name)
108+
var newName string
109+
if strings.HasSuffix(proxy.Name, "-") {
110+
newName = fmt.Sprintf("%s%s", proxy.Name, c.Name)
111+
} else {
112+
newName = fmt.Sprintf("%s-%s", proxy.Name, c.Name)
113+
}
114+
87115
rc, err := proxy.ToRelayConfig(listenHost, newName)
88116
if err != nil {
89117
return nil, err
90118
}
91119
relayConfigs = append(relayConfigs, rc)
92120
}
121+
122+
// generate relay config for each group
123+
groupProxy := c.cCfg.groupByLongestCommonPrefix()
124+
for groupName, proxies := range groupProxy {
125+
// only use first proxy will be show in proxy provider, other will be merged into load balance in relay
126+
groupLeader := proxies[0].getOrCreateGroupLeader()
127+
var newName string
128+
if strings.HasSuffix(groupName, "-") {
129+
newName = fmt.Sprintf("%slb", groupName)
130+
} else {
131+
newName = fmt.Sprintf("%s-lb", groupName)
132+
}
133+
rc, err := groupLeader.ToRelayConfig(listenHost, newName)
134+
if err != nil {
135+
return nil, err
136+
}
137+
138+
// add other proxies in group to relay config
139+
for _, proxy := range proxies[1:] {
140+
remote := net.JoinHostPort(proxy.rawServer, proxy.rawPort)
141+
// skip duplicate remote, because the relay cfg for this leader will be cached when first init
142+
if strInArray(remote, rc.TCPRemotes) {
143+
continue
144+
}
145+
rc.TCPRemotes = append(rc.TCPRemotes, remote)
146+
if proxy.UDP {
147+
rc.UDPRemotes = append(rc.UDPRemotes, remote)
148+
}
149+
}
150+
relayConfigs = append(relayConfigs, rc)
151+
}
93152
return relayConfigs, nil
94153
}

pkg/sub/clash_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,13 @@ func TestNewClashConfig(t *testing.T) {
5151

5252
func TestToRelayConfigs(t *testing.T) {
5353
cs, err := NewClashSub(configBuf, "test", "")
54-
assert.NoError(t, err, "NewConfig should not retur an error")
54+
assert.NoError(t, err, "NewConfig should not return an error")
5555
assert.NotNil(t, cs, "Config should not be nil")
5656

5757
relayConfigs, err := cs.ToRelayConfigs("localhost")
5858
assert.NoError(t, err, "ToRelayConfigs should not return an error")
5959
assert.NotNil(t, relayConfigs, "relayConfigs should not be nil")
60-
expectedRelayCount := 2
60+
expectedRelayCount := 4 // 2 proxy + 2 load balance
6161
assert.Equal(t, expectedRelayCount, len(relayConfigs), "Relay count should match")
6262
l.Info("relayConfigs", zap.Any("relayConfigs", relayConfigs))
6363
}

pkg/sub/clash_types.go

+72-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@ func (cc *clashConfig) GetProxyByRawName(name string) *Proxies {
2121
return nil
2222
}
2323

24+
func (cc *clashConfig) GetProxyByName(name string) *Proxies {
25+
for _, proxy := range *cc.Proxies {
26+
if proxy.Name == name {
27+
return proxy
28+
}
29+
}
30+
return nil
31+
}
32+
2433
func (cc *clashConfig) Adjust() {
2534
for _, proxy := range *cc.Proxies {
2635
if proxy.rawName == "" {
@@ -31,6 +40,24 @@ func (cc *clashConfig) Adjust() {
3140
}
3241
}
3342

43+
func (cc *clashConfig) groupByLongestCommonPrefix() map[string][]*Proxies {
44+
proxies := cc.Proxies
45+
46+
proxyNameList := []string{}
47+
for _, proxy := range *proxies {
48+
proxyNameList = append(proxyNameList, proxy.Name)
49+
}
50+
groupNameMap := groupByLongestCommonPrefix(proxyNameList)
51+
52+
proxyGroups := make(map[string][]*Proxies)
53+
for groupName, proxyNames := range groupNameMap {
54+
for _, proxyName := range proxyNames {
55+
proxyGroups[groupName] = append(proxyGroups[groupName], cc.GetProxyByName(proxyName))
56+
}
57+
}
58+
return proxyGroups
59+
}
60+
3461
type Proxies struct {
3562
// basic fields
3663
Name string `yaml:"name"`
@@ -62,6 +89,8 @@ type Proxies struct {
6289
rawServer string
6390
rawPort string
6491
relayCfg *relay_cfg.Config
92+
93+
groupLeader *Proxies
6594
}
6695

6796
func (p *Proxies) Different(new *Proxies) bool {
@@ -110,7 +139,7 @@ func (p *Proxies) ToRelayConfig(listenHost string, newName string) (*relay_cfg.C
110139
listenAddr := net.JoinHostPort(listenHost, strconv.Itoa(listenPort))
111140
remoteAddr := net.JoinHostPort(p.Server, p.Port)
112141
r := &relay_cfg.Config{
113-
Label: p.Name,
142+
Label: newName,
114143
ListenType: constant.Listen_RAW,
115144
TransportType: constant.Transport_RAW,
116145
Listen: listenAddr,
@@ -129,3 +158,45 @@ func (p *Proxies) ToRelayConfig(listenHost string, newName string) (*relay_cfg.C
129158
p.relayCfg = r
130159
return r, nil
131160
}
161+
162+
func (p *Proxies) Clone() *Proxies {
163+
cloned := &Proxies{
164+
Name: p.Name,
165+
Type: p.Type,
166+
Server: p.Server,
167+
Port: p.Port,
168+
Password: p.Password,
169+
UDP: p.UDP,
170+
Cipher: p.Cipher,
171+
ALPN: p.ALPN,
172+
SkipCertVerify: p.SkipCertVerify,
173+
SNI: p.SNI,
174+
Network: p.Network,
175+
UserName: p.UserName,
176+
TLS: p.TLS,
177+
UUID: p.UUID,
178+
AlterID: p.AlterID,
179+
ServerName: p.ServerName,
180+
181+
rawName: p.rawName,
182+
rawServer: p.rawServer,
183+
rawPort: p.rawPort,
184+
}
185+
if p.relayCfg != nil {
186+
cloned.relayCfg = p.relayCfg.Clone()
187+
}
188+
return cloned
189+
}
190+
191+
func (p *Proxies) getOrCreateGroupLeader() *Proxies {
192+
if p.groupLeader != nil {
193+
return p.groupLeader
194+
}
195+
p.groupLeader = p.Clone()
196+
// reset name,port,and server to raw
197+
p.groupLeader.Name = p.rawName
198+
p.groupLeader.Port = p.rawPort
199+
p.groupLeader.Server = p.rawServer
200+
p.groupLeader.relayCfg = nil
201+
return p.groupLeader
202+
}

pkg/sub/utils.go

+78-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ import (
55
"io"
66
"net"
77
"net/http"
8+
"sort"
9+
"strings"
10+
"time"
11+
)
12+
13+
var (
14+
client = http.Client{Timeout: time.Second * 10}
815
)
916

1017
func getFreePortInBatch(host string, count int) ([]int, error) {
@@ -26,7 +33,7 @@ func getFreePortInBatch(host string, count int) ([]int, error) {
2633
}
2734

2835
func getHttpBody(url string) ([]byte, error) {
29-
resp, err := http.Get(url)
36+
resp, err := client.Get(url)
3037
if err != nil {
3138
msg := fmt.Sprintf("http get sub config url=%s meet err=%v", url, err)
3239
return nil, fmt.Errorf(msg)
@@ -43,3 +50,73 @@ func getHttpBody(url string) ([]byte, error) {
4350
}
4451
return body, nil
4552
}
53+
54+
func longestCommonPrefix(s string, t string) string {
55+
runeS := []rune(s)
56+
runeT := []rune(t)
57+
58+
i := 0
59+
for i = 0; i < len(runeS) && i < len(runeT); i++ {
60+
if runeS[i] != runeT[i] {
61+
return string(runeS[:i])
62+
}
63+
}
64+
return string(runeS[:i])
65+
}
66+
67+
func groupByLongestCommonPrefix(strList []string) map[string][]string {
68+
sort.Strings(strList)
69+
70+
grouped := make(map[string][]string)
71+
72+
// 先找出有相同前缀的字符串
73+
for i := 0; i < len(strList); i++ {
74+
for j := i + 1; j < len(strList); j++ {
75+
prefix := longestCommonPrefix(strList[i], strList[j])
76+
if prefix == "" {
77+
continue
78+
}
79+
if _, ok := grouped[prefix]; !ok {
80+
grouped[prefix] = []string{}
81+
}
82+
}
83+
}
84+
85+
// 过滤掉有相同前缀的前缀中较短的
86+
for prefix := range grouped {
87+
for otherPrefix := range grouped {
88+
if prefix == otherPrefix {
89+
continue
90+
}
91+
if strings.HasPrefix(otherPrefix, prefix) {
92+
delete(grouped, prefix)
93+
}
94+
}
95+
}
96+
97+
// 将字符串分组
98+
for _, proxy := range strList {
99+
foundPrefix := false
100+
for prefix := range grouped {
101+
if strings.HasPrefix(proxy, prefix) {
102+
grouped[prefix] = append(grouped[prefix], proxy)
103+
foundPrefix = true
104+
break
105+
}
106+
}
107+
// 处理没有相同前缀的字符串,自己是一个分组
108+
if !foundPrefix {
109+
grouped[proxy] = []string{proxy}
110+
}
111+
}
112+
return grouped
113+
}
114+
115+
func strInArray(ele string, array []string) bool {
116+
for _, v := range array {
117+
if v == ele {
118+
return true
119+
}
120+
}
121+
return false
122+
}

0 commit comments

Comments
 (0)