feat(sub): emit socks:// links for clients on socks/mixed inbounds
GetLink had no socks-family case, so an attached client's subscription contained no link for the inbound at all, and getInboundsBySubId's protocol list omitted socks/mixed entirely, so such a subscription 404'd when the SK5 inbound was the client's only one. genSocksLink emits socks://user:pass@host:port with the same credential pair the core config carries — user is the client email, pass is the client password, both percent-encoded. The inbound's own fixed account is an operator credential and is never emitted.
This commit is contained in:
+28
-2
@@ -484,7 +484,7 @@ func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error)
|
||||
JOIN client_inbounds ON client_inbounds.inbound_id = inbounds.id
|
||||
JOIN clients ON clients.id = client_inbounds.client_id
|
||||
WHERE
|
||||
inbounds.protocol in ('vmess','vless','trojan','shadowsocks','hysteria','wireguard','amneziawg','mtproto')
|
||||
inbounds.protocol in ('vmess','vless','trojan','shadowsocks','hysteria','wireguard','amneziawg','mtproto','socks','mixed')
|
||||
AND clients.sub_id = ? AND inbounds.enable = ?
|
||||
)`, subId, true).Order("sub_sort_index ASC").Order("id ASC").Find(&inbounds).Error
|
||||
if err != nil {
|
||||
@@ -617,7 +617,7 @@ func mergeStreamFromMaster(childStream, masterStream string) string {
|
||||
|
||||
// GetLink dispatches to the protocol-specific generator for one (inbound, client)
|
||||
// pair. Returns "" when the inbound's protocol doesn't produce a subscription URL
|
||||
// (socks, http, mixed, wireguard, dokodemo, tunnel). The returned string may
|
||||
// (http, wireguard, dokodemo, tunnel). The returned string may
|
||||
// contain multiple `\n`-separated URLs when the inbound has externalProxy set.
|
||||
func (s *SubService) GetLink(inbound *model.Inbound, email string) string {
|
||||
switch inbound.Protocol {
|
||||
@@ -629,6 +629,8 @@ func (s *SubService) GetLink(inbound *model.Inbound, email string) string {
|
||||
return s.genTrojanLink(inbound, email)
|
||||
case "shadowsocks":
|
||||
return s.genShadowsocksLink(inbound, email)
|
||||
case "socks", "mixed":
|
||||
return s.genSocksLink(inbound, email)
|
||||
case "hysteria":
|
||||
return s.genHysteriaLink(inbound, email)
|
||||
case "mtproto":
|
||||
@@ -1126,6 +1128,30 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
||||
return buildLinkWithParams(link, params, s.genRemark(inbound, email, "", streamNetwork))
|
||||
}
|
||||
|
||||
// genSocksLink builds socks://user:pass@host:port for a client attached
|
||||
// to a socks-family inbound: the account name is the client email and
|
||||
// the password is the client's password, exactly the pair the generated
|
||||
// core config carries in settings.accounts. Userinfo parts are
|
||||
// percent-encoded (client emails contain '@'). Inbounds with no client
|
||||
// attached produce no link — the inbound's own fixed account is an
|
||||
// operator credential, not a subscription identity.
|
||||
func (s *SubService) genSocksLink(inbound *model.Inbound, email string) string {
|
||||
if inbound.Protocol != model.Mixed && inbound.Protocol != model.Protocol("socks") {
|
||||
return ""
|
||||
}
|
||||
address := s.resolveInboundAddress(inbound)
|
||||
client, ok := s.clientForLink(inbound, email)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
userInfo := encodeUserinfo(client.Email)
|
||||
if client.Password != "" {
|
||||
userInfo += ":" + encodeUserinfo(client.Password)
|
||||
}
|
||||
link := fmt.Sprintf("socks://%s@%s", userInfo, joinHostPort(address, inbound.Port))
|
||||
return buildLinkWithParams(link, nil, s.genRemark(inbound, email, "", "tcp"))
|
||||
}
|
||||
|
||||
func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) string {
|
||||
if inbound.Protocol != model.Hysteria {
|
||||
return ""
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
)
|
||||
|
||||
// Socks-family inbounds produce a per-client socks:// link: the account
|
||||
// name is the client email, the password is the client's password — the
|
||||
// same pair the generated core config carries in settings.accounts.
|
||||
func TestGetLink_SocksFamily(t *testing.T) {
|
||||
seedSubDB(t)
|
||||
db := database.GetDB()
|
||||
settings := `{"auth":"password","accounts":[{"user":"fixed@landing","pass":"fixed-pass"}],"udp":true,` +
|
||||
`"clients":[{"email":"user1@x","password":"pass-1","subId":"sub1","enable":true}]}`
|
||||
ib := &model.Inbound{
|
||||
UserId: 1, Tag: "sk5", Enable: true, Listen: "203.0.113.9", Port: 32080,
|
||||
Protocol: model.Mixed, Remark: "sk5-mixed", Settings: settings,
|
||||
}
|
||||
if err := db.Create(ib).Error; err != nil {
|
||||
t.Fatalf("seed inbound: %v", err)
|
||||
}
|
||||
|
||||
svc := NewSubService("")
|
||||
svc.PrepareForRequest("req.example.com")
|
||||
|
||||
link := svc.GetLink(ib, "user1@x")
|
||||
wantPrefix := "socks://user1%40x:pass-1@203.0.113.9:32080"
|
||||
if !strings.HasPrefix(link, wantPrefix) {
|
||||
t.Fatalf("socks link = %q, want prefix %q", link, wantPrefix)
|
||||
}
|
||||
if strings.Contains(link, "fixed@landing") || strings.Contains(link, "fixed-pass") {
|
||||
t.Fatalf("the inbound's fixed account must never leak into a client link: %q", link)
|
||||
}
|
||||
|
||||
if got := svc.GetLink(ib, "nobody@x"); got != "" {
|
||||
t.Fatalf("unknown client must produce no link, got %q", got)
|
||||
}
|
||||
|
||||
links := svc.inboundLinks(ib)
|
||||
if len(links) != 1 || !strings.HasPrefix(links[0], wantPrefix) {
|
||||
t.Fatalf("inboundLinks must enumerate the socks client link, got %v", links)
|
||||
}
|
||||
}
|
||||
|
||||
// The full subscription path must resolve a socks-family inbound too:
|
||||
// getInboundsBySubId gates on a protocol list that historically omitted
|
||||
// socks/mixed, which made such subscriptions 404 even with a link
|
||||
// generator in place.
|
||||
func TestGetSubs_SocksFamily(t *testing.T) {
|
||||
seedSubDB(t)
|
||||
db := database.GetDB()
|
||||
settings := `{"auth":"password","accounts":[{"user":"fixed@landing","pass":"fixed-pass"}],"udp":true,` +
|
||||
`"clients":[{"email":"user1@x","password":"pass-1","subId":"sub1","enable":true}]}`
|
||||
ib := &model.Inbound{
|
||||
UserId: 1, Tag: "sk5", Enable: true, Listen: "203.0.113.9", Port: 32080,
|
||||
Protocol: model.Mixed, Remark: "sk5-mixed", Settings: settings,
|
||||
}
|
||||
if err := db.Create(ib).Error; err != nil {
|
||||
t.Fatalf("seed inbound: %v", err)
|
||||
}
|
||||
rec := &model.ClientRecord{Email: "user1@x", SubID: "sub1", Password: "pass-1", Enable: true}
|
||||
if err := db.Create(rec).Error; err != nil {
|
||||
t.Fatalf("seed client: %v", err)
|
||||
}
|
||||
if err := db.Create(&model.ClientInbound{ClientId: rec.Id, InboundId: ib.Id}).Error; err != nil {
|
||||
t.Fatalf("seed link: %v", err)
|
||||
}
|
||||
|
||||
svc := NewSubService("")
|
||||
links, _, _, _, err := svc.GetSubs("sub1", "req.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GetSubs: %v", err)
|
||||
}
|
||||
if len(links) != 1 || !strings.HasPrefix(links[0], "socks://user1%40x:pass-1@203.0.113.9:32080") {
|
||||
t.Fatalf("GetSubs must return the socks link, got %v", links)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLink_LegacySocksProtocol(t *testing.T) {
|
||||
seedSubDB(t)
|
||||
db := database.GetDB()
|
||||
settings := `{"auth":"password","accounts":[],"clients":[{"email":"user1@x","password":"pass-1","subId":"sub1","enable":true}]}`
|
||||
ib := &model.Inbound{
|
||||
UserId: 1, Tag: "sk5-old", Enable: true, Listen: "203.0.113.9", Port: 32081,
|
||||
Protocol: model.Protocol("socks"), Remark: "sk5-old", Settings: settings,
|
||||
}
|
||||
if err := db.Create(ib).Error; err != nil {
|
||||
t.Fatalf("seed inbound: %v", err)
|
||||
}
|
||||
|
||||
svc := NewSubService("")
|
||||
svc.PrepareForRequest("req.example.com")
|
||||
link := svc.GetLink(ib, "user1@x")
|
||||
if !strings.HasPrefix(link, "socks://user1%40x:pass-1@203.0.113.9:32081") {
|
||||
t.Fatalf("legacy socks link = %q", link)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user