V1.4.0 Update

This commit is contained in:
ThisIsDara
2026-05-14 12:44:56 +03:30
parent 197ee4441f
commit afc979ef30
8 changed files with 166 additions and 15 deletions
+15 -8
View File
@@ -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 }}
+1
View File
@@ -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
View File
@@ -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,
+5 -1
View File
@@ -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": {},
+48
View File
@@ -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
+2
View File
@@ -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{
+85
View File
@@ -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 == "" {
+5 -1
View File
@@ -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 {