fix: replace sing-box with mihomo for egress probe (xhttp support)

mihomo (Clash Meta) natively supports xhttp and all Clash transports.
Build minimal mihomo YAML config directly from the proxy node instead
of converting to sing-box JSON format.
This commit is contained in:
2026-07-28 19:15:11 +08:00
parent 5f0edae6fe
commit 8d396e63d6
2 changed files with 32 additions and 34 deletions
+5 -5
View File
@@ -18,11 +18,11 @@ RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /sub-store .
# ─── Stage 3: Runtime ───
FROM alpine:3.21
RUN apk add --no-cache ca-certificates tzdata && \
wget -qO /tmp/sing-box.tar.gz "https://github.com/SagerNet/sing-box/releases/download/v1.13.14/sing-box-1.13.14-linux-amd64-musl.tar.gz" && \
tar xzf /tmp/sing-box.tar.gz -C /tmp && \
mv /tmp/sing-box-*/sing-box /usr/local/bin/sing-box && \
chmod +x /usr/local/bin/sing-box && \
rm -rf /tmp/sing-box*
wget -qO /tmp/mihomo.gz "https://github.com/MetaCubeX/mihomo/releases/download/v1.19.29/mihomo-linux-amd64-v1.19.29.gz" && \
gunzip /tmp/mihomo.gz && \
mv /tmp/mihomo /usr/local/bin/mihomo && \
chmod +x /usr/local/bin/mihomo && \
rm -rf /tmp/mihomo*
WORKDIR /app
COPY --from=backend-builder /sub-store /app/sub-store
COPY --from=frontend-builder /frontend/dist /app/frontend/dist
+27 -29
View File
@@ -17,9 +17,9 @@ import (
"time"
"github.com/gofiber/fiber/v3"
"gopkg.in/yaml.v3"
"github.com/peterqiu0516/sub-store/internal/model"
"github.com/peterqiu0516/sub-store/internal/render"
"github.com/peterqiu0516/sub-store/internal/util"
)
@@ -134,37 +134,35 @@ func buildEgressProbeConfig(node model.ProxyNode, port int) ([]byte, error) {
}
probeNode["name"] = "PROXY"
outbound := render.ToSingBoxOutbound(probeNode)
if outbound == nil {
return nil, fmt.Errorf("Unsupported proxy node for egress probe")
// Build a minimal mihomo (Clash Meta) config with the probe node as the
// only proxy. mihomo natively supports xhttp and other Clash transports.
proxyYaml, err := yaml.Marshal(probeNode)
if err != nil {
return nil, fmt.Errorf("Failed to marshal proxy node: %w", err)
}
doc := map[string]any{
"log": map[string]any{"level": "warn"},
"inbounds": []any{
map[string]any{
"type": "mixed",
"tag": "mixed-in",
"listen": "127.0.0.1",
"listen_port": port,
},
},
"outbounds": []any{
outbound,
map[string]any{"type": "direct", "tag": "DIRECT"},
},
"route": map[string]any{
"auto_detect_interface": true,
"final": "PROXY",
"rules": []any{map[string]any{"action": "sniff"}},
},
}
return json.Marshal(doc)
config := fmt.Sprintf(`mixed-port: %d
allow-lan: false
mode: direct
log-level: warning
proxies:
- %s
proxy-groups:
- name: PROXY
type: select
proxies:
- PROXY
rules:
- MATCH,PROXY
`, port, strings.ReplaceAll(strings.TrimSuffix(string(proxyYaml), "\n"), "\n", "\n "))
return []byte(config), nil
}
func runEgressProbe(configData []byte, port int) (fiber.Map, error) {
singBox, err := exec.LookPath("sing-box")
mihomo, err := exec.LookPath("mihomo")
if err != nil {
return nil, fmt.Errorf("sing-box executable not found")
return nil, fmt.Errorf("mihomo executable not found")
}
dir, err := os.MkdirTemp("", "sub-store-egress-*")
if err != nil {
@@ -172,14 +170,14 @@ func runEgressProbe(configData []byte, port int) (fiber.Map, error) {
}
defer os.RemoveAll(dir)
configPath := filepath.Join(dir, "config.json")
configPath := filepath.Join(dir, "config.yaml")
if err := os.WriteFile(configPath, configData, 0o600); err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, singBox, "run", "-c", configPath)
cmd := exec.CommandContext(ctx, mihomo, "-f", configPath)
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Start(); err != nil {