Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: proxy to lb #283

Merged
merged 7 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/constant/constant.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ const WelcomeHTML = `
<ul>
{{range .SubConfigs}}
<li><a href="/clash_proxy_provider/?sub_name={{.Name}}">{{.Name}}</a></li>
<li><a href="/clash_proxy_provider/?sub_name={{.Name}}&grouped=true">{{.Name}}-lb</a></li>
{{end}}
</ul>
{{end}}
Expand Down
11 changes: 11 additions & 0 deletions internal/relay/conf/cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,14 @@ func (r *Config) Validate() error {
}
return nil
}

func (r *Config) Clone() *Config {
return &Config{
Listen: r.Listen,
ListenType: r.ListenType,
TransportType: r.TransportType,
TCPRemotes: r.TCPRemotes,
UDPRemotes: r.UDPRemotes,
Label: r.Label,
}
}
17 changes: 16 additions & 1 deletion internal/web/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ func (s *Server) HandleClashProxyProvider(w http.ResponseWriter, r *http.Request
writerBadRequestMsg(w, msg)
return
}
grouped := r.URL.Query().Get("grouped")
if grouped == "true" {
handleClashProxyProvider(s, w, r, subName, true)
} else {
handleClashProxyProvider(s, w, r, subName, false)
}
}

func handleClashProxyProvider(s *Server, w http.ResponseWriter, r *http.Request, subName string, grouped bool) {
if s.relayServerReloader != nil {
if err := s.relayServerReloader.Reload(); err != nil {
writerBadRequestMsg(w, err.Error())
Expand All @@ -66,7 +75,13 @@ func (s *Server) HandleClashProxyProvider(w http.ResponseWriter, r *http.Request
}
for _, clashSub := range clashSubList {
if clashSub.Name == subName {
clashCfgBuf, err := clashSub.ToClashConfigYaml()
var clashCfgBuf []byte
var err error
if grouped {
clashCfgBuf, err = clashSub.ToGroupedClashConfigYaml()
} else {
clashCfgBuf, err = clashSub.ToClashConfigYaml()
}
if err != nil {
writerBadRequestMsg(w, err.Error())
return
Expand Down
61 changes: 60 additions & 1 deletion pkg/sub/clash.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package sub

import (
"fmt"
"net"
"sort"
"strings"

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

func (c *ClashSub) ToGroupedClashConfigYaml() ([]byte, error) {
groupProxy := c.cCfg.groupByLongestCommonPrefix()
ps := []*Proxies{}
groupNameList := []string{}
for groupName := range groupProxy {
groupNameList = append(groupNameList, groupName)
}
sort.Strings(groupNameList)
for _, groupName := range groupNameList {
proxies := groupProxy[groupName]
// only use first proxy will be show in proxy provider, other will be merged into load balance in relay
p := proxies[0].getOrCreateGroupLeader()
ps = append(ps, p)
}
groupedCfg := &clashConfig{&ps}
return yaml.Marshal(groupedCfg)
}

func (c *ClashSub) Refresh() error {
// get new clash sub by url
newSub, err := NewClashSubByURL(c.URL, c.Name)
Expand Down Expand Up @@ -82,13 +103,51 @@ func (c *ClashSub) Refresh() error {

func (c *ClashSub) ToRelayConfigs(listenHost string) ([]*relay_cfg.Config, error) {
relayConfigs := []*relay_cfg.Config{}
// generate relay config for each proxy
for _, proxy := range *c.cCfg.Proxies {
newName := fmt.Sprintf("%s-%s", c.Name, proxy.Name)
var newName string
if strings.HasSuffix(proxy.Name, "-") {
newName = fmt.Sprintf("%s%s", proxy.Name, c.Name)
} else {
newName = fmt.Sprintf("%s-%s", proxy.Name, c.Name)
}

rc, err := proxy.ToRelayConfig(listenHost, newName)
if err != nil {
return nil, err
}
relayConfigs = append(relayConfigs, rc)
}

// generate relay config for each group
groupProxy := c.cCfg.groupByLongestCommonPrefix()
for groupName, proxies := range groupProxy {
// only use first proxy will be show in proxy provider, other will be merged into load balance in relay
groupLeader := proxies[0].getOrCreateGroupLeader()
var newName string
if strings.HasSuffix(groupName, "-") {
newName = fmt.Sprintf("%slb", groupName)
} else {
newName = fmt.Sprintf("%s-lb", groupName)
}
rc, err := groupLeader.ToRelayConfig(listenHost, newName)
if err != nil {
return nil, err
}

// add other proxies in group to relay config
for _, proxy := range proxies[1:] {
remote := net.JoinHostPort(proxy.rawServer, proxy.rawPort)
// skip duplicate remote, because the relay cfg for this leader will be cached when first init
if strInArray(remote, rc.TCPRemotes) {
continue
}
rc.TCPRemotes = append(rc.TCPRemotes, remote)
if proxy.UDP {
rc.UDPRemotes = append(rc.UDPRemotes, remote)
}
}
relayConfigs = append(relayConfigs, rc)
}
return relayConfigs, nil
}
4 changes: 2 additions & 2 deletions pkg/sub/clash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ func TestNewClashConfig(t *testing.T) {

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

relayConfigs, err := cs.ToRelayConfigs("localhost")
assert.NoError(t, err, "ToRelayConfigs should not return an error")
assert.NotNil(t, relayConfigs, "relayConfigs should not be nil")
expectedRelayCount := 2
expectedRelayCount := 4 // 2 proxy + 2 load balance
assert.Equal(t, expectedRelayCount, len(relayConfigs), "Relay count should match")
l.Info("relayConfigs", zap.Any("relayConfigs", relayConfigs))
}
73 changes: 72 additions & 1 deletion pkg/sub/clash_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ func (cc *clashConfig) GetProxyByRawName(name string) *Proxies {
return nil
}

func (cc *clashConfig) GetProxyByName(name string) *Proxies {
for _, proxy := range *cc.Proxies {
if proxy.Name == name {
return proxy
}
}
return nil
}

func (cc *clashConfig) Adjust() {
for _, proxy := range *cc.Proxies {
if proxy.rawName == "" {
Expand All @@ -31,6 +40,24 @@ func (cc *clashConfig) Adjust() {
}
}

func (cc *clashConfig) groupByLongestCommonPrefix() map[string][]*Proxies {
proxies := cc.Proxies

proxyNameList := []string{}
for _, proxy := range *proxies {
proxyNameList = append(proxyNameList, proxy.Name)
}
groupNameMap := groupByLongestCommonPrefix(proxyNameList)

proxyGroups := make(map[string][]*Proxies)
for groupName, proxyNames := range groupNameMap {
for _, proxyName := range proxyNames {
proxyGroups[groupName] = append(proxyGroups[groupName], cc.GetProxyByName(proxyName))
}
}
return proxyGroups
}

type Proxies struct {
// basic fields
Name string `yaml:"name"`
Expand Down Expand Up @@ -62,6 +89,8 @@ type Proxies struct {
rawServer string
rawPort string
relayCfg *relay_cfg.Config

groupLeader *Proxies
}

func (p *Proxies) Different(new *Proxies) bool {
Expand Down Expand Up @@ -110,7 +139,7 @@ func (p *Proxies) ToRelayConfig(listenHost string, newName string) (*relay_cfg.C
listenAddr := net.JoinHostPort(listenHost, strconv.Itoa(listenPort))
remoteAddr := net.JoinHostPort(p.Server, p.Port)
r := &relay_cfg.Config{
Label: p.Name,
Label: newName,
ListenType: constant.Listen_RAW,
TransportType: constant.Transport_RAW,
Listen: listenAddr,
Expand All @@ -129,3 +158,45 @@ func (p *Proxies) ToRelayConfig(listenHost string, newName string) (*relay_cfg.C
p.relayCfg = r
return r, nil
}

func (p *Proxies) Clone() *Proxies {
cloned := &Proxies{
Name: p.Name,
Type: p.Type,
Server: p.Server,
Port: p.Port,
Password: p.Password,
UDP: p.UDP,
Cipher: p.Cipher,
ALPN: p.ALPN,
SkipCertVerify: p.SkipCertVerify,
SNI: p.SNI,
Network: p.Network,
UserName: p.UserName,
TLS: p.TLS,
UUID: p.UUID,
AlterID: p.AlterID,
ServerName: p.ServerName,

rawName: p.rawName,
rawServer: p.rawServer,
rawPort: p.rawPort,
}
if p.relayCfg != nil {
cloned.relayCfg = p.relayCfg.Clone()
}
return cloned
}

func (p *Proxies) getOrCreateGroupLeader() *Proxies {
if p.groupLeader != nil {
return p.groupLeader
}
p.groupLeader = p.Clone()
// reset name,port,and server to raw
p.groupLeader.Name = p.rawName
p.groupLeader.Port = p.rawPort
p.groupLeader.Server = p.rawServer
p.groupLeader.relayCfg = nil
return p.groupLeader
}
79 changes: 78 additions & 1 deletion pkg/sub/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ import (
"io"
"net"
"net/http"
"sort"
"strings"
"time"
)

var (
client = http.Client{Timeout: time.Second * 10}
)

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

func getHttpBody(url string) ([]byte, error) {
resp, err := http.Get(url)
resp, err := client.Get(url)
if err != nil {
msg := fmt.Sprintf("http get sub config url=%s meet err=%v", url, err)
return nil, fmt.Errorf(msg)
Expand All @@ -43,3 +50,73 @@ func getHttpBody(url string) ([]byte, error) {
}
return body, nil
}

func longestCommonPrefix(s string, t string) string {
runeS := []rune(s)
runeT := []rune(t)

i := 0
for i = 0; i < len(runeS) && i < len(runeT); i++ {
if runeS[i] != runeT[i] {
return string(runeS[:i])
}
}
return string(runeS[:i])
}

func groupByLongestCommonPrefix(strList []string) map[string][]string {
sort.Strings(strList)

grouped := make(map[string][]string)

// 先找出有相同前缀的字符串
for i := 0; i < len(strList); i++ {
for j := i + 1; j < len(strList); j++ {
prefix := longestCommonPrefix(strList[i], strList[j])
if prefix == "" {
continue
}
if _, ok := grouped[prefix]; !ok {
grouped[prefix] = []string{}
}
}
}

// 过滤掉有相同前缀的前缀中较短的
for prefix := range grouped {
for otherPrefix := range grouped {
if prefix == otherPrefix {
continue
}
if strings.HasPrefix(otherPrefix, prefix) {
delete(grouped, prefix)
}
}
}

// 将字符串分组
for _, proxy := range strList {
foundPrefix := false
for prefix := range grouped {
if strings.HasPrefix(proxy, prefix) {
grouped[prefix] = append(grouped[prefix], proxy)
foundPrefix = true
break
}
}
// 处理没有相同前缀的字符串,自己是一个分组
if !foundPrefix {
grouped[proxy] = []string{proxy}
}
}
return grouped
}

func strInArray(ele string, array []string) bool {
for _, v := range array {
if v == ele {
return true
}
}
return false
}
Loading