| 79 | } |
| 80 | |
| 81 | func jsonProxyPatcher(path string) func(string, uint16) error { |
| 82 | return func(origHost string, origPort uint16) error { |
| 83 | raw, err := os.ReadFile(path) |
| 84 | if err != nil { |
| 85 | return err |
| 86 | } |
| 87 | var doc map[string]any |
| 88 | if err := json.Unmarshal(raw, &doc); err != nil { |
| 89 | return fmt.Errorf("unparseable JSON: %w", err) |
| 90 | } |
| 91 | |
| 92 | // Xray / v2ray outbounds live under .outbounds[].settings.vnext[] for |
| 93 | // VLESS/VMess, or .outbounds[].settings.servers[] for Trojan/Shadowsocks. |
| 94 | outbounds, ok := doc["outbounds"].([]any) |
| 95 | if !ok || len(outbounds) == 0 { |
| 96 | return errors.New("no outbounds array found") |
| 97 | } |
| 98 | patched := false |
| 99 | for _, ob := range outbounds { |
| 100 | obm, ok := ob.(map[string]any) |
| 101 | if !ok { |
| 102 | continue |
| 103 | } |
| 104 | settings, _ := obm["settings"].(map[string]any) |
| 105 | if settings == nil { |
| 106 | continue |
| 107 | } |
| 108 | if patchVNext(settings, origHost, origPort) || patchServers(settings, origHost, origPort) { |
| 109 | patched = true |
| 110 | } |
| 111 | } |
| 112 | if !patched { |
| 113 | return errors.New("no VLESS/VMess/Trojan outbound with an address field found") |
| 114 | } |
| 115 | |
| 116 | // Backup using the same bytes we parsed — safer than re-reading |
| 117 | // (avoids a TOCTOU where the file changed between read and backup). |
| 118 | if err := os.WriteFile(path+".bak", raw, 0o644); err != nil { |
| 119 | return fmt.Errorf("backup: %w", err) |
| 120 | } |
| 121 | |
| 122 | // Write new. |
| 123 | updated, err := json.MarshalIndent(doc, "", " ") |
| 124 | if err != nil { |
| 125 | return err |
| 126 | } |
| 127 | return os.WriteFile(path, updated, 0o644) |
| 128 | } |
| 129 | } |
| 130 | |
| 131 | // patchVNext rewrites the first vnext[].address/port it finds. Returns true if changed. |
| 132 | func patchVNext(settings map[string]any, origHost string, origPort uint16) bool { |