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: |
|
run: |
|
||||||
export GOOS=${{ matrix.goos }}
|
export GOOS=${{ matrix.goos }}
|
||||||
export GOARCH=${{ matrix.goarch }}
|
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
|
go build -ldflags "-s -w" -o mhr-cfw-go-${{ matrix.goos }}-${{ matrix.goarch }} ./cmd/mhr-cfw
|
||||||
|
|
||||||
- name: Rename Windows executable
|
- name: Rename Windows executable
|
||||||
@@ -55,6 +59,7 @@ jobs:
|
|||||||
run: mv mhr-cfw-go-windows-${{ matrix.goarch }} mhr-cfw-go-windows-${{ matrix.goarch }}.exe
|
run: mv mhr-cfw-go-windows-${{ matrix.goarch }} mhr-cfw-go-windows-${{ matrix.goarch }}.exe
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
|
if: matrix.goos != 'windows' || matrix.goarch != 'arm64'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: mhr-cfw-go-${{ matrix.goos }}-${{ matrix.goarch }}
|
name: mhr-cfw-go-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||||
@@ -84,13 +89,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
files: ./binaries/*
|
files: ./binaries/*
|
||||||
body: |
|
body: |
|
||||||
## Bug Fixes
|
Changelog
|
||||||
- Fixed Telegram API connectivity issues
|
Improvements
|
||||||
- Fixed encoding issues on some websites (brotli/zstd decompression)
|
- Added startup prewarm to keep Apps Script containers hot for faster first requests.
|
||||||
|
- Added periodic keepalive ping to reduce idle latency spikes.
|
||||||
## Improvements
|
- Tuned HTTP/2 transport with idle and ping timeouts for more stable performance.
|
||||||
- Better compatibility with Google Apps Script relay
|
- 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:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ func main() {
|
|||||||
func runMenu(a *args) error {
|
func runMenu(a *args) error {
|
||||||
menu := &tui.Menu{
|
menu := &tui.Menu{
|
||||||
Title: "mhr-cfw",
|
Title: "mhr-cfw",
|
||||||
|
Version: constants.Version,
|
||||||
Options: []tui.Option{
|
Options: []tui.Option{
|
||||||
{Key: 1, Label: "Start proxy", Handler: func() error { return runProxy(a) }},
|
{Key: 1, Label: "Start proxy", Handler: func() error { return runProxy(a) }},
|
||||||
{Key: 2, Label: "Setup wizard", Handler: func() error { return setup.RunInteractiveWizard(a.configPath) }},
|
{Key: 2, Label: "Setup wizard", Handler: func() error { return setup.RunInteractiveWizard(a.configPath) }},
|
||||||
|
|||||||
+5
-5
@@ -1,21 +1,21 @@
|
|||||||
{
|
{
|
||||||
"auth_key": "changeme",
|
"auth_key": "changeme",
|
||||||
"chunked_download_chunk_size": 524288,
|
"script_id": "changeme",
|
||||||
"chunked_download_max_chunks": 256,
|
"chunked_download_max_chunks": 256,
|
||||||
"chunked_download_max_parallel": 8,
|
"chunked_download_max_parallel": 8,
|
||||||
"chunked_download_min_size": 5242880,
|
"chunked_download_min_size": 5242880,
|
||||||
"front_domain": "www.google.com",
|
"front_domain": "www.google.com",
|
||||||
"google_ip": "216.239.38.120",
|
"google_ip": "216.239.38.120",
|
||||||
"hosts": {},
|
"hosts": {},
|
||||||
"lan_sharing": true,
|
"lan_sharing": false,
|
||||||
"listen_host": "0.0.0.0",
|
"listen_host": "127.0.0.1",
|
||||||
"listen_port": 8085,
|
"listen_port": 8085,
|
||||||
"log_level": "INFO",
|
"log_level": "INFO",
|
||||||
"max_response_body_bytes": 209715200,
|
"max_response_body_bytes": 209715200,
|
||||||
"mode": "apps_script",
|
"mode": "apps_script",
|
||||||
"relay_timeout": 25,
|
"relay_timeout": 25,
|
||||||
"script_id": "changeme",
|
"chunked_download_chunk_size": 524288,
|
||||||
"socks5_enabled": false,
|
"socks5_enabled": true,
|
||||||
"socks5_port": 1080,
|
"socks5_port": 1080,
|
||||||
"tcp_connect_timeout": 10,
|
"tcp_connect_timeout": 10,
|
||||||
"tls_connect_timeout": 15,
|
"tls_connect_timeout": 15,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package constants
|
package constants
|
||||||
|
|
||||||
const Version = "1.1.0"
|
const Version = "1.4.0"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MaxRequestBodyBytes = 100 * 1024 * 1024
|
MaxRequestBodyBytes = 100 * 1024 * 1024
|
||||||
@@ -86,6 +86,10 @@ const (
|
|||||||
StatsLogTopN = 10
|
StatsLogTopN = 10
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
KeepaliveInterval = 240.0
|
||||||
|
)
|
||||||
|
|
||||||
var GoogleDirectExactExclude = map[string]struct{}{
|
var GoogleDirectExactExclude = map[string]struct{}{
|
||||||
"gemini.google.com": {},
|
"gemini.google.com": {},
|
||||||
"aistudio.google.com": {},
|
"aistudio.google.com": {},
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ type DomainFronter struct {
|
|||||||
coalesce map[string][]chan []byte
|
coalesce map[string][]chan []byte
|
||||||
|
|
||||||
statsStop chan struct{}
|
statsStop chan struct{}
|
||||||
|
keepalive *time.Ticker
|
||||||
|
warmOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
type pooledConn struct {
|
type pooledConn struct {
|
||||||
@@ -128,6 +130,8 @@ func New(cfg config.Config) *DomainFronter {
|
|||||||
|
|
||||||
f.h2 = h2.New(f.connectHost, f.sniHosts, f.verifySSL)
|
f.h2 = h2.New(f.connectHost, f.sniHosts, f.verifySSL)
|
||||||
go f.statsLoop()
|
go f.statsLoop()
|
||||||
|
go f.keepaliveLoop()
|
||||||
|
go f.prewarm()
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,6 +168,9 @@ func buildSNIPool(frontDomain string, overrides []string) []string {
|
|||||||
|
|
||||||
func (f *DomainFronter) Close() error {
|
func (f *DomainFronter) Close() error {
|
||||||
close(f.statsStop)
|
close(f.statsStop)
|
||||||
|
if f.keepalive != nil {
|
||||||
|
f.keepalive.Stop()
|
||||||
|
}
|
||||||
if f.h2 != nil {
|
if f.h2 != nil {
|
||||||
_ = f.h2.Close()
|
_ = 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() {
|
func (f *DomainFronter) logStats() {
|
||||||
if len(f.perSite) == 0 {
|
if len(f.perSite) == 0 {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ func (t *Transport) ensure() {
|
|||||||
}
|
}
|
||||||
tr := &http2.Transport{
|
tr := &http2.Transport{
|
||||||
AllowHTTP: false,
|
AllowHTTP: false,
|
||||||
|
ReadIdleTimeout: 90 * time.Second,
|
||||||
|
PingTimeout: 15 * time.Second,
|
||||||
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
|
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
|
||||||
sni := t.nextSNI()
|
sni := t.nextSNI()
|
||||||
tlsCfg := &tls.Config{
|
tlsCfg := &tls.Config{
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/url"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -153,6 +154,8 @@ type Server struct {
|
|||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
|
||||||
servers []net.Listener
|
servers []net.Listener
|
||||||
|
conns map[net.Conn]struct{}
|
||||||
|
connMu sync.Mutex
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
}
|
}
|
||||||
@@ -177,6 +180,7 @@ func NewServer(cfg config.Config) (*Server, error) {
|
|||||||
mitm: mitm.NewManager(),
|
mitm: mitm.NewManager(),
|
||||||
cache: NewResponseCache(constants.CacheMaxMB),
|
cache: NewResponseCache(constants.CacheMaxMB),
|
||||||
directFailUntil: map[string]time.Time{},
|
directFailUntil: map[string]time.Time{},
|
||||||
|
conns: map[net.Conn]struct{}{},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,6 +218,7 @@ func (s *Server) Start(ctx context.Context) error {
|
|||||||
for _, l := range s.servers {
|
for _, l := range s.servers {
|
||||||
_ = l.Close()
|
_ = l.Close()
|
||||||
}
|
}
|
||||||
|
s.closeAllConns()
|
||||||
_ = s.fronter.Close()
|
_ = s.fronter.Close()
|
||||||
s.wg.Wait()
|
s.wg.Wait()
|
||||||
log.Infof("Server stopped")
|
log.Infof("Server stopped")
|
||||||
@@ -233,11 +238,37 @@ func (s *Server) acceptLoop(ln net.Listener, handler func(net.Conn)) {
|
|||||||
s.wg.Add(1)
|
s.wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer s.wg.Done()
|
defer s.wg.Done()
|
||||||
|
s.trackConn(conn)
|
||||||
|
defer s.untrackConn(conn)
|
||||||
handler(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) {
|
func (s *Server) handleHTTPConn(conn net.Conn) {
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
conn.SetDeadline(time.Now().Add(30 * time.Second))
|
conn.SetDeadline(time.Now().Add(30 * time.Second))
|
||||||
@@ -341,7 +372,23 @@ func (s *Server) relayHTTPStream(host string, port int, conn net.Conn) {
|
|||||||
continue
|
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)
|
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 != "" {
|
if origin != "" {
|
||||||
response = injectCORSHeaders(response, origin)
|
response = injectCORSHeaders(response, origin)
|
||||||
}
|
}
|
||||||
@@ -367,7 +414,23 @@ func (s *Server) handlePlainHTTP(conn net.Conn, reader *bufio.Reader, headers []
|
|||||||
}
|
}
|
||||||
|
|
||||||
urlStr := path
|
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)
|
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 != "" {
|
if origin != "" {
|
||||||
response = injectCORSHeaders(response, origin)
|
response = injectCORSHeaders(response, origin)
|
||||||
}
|
}
|
||||||
@@ -520,6 +583,28 @@ func headerValue(headers map[string]string, name string) string {
|
|||||||
return ""
|
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 {
|
func corsPreflight(origin, acrMethod, acrHeaders string) []byte {
|
||||||
allowOrigin := origin
|
allowOrigin := origin
|
||||||
if allowOrigin == "" {
|
if allowOrigin == "" {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type Option struct {
|
|||||||
|
|
||||||
type Menu struct {
|
type Menu struct {
|
||||||
Title string
|
Title string
|
||||||
|
Version string
|
||||||
Options []Option
|
Options []Option
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +59,10 @@ func (m *Menu) render() {
|
|||||||
borderMid := "╠" + strings.Repeat("═", width+2) + "╣"
|
borderMid := "╠" + strings.Repeat("═", width+2) + "╣"
|
||||||
borderBot := "╚ " + strings.Repeat("═", width) + " ╝"
|
borderBot := "╚ " + strings.Repeat("═", width) + " ╝"
|
||||||
inner := "║" + strings.Repeat(" ", width+2) + "║"
|
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/"
|
link := "https://github.com/ThisIsDara/"
|
||||||
|
|
||||||
centerText := func(text string) string {
|
centerText := func(text string) string {
|
||||||
|
|||||||
Reference in New Issue
Block a user