Skip to content
Open
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
6 changes: 6 additions & 0 deletions .codex/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[mcp_servers.codegraph]
command = "codegraph"
args = [
"serve",
"--mcp",
]
4 changes: 3 additions & 1 deletion arb/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -550,5 +550,7 @@
"geoSkipped": "{name} skipped",
"geoUpdated": "{name} updated",
"secondsCount": "{count} seconds",
"entriesCount": "{count} entries"
"entriesCount": "{count} entries",
"directProfileUpdate": "Use direct connection for profile updates",
"directProfileUpdateDesc": "Use DIRECT instead of the current proxy when updating profiles"
}
4 changes: 3 additions & 1 deletion arb/intl_ja.arb
Original file line number Diff line number Diff line change
Expand Up @@ -550,5 +550,7 @@
"geoSkipped": "{name} スキップ済み",
"geoUpdated": "{name} 更新済み",
"secondsCount": "{count} 秒",
"entriesCount": "{count} エントリ"
"entriesCount": "{count} エントリ",
"directProfileUpdate": "プロファイル更新に直接接続を使用",
"directProfileUpdateDesc": "プロファイル更新時に現在のプロキシではなく DIRECT を使用します"
}
4 changes: 3 additions & 1 deletion arb/intl_ru.arb
Original file line number Diff line number Diff line change
Expand Up @@ -550,5 +550,7 @@
"geoSkipped": "{name} пропущено",
"geoUpdated": "{name} обновлено",
"secondsCount": "{count} секунд",
"entriesCount": "{count} записей"
"entriesCount": "{count} записей",
"directProfileUpdate": "Прямое подключение для обновления профилей",
"directProfileUpdateDesc": "Использовать DIRECT вместо текущего прокси при обновлении профилей"
}
4 changes: 3 additions & 1 deletion arb/intl_zh_CN.arb
Original file line number Diff line number Diff line change
Expand Up @@ -550,5 +550,7 @@
"geoSkipped": "{name} 已跳过",
"geoUpdated": "{name} 已更新",
"secondsCount": "{count} 秒",
"entriesCount": "{count} 个条目"
"entriesCount": "{count} 个条目",
"directProfileUpdate": "订阅更新使用直连",
"directProfileUpdateDesc": "更新订阅时使用 DIRECT,不经过当前代理"
}
6 changes: 6 additions & 0 deletions core/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ func handleAction(action *Action, result ActionResult) {
result.success(value)
})
return
case downloadFileMethod:
data := action.Data.(string)
handleDownloadFile(data, func(value string) {
result.success(value)
})
return
case getConnectionsMethod:
result.success(handleGetConnections())
return
Expand Down
49 changes: 49 additions & 0 deletions core/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import (
"encoding/json"
"errors"
"fmt"
"net/netip"
"os"
"path/filepath"
"runtime"
"sync"
"time"

"github.com/metacubex/mihomo/adapter"
"github.com/metacubex/mihomo/adapter/inbound"
Expand Down Expand Up @@ -41,6 +43,52 @@ var (
debugError = false
)

const tunReadyPollInterval = 100 * time.Millisecond

func waitForTunInterface(ctx context.Context) error {
return waitForTunInterfaceWithInterval(ctx, tunReadyPollInterval)
}

func waitForTunInterfaceWithInterval(ctx context.Context, interval time.Duration) error {
if runtime.GOOS != "linux" {
return nil
}

ticker := time.NewTicker(interval)
defer ticker.Stop()

for {
if isTunInterfaceReady() {
return nil
}

select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
}
}

func isTunInterfaceReady() bool {
runLock.Lock()
needsInterface := currentConfig != nil &&
currentConfig.General != nil &&
currentConfig.General.Tun.Enable &&
currentConfig.General.Tun.AutoDetectInterface
runLock.Unlock()
if !needsInterface {
return true
}

finder := dialer.DefaultInterfaceFinder.Load()
if finder == nil {
return false
}
name := finder.FindInterfaceName(netip.IPv4Unspecified())
return name != "" && name != "<invalid>"
}

func getExternalProvidersRaw() map[string]cp.Provider {
eps := make(map[string]cp.Provider)
for n, p := range tunnel.Providers() {
Expand Down Expand Up @@ -230,6 +278,7 @@ func updateConfig(params *UpdateParams) {
if params.Tun != nil {
general.Tun.Enable = params.Tun.Enable
general.Tun.AutoRoute = *params.Tun.AutoRoute
general.Tun.AutoDetectInterface = *params.Tun.AutoDetectInterface
general.Tun.Device = *params.Tun.Device
general.Tun.RouteAddress = *params.Tun.RouteAddress
general.Tun.DNSHijack = *params.Tun.DNSHijack
Expand Down
26 changes: 20 additions & 6 deletions core/constant.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@ type UpdateParams struct {
}

type tunSchema struct {
Enable bool `yaml:"enable" json:"enable"`
Device *string `yaml:"device" json:"device"`
Stack *constant.TUNStack `yaml:"stack" json:"stack"`
DNSHijack *[]string `yaml:"dns-hijack" json:"dns-hijack"`
AutoRoute *bool `yaml:"auto-route" json:"auto-route"`
RouteAddress *[]netip.Prefix `yaml:"route-address" json:"route-address,omitempty"`
Enable bool `yaml:"enable" json:"enable"`
Device *string `yaml:"device" json:"device"`
Stack *constant.TUNStack `yaml:"stack" json:"stack"`
DNSHijack *[]string `yaml:"dns-hijack" json:"dns-hijack"`
AutoRoute *bool `yaml:"auto-route" json:"auto-route"`
AutoDetectInterface *bool `yaml:"auto-detect-interface" json:"auto-detect-interface"`
RouteAddress *[]netip.Prefix `yaml:"route-address" json:"route-address,omitempty"`
}

type ChangeProxyParams struct {
Expand All @@ -58,6 +59,18 @@ type TestDelayParams struct {
Timeout int64 `json:"timeout"`
}

type DownloadFileParams struct {
URL string `json:"url"`
Path string `json:"path"`
UserAgent string `json:"user-agent"`
}

type DownloadFileResult struct {
ContentDisposition string `json:"content-disposition"`
SubscriptionUserinfo string `json:"subscription-userinfo"`
Error string `json:"error"`
}

type ExternalProvider struct {
Name string `json:"name"`
Type string `json:"type"`
Expand Down Expand Up @@ -87,6 +100,7 @@ const (
getTotalTrafficMethod Method = "getTotalTraffic"
resetTrafficMethod Method = "resetTraffic"
asyncTestDelayMethod Method = "asyncTestDelay"
downloadFileMethod Method = "downloadFile"
getConnectionsMethod Method = "getConnections"
closeConnectionsMethod Method = "closeConnections"
resetConnectionsMethod Method = "resetConnections"
Expand Down
30 changes: 30 additions & 0 deletions core/file_owner_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//go:build !windows

package main

import "os"

func restoreFileOwnership(path string) error {
return restoreFileOwnershipAs(
path,
os.Getuid(),
os.Getgid(),
os.Geteuid(),
os.Getegid(),
os.Chown,
)
}

func restoreFileOwnershipAs(
path string,
uid int,
gid int,
effectiveUID int,
effectiveGID int,
chown func(string, int, int) error,
) error {
if uid == effectiveUID && gid == effectiveGID {
return nil
}
return chown(path, uid, gid)
}
46 changes: 46 additions & 0 deletions core/file_owner_unix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//go:build !windows

package main

import "testing"

func TestRestoreFileOwnershipAsRestoresRealUser(t *testing.T) {
called := false
err := restoreFileOwnershipAs(
"/tmp/profile.yaml",
1000,
1000,
0,
0,
func(path string, uid int, gid int) error {
called = true
if path != "/tmp/profile.yaml" || uid != 1000 || gid != 1000 {
t.Fatalf("unexpected chown arguments: %s %d:%d", path, uid, gid)
}
return nil
},
)
if err != nil {
t.Fatal(err)
}
if !called {
t.Fatal("expected chown to be called")
}
}

func TestRestoreFileOwnershipAsSkipsMatchingUser(t *testing.T) {
err := restoreFileOwnershipAs(
"/tmp/profile.yaml",
1000,
1000,
1000,
1000,
func(string, int, int) error {
t.Fatal("chown should not be called")
return nil
},
)
if err != nil {
t.Fatal(err)
}
}
7 changes: 7 additions & 0 deletions core/file_owner_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//go:build windows

package main

func restoreFileOwnership(string) error {
return nil
}
Loading