mirror of
https://github.com/ThisIsDara/mhr-cfw-go.git
synced 2026-05-17 21:24:36 +03:00
V1.4.0 Update
This commit is contained in:
@@ -47,6 +47,10 @@ jobs:
|
||||
run: |
|
||||
export GOOS=${{ matrix.goos }}
|
||||
export GOARCH=${{ matrix.goarch }}
|
||||
if [ "$GOOS" = "windows" ] && [ "$GOARCH" = "arm64" ]; then
|
||||
echo "Skipping unsupported windows/arm64"
|
||||
exit 0
|
||||
fi
|
||||
go build -ldflags "-s -w" -o mhr-cfw-go-${{ matrix.goos }}-${{ matrix.goarch }} ./cmd/mhr-cfw
|
||||
|
||||
- name: Rename Windows executable
|
||||
@@ -55,6 +59,7 @@ jobs:
|
||||
run: mv mhr-cfw-go-windows-${{ matrix.goarch }} mhr-cfw-go-windows-${{ matrix.goarch }}.exe
|
||||
|
||||
- name: Upload artifact
|
||||
if: matrix.goos != 'windows' || matrix.goarch != 'arm64'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: mhr-cfw-go-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
@@ -84,13 +89,15 @@ jobs:
|
||||
with:
|
||||
files: ./binaries/*
|
||||
body: |
|
||||
## Bug Fixes
|
||||
- Fixed Telegram API connectivity issues
|
||||
- Fixed encoding issues on some websites (brotli/zstd decompression)
|
||||
|
||||
## Improvements
|
||||
- Better compatibility with Google Apps Script relay
|
||||
|
||||
Changelog
|
||||
Improvements
|
||||
- Added startup prewarm to keep Apps Script containers hot for faster first requests.
|
||||
- Added periodic keepalive ping to reduce idle latency spikes.
|
||||
- Tuned HTTP/2 transport with idle and ping timeouts for more stable performance.
|
||||
- Enabled static asset cache fast-path to reduce repeated relay calls.
|
||||
Fixes
|
||||
- Updated version to 1.4.0.
|
||||
- Improved graceful shutdown by closing active client connections on stop.
|
||||
🦢
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -91,6 +91,7 @@ func main() {
|
||||
func runMenu(a *args) error {
|
||||
menu := &tui.Menu{
|
||||
Title: "mhr-cfw",
|
||||
Version: constants.Version,
|
||||
Options: []tui.Option{
|
||||
{Key: 1, Label: "Start proxy", Handler: func() error { return runProxy(a) }},
|
||||
{Key: 2, Label: "Setup wizard", Handler: func() error { return setup.RunInteractiveWizard(a.configPath) }},
|
||||
|
||||
+5
-5
@@ -1,21 +1,21 @@
|
||||
{
|
||||
"auth_key": "changeme",
|
||||
"chunked_download_chunk_size": 524288,
|
||||
"script_id": "changeme",
|
||||
"chunked_download_max_chunks": 256,
|
||||
"chunked_download_max_parallel": 8,
|
||||
"chunked_download_min_size": 5242880,
|
||||
"front_domain": "www.google.com",
|
||||
"google_ip": "216.239.38.120",
|
||||
"hosts": {},
|
||||
"lan_sharing": true,
|
||||
"listen_host": "0.0.0.0",
|
||||
"lan_sharing": false,
|
||||
"listen_host": "127.0.0.1",
|
||||
"listen_port": 8085,
|
||||
"log_level": "INFO",
|
||||
"max_response_body_bytes": 209715200,
|
||||
"mode": "apps_script",
|
||||
"relay_timeout": 25,
|
||||
"script_id": "changeme",
|
||||
"socks5_enabled": false,
|
||||
"chunked_download_chunk_size": 524288,
|
||||
"socks5_enabled": true,
|
||||
"socks5_port": 1080,
|
||||
"tcp_connect_timeout": 10,
|
||||
"tls_connect_timeout": 15,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package constants
|
||||
|
||||
const Version = "1.1.0"
|
||||
const Version = "1.4.0"
|
||||
|
||||
const (
|
||||
MaxRequestBodyBytes = 100 * 1024 * 1024
|
||||
@@ -86,6 +86,10 @@ const (
|
||||
StatsLogTopN = 10
|
||||
)
|
||||
|
||||
const (
|
||||
KeepaliveInterval = 240.0
|
||||
)
|
||||
|
||||
var GoogleDirectExactExclude = map[string]struct{}{
|
||||
"gemini.google.com": {},
|
||||
"aistudio.google.com": {},
|
||||
|
||||
@@ -72,6 +72,8 @@ type DomainFronter struct {
|
||||
coalesce map[string][]chan []byte
|
||||
|
||||
statsStop chan struct{}
|
||||
keepalive *time.Ticker
|
||||
warmOnce sync.Once
|
||||
}
|
||||
|
||||
type pooledConn struct {
|
||||
@@ -128,6 +130,8 @@ func New(cfg config.Config) *DomainFronter {
|
||||
|
||||
f.h2 = h2.New(f.connectHost, f.sniHosts, f.verifySSL)
|
||||
go f.statsLoop()
|
||||
go f.keepaliveLoop()
|
||||
go f.prewarm()
|
||||
return f
|
||||
}
|
||||
|
||||
@@ -164,6 +168,9 @@ func buildSNIPool(frontDomain string, overrides []string) []string {
|
||||
|
||||
func (f *DomainFronter) Close() error {
|
||||
close(f.statsStop)
|
||||
if f.keepalive != nil {
|
||||
f.keepalive.Stop()
|
||||
}
|
||||
if f.h2 != nil {
|
||||
_ = f.h2.Close()
|
||||
}
|
||||
@@ -715,6 +722,47 @@ func (f *DomainFronter) statsLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
func (f *DomainFronter) keepaliveLoop() {
|
||||
f.keepalive = time.NewTicker(time.Duration(constants.KeepaliveInterval) * time.Second)
|
||||
defer f.keepalive.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-f.statsStop:
|
||||
return
|
||||
case <-f.keepalive.C:
|
||||
if strings.TrimSpace(f.authKey) == "" {
|
||||
continue
|
||||
}
|
||||
payload := map[string]any{
|
||||
"m": "HEAD",
|
||||
"u": "http://example.com/",
|
||||
"r": false,
|
||||
"k": f.authKey,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(payload)
|
||||
path := f.execPath("example.com")
|
||||
_, _, _, _ = f.h2.Request(context.Background(), "POST", path, f.httpHost, map[string]string{"content-type": "application/json"}, jsonBody, 20*time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *DomainFronter) prewarm() {
|
||||
f.warmOnce.Do(func() {
|
||||
if strings.TrimSpace(f.authKey) == "" {
|
||||
return
|
||||
}
|
||||
payload := map[string]any{
|
||||
"m": "HEAD",
|
||||
"u": "http://example.com/",
|
||||
"r": false,
|
||||
"k": f.authKey,
|
||||
}
|
||||
jsonBody, _ := json.Marshal(payload)
|
||||
path := f.execPath("example.com")
|
||||
_, _, _, _ = f.h2.Request(context.Background(), "POST", path, f.httpHost, map[string]string{"content-type": "application/json"}, jsonBody, 20*time.Second)
|
||||
})
|
||||
}
|
||||
|
||||
func (f *DomainFronter) logStats() {
|
||||
if len(f.perSite) == 0 {
|
||||
return
|
||||
|
||||
@@ -52,6 +52,8 @@ func (t *Transport) ensure() {
|
||||
}
|
||||
tr := &http2.Transport{
|
||||
AllowHTTP: false,
|
||||
ReadIdleTimeout: 90 * time.Second,
|
||||
PingTimeout: 15 * time.Second,
|
||||
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
|
||||
sni := t.nextSNI()
|
||||
tlsCfg := &tls.Config{
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"net/textproto"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -153,6 +154,8 @@ type Server struct {
|
||||
mu sync.Mutex
|
||||
|
||||
servers []net.Listener
|
||||
conns map[net.Conn]struct{}
|
||||
connMu sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
ctx context.Context
|
||||
}
|
||||
@@ -177,6 +180,7 @@ func NewServer(cfg config.Config) (*Server, error) {
|
||||
mitm: mitm.NewManager(),
|
||||
cache: NewResponseCache(constants.CacheMaxMB),
|
||||
directFailUntil: map[string]time.Time{},
|
||||
conns: map[net.Conn]struct{}{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -214,6 +218,7 @@ func (s *Server) Start(ctx context.Context) error {
|
||||
for _, l := range s.servers {
|
||||
_ = l.Close()
|
||||
}
|
||||
s.closeAllConns()
|
||||
_ = s.fronter.Close()
|
||||
s.wg.Wait()
|
||||
log.Infof("Server stopped")
|
||||
@@ -233,11 +238,37 @@ func (s *Server) acceptLoop(ln net.Listener, handler func(net.Conn)) {
|
||||
s.wg.Add(1)
|
||||
go func() {
|
||||
defer s.wg.Done()
|
||||
s.trackConn(conn)
|
||||
defer s.untrackConn(conn)
|
||||
handler(conn)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) trackConn(conn net.Conn) {
|
||||
s.connMu.Lock()
|
||||
s.conns[conn] = struct{}{}
|
||||
s.connMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Server) untrackConn(conn net.Conn) {
|
||||
s.connMu.Lock()
|
||||
delete(s.conns, conn)
|
||||
s.connMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Server) closeAllConns() {
|
||||
s.connMu.Lock()
|
||||
conns := make([]net.Conn, 0, len(s.conns))
|
||||
for conn := range s.conns {
|
||||
conns = append(conns, conn)
|
||||
}
|
||||
s.connMu.Unlock()
|
||||
for _, conn := range conns {
|
||||
_ = conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleHTTPConn(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
conn.SetDeadline(time.Now().Add(30 * time.Second))
|
||||
@@ -341,7 +372,23 @@ func (s *Server) relayHTTPStream(host string, port int, conn net.Conn) {
|
||||
continue
|
||||
}
|
||||
|
||||
if s.cacheAllowed(method, urlStr, headerMap, body) {
|
||||
if cached := s.cache.Get(urlStr); cached != nil {
|
||||
if origin != "" {
|
||||
cached = injectCORSHeaders(cached, origin)
|
||||
}
|
||||
_, _ = conn.Write(cached)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
response := s.fronter.Relay(method, urlStr, headerMap, body)
|
||||
if s.cacheAllowed(method, urlStr, headerMap, body) {
|
||||
ttl := s.cache.ParseTTL(response, urlStr)
|
||||
if ttl > 0 {
|
||||
s.cache.Put(urlStr, response, ttl)
|
||||
}
|
||||
}
|
||||
if origin != "" {
|
||||
response = injectCORSHeaders(response, origin)
|
||||
}
|
||||
@@ -367,7 +414,23 @@ func (s *Server) handlePlainHTTP(conn net.Conn, reader *bufio.Reader, headers []
|
||||
}
|
||||
|
||||
urlStr := path
|
||||
if s.cacheAllowed(method, urlStr, headerMap, body) {
|
||||
if cached := s.cache.Get(urlStr); cached != nil {
|
||||
if origin != "" {
|
||||
cached = injectCORSHeaders(cached, origin)
|
||||
}
|
||||
_, _ = conn.Write(cached)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response := s.fronter.Relay(method, urlStr, headerMap, body)
|
||||
if s.cacheAllowed(method, urlStr, headerMap, body) {
|
||||
ttl := s.cache.ParseTTL(response, urlStr)
|
||||
if ttl > 0 {
|
||||
s.cache.Put(urlStr, response, ttl)
|
||||
}
|
||||
}
|
||||
if origin != "" {
|
||||
response = injectCORSHeaders(response, origin)
|
||||
}
|
||||
@@ -520,6 +583,28 @@ func headerValue(headers map[string]string, name string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *Server) cacheAllowed(method, urlStr string, headers map[string]string, body []byte) bool {
|
||||
if strings.ToUpper(method) != "GET" || len(body) > 0 {
|
||||
return false
|
||||
}
|
||||
for _, name := range constants.UncacheableHeaderNames {
|
||||
if headerValue(headers, name) != "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
parsed, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
path := strings.ToLower(parsed.Path)
|
||||
for _, ext := range constants.StaticExts {
|
||||
if strings.HasSuffix(path, ext) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func corsPreflight(origin, acrMethod, acrHeaders string) []byte {
|
||||
allowOrigin := origin
|
||||
if allowOrigin == "" {
|
||||
|
||||
@@ -16,6 +16,7 @@ type Option struct {
|
||||
|
||||
type Menu struct {
|
||||
Title string
|
||||
Version string
|
||||
Options []Option
|
||||
}
|
||||
|
||||
@@ -58,7 +59,10 @@ func (m *Menu) render() {
|
||||
borderMid := "╠" + strings.Repeat("═", width+2) + "╣"
|
||||
borderBot := "╚ " + strings.Repeat("═", width) + " ╝"
|
||||
inner := "║" + strings.Repeat(" ", width+2) + "║"
|
||||
tag := "Mhr-Cfw-Go V1.0"
|
||||
tag := "Mhr-Cfw-Go"
|
||||
if strings.TrimSpace(m.Version) != "" {
|
||||
tag = fmt.Sprintf("Mhr-Cfw-Go v%s", m.Version)
|
||||
}
|
||||
link := "https://github.com/ThisIsDara/"
|
||||
|
||||
centerText := func(text string) string {
|
||||
|
||||
Reference in New Issue
Block a user