Implement Mihomo external-controller support in configuration and gateway
Publish telemt-api gateway Docker image / test (push) Successful in 34s
Publish telemt-api gateway Docker image / build-and-push (push) Successful in 2m55s

- Added optional Mihomo configuration fields in `config.compose.yaml` and `config.example.yaml` for enhanced integration with the Mihomo external-controller.
- Updated the `Gateway` to handle Mihomo API requests, including proxying and error handling for Mihomo-specific endpoints.
- Enhanced the documentation in `GATEWAY_RUN.md` to guide users on configuring Mihomo integration.
- Introduced new utility functions in the web client for interacting with Mihomo API endpoints, improving the overall user experience.
- Updated the sidebar in the Svelte components to include a link to the Mihomo section, enhancing navigation.
This commit is contained in:
Denozordec
2026-03-31 00:32:19 +07:00
parent bbd9619290
commit 2d7b06260e
17 changed files with 1093 additions and 9 deletions
+74 -5
View File
@@ -49,6 +49,16 @@ type Server struct {
BaseURL string `yaml:"base_url"`
PathPrefix string `yaml:"path_prefix"`
AuthorizationEnv string `yaml:"authorization_env"`
// Mihomo external-controller (optional): REST + WebSocket at controller root.
MihomoBaseURL string `yaml:"mihomo_base_url"`
MihomoBaseURLEnv string `yaml:"mihomo_base_url_env"`
MihomoAuthorizationEnv string `yaml:"mihomo_authorization_env"`
}
// Mihomo holds resolved external-controller upstream for a server alias.
type Mihomo struct {
Base *url.URL
Auth string // full Authorization header value (e.g. Bearer <secret>)
}
// Load reads and validates configuration from path.
@@ -79,6 +89,9 @@ func (c *Config) Validate() error {
s.BaseURL = strings.TrimSpace(s.BaseURL)
s.PathPrefix = strings.TrimSpace(s.PathPrefix)
s.AuthorizationEnv = strings.TrimSpace(s.AuthorizationEnv)
s.MihomoBaseURL = strings.TrimSpace(s.MihomoBaseURL)
s.MihomoBaseURLEnv = strings.TrimSpace(s.MihomoBaseURLEnv)
s.MihomoAuthorizationEnv = strings.TrimSpace(s.MihomoAuthorizationEnv)
if s.Alias == "" {
return fmt.Errorf("servers[%d]: alias is required", i)
}
@@ -106,6 +119,19 @@ func (c *Config) Validate() error {
if !strings.HasPrefix(s.PathPrefix, "/") {
s.PathPrefix = "/" + s.PathPrefix
}
hasMihomoURL := s.MihomoBaseURL != "" || s.MihomoBaseURLEnv != ""
if hasMihomoURL && s.MihomoAuthorizationEnv == "" {
return fmt.Errorf("servers[%d]: mihomo_authorization_env is required when mihomo url is set", i)
}
if !hasMihomoURL && s.MihomoAuthorizationEnv != "" {
return fmt.Errorf("servers[%d]: mihomo_base_url or mihomo_base_url_env is required when mihomo_authorization_env is set", i)
}
if s.MihomoBaseURL != "" {
mu, err := url.Parse(s.MihomoBaseURL)
if err != nil || mu.Scheme == "" || mu.Host == "" {
return fmt.Errorf("servers[%d]: invalid mihomo_base_url %q", i, s.MihomoBaseURL)
}
}
}
if len(c.Servers) == 0 {
return fmt.Errorf("at least one server entry is required")
@@ -160,6 +186,7 @@ type Parsed struct {
Trusted []netip.Prefix
ByAlias map[string]*Server
AuthByAlias map[string]string // non-empty Authorization value per alias
MihomoByAlias map[string]*Mihomo
}
// Parse compiles CIDRs and resolves authorization from environment.
@@ -182,6 +209,7 @@ func (c *Config) Parse() (*Parsed, error) {
}
by := make(map[string]*Server, len(c.Servers))
auth := make(map[string]string)
mihomo := make(map[string]*Mihomo)
for i := range c.Servers {
s := &c.Servers[i]
by[s.Alias] = s
@@ -192,12 +220,53 @@ func (c *Config) Parse() (*Parsed, error) {
}
auth[s.Alias] = v
}
mu, err := resolveMihomo(s)
if err != nil {
return nil, err
}
if mu != nil {
mihomo[s.Alias] = mu
}
}
return &Parsed{
Config: c,
Whitelist: wl,
Trusted: tr,
ByAlias: by,
AuthByAlias: auth,
Config: c,
Whitelist: wl,
Trusted: tr,
ByAlias: by,
AuthByAlias: auth,
MihomoByAlias: mihomo,
}, nil
}
// resolveMihomo returns non-nil only when Mihomo is configured for this server.
func resolveMihomo(s *Server) (*Mihomo, error) {
hasURL := s.MihomoBaseURL != "" || s.MihomoBaseURLEnv != ""
if !hasURL {
return nil, nil
}
if s.MihomoAuthorizationEnv == "" {
return nil, fmt.Errorf("server %q: mihomo_authorization_env is required when mihomo url is set", s.Alias)
}
var urlStr string
if s.MihomoBaseURLEnv != "" {
v := strings.TrimSpace(os.Getenv(s.MihomoBaseURLEnv))
if v != "" {
urlStr = v
}
}
if urlStr == "" && s.MihomoBaseURL != "" {
urlStr = s.MihomoBaseURL
}
if urlStr == "" {
return nil, fmt.Errorf("server %q: mihomo controller url is empty (set mihomo_base_url or env %q)", s.Alias, s.MihomoBaseURLEnv)
}
u, err := url.Parse(urlStr)
if err != nil || u.Scheme == "" || u.Host == "" {
return nil, fmt.Errorf("server %q: invalid mihomo controller url %q", s.Alias, urlStr)
}
authVal := os.Getenv(s.MihomoAuthorizationEnv)
if strings.TrimSpace(authVal) == "" {
return nil, fmt.Errorf("server %q: env %q is empty or unset", s.Alias, s.MihomoAuthorizationEnv)
}
return &Mihomo{Base: u, Auth: authVal}, nil
}