Last active
June 20, 2023 09:08
-
-
Save fanpei91/b1c4daf375fd84d7fca079e16e5cf836 to your computer and use it in GitHub Desktop.
博客:https://fanpei91.com/posts/implement-double-proxies-to-cross-firewall-by-using-https/ 增强版:https://github.com/fanpei91/sandwich
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"crypto/tls" | |
"flag" | |
"fmt" | |
"io" | |
"log" | |
"net" | |
"net/http" | |
"net/http/httputil" | |
"net/url" | |
"strings" | |
) | |
const headerSecretKey = "Misha-Secret" | |
const reversedWebsite = "http://mirrors.codec-cluster.org/" | |
type localProxy struct { | |
remoteProxyAddr string | |
secure bool | |
secret string | |
} | |
func (l *localProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { | |
hijacker, _ := rw.(http.Hijacker) | |
client, _, _ := hijacker.Hijack() | |
var remoteProxy net.Conn | |
var err error | |
if l.secure { | |
remoteProxy, err = tls.Dial("tcp", l.remoteProxyAddr, nil) | |
} else { | |
remoteProxy, err = net.Dial("tcp", l.remoteProxyAddr) | |
} | |
if err != nil { | |
return | |
} | |
req.Header.Set(headerSecretKey, l.secret) | |
req.Write(remoteProxy) | |
go transfer(remoteProxy, client) | |
transfer(client, remoteProxy) | |
} | |
type remoteProxy struct { | |
secret string | |
} | |
func (s *remoteProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { | |
secretValue := req.Header.Get(headerSecretKey) | |
req.Header.Del(headerSecretKey) | |
if secretValue == s.secret { | |
s.crossWall(rw, req) | |
return | |
} | |
s.reverseProxy(rw, req) | |
} | |
func (s *remoteProxy) crossWall(rw http.ResponseWriter, req *http.Request) { | |
if strings.Index(req.Host, ":") < 0 || strings.HasSuffix(req.Host, "]") { | |
req.Host += ":80" | |
} | |
hijacker, _ := rw.(http.Hijacker) | |
localProxy, _, _ := hijacker.Hijack() | |
target, err := net.Dial("tcp", req.Host) | |
if err != nil { | |
return | |
} | |
if req.Method == http.MethodConnect { | |
localProxy.Write([]byte(fmt.Sprintf("%s 200 OK\r\n\r\n", req.Proto))) | |
} else { | |
if v := req.Header.Get("Proxy-Connection"); v != "" { | |
req.Header.Del("Proxy-Connection") | |
req.Header.Set("Connection", v) | |
} | |
req.Write(target) | |
} | |
go transfer(localProxy, target) | |
transfer(target, localProxy) | |
} | |
func (s *remoteProxy) reverseProxy(rw http.ResponseWriter, req *http.Request) { | |
u, _ := url.Parse(reversedWebsite) | |
req.URL.Host = u.Host | |
req.URL.Scheme = u.Scheme | |
req.Host = "" | |
httputil.NewSingleHostReverseProxy(u).ServeHTTP(rw, req) | |
} | |
func transfer(dst io.WriteCloser, src io.ReadCloser) { | |
defer dst.Close() | |
defer src.Close() | |
io.Copy(dst, src) | |
} | |
type options struct { | |
typo string | |
remoteProxyAddr string | |
listenAddr string | |
certFile string | |
keyFile string | |
secure bool | |
secret string | |
} | |
func main() { | |
var o options | |
flag.StringVar(&o.typo, "typo", "local", "start local or remote proxy. [local, remote]") | |
flag.StringVar(&o.remoteProxyAddr, "remote-proxy-addr", "yourdomain.com:443", "the remote proxy address to connect to") | |
flag.StringVar(&o.listenAddr, "addr", "127.0.0.1:8080", "listen on given address") | |
flag.StringVar(&o.certFile, "cert", "", "cert file path") | |
flag.StringVar(&o.keyFile, "key", "", "key file path") | |
flag.BoolVar(&o.secure, "secure", true, "secure mode") | |
flag.StringVar(&o.secret, "secret", "4db0f78bd1d9f723a1e8daf97cfb73d5af0777e5", "secret value") | |
flag.Parse() | |
var listener net.Listener | |
var err error | |
if listener, err = net.Listen("tcp", o.listenAddr); err != nil { | |
log.Panic(err) | |
} | |
if o.typo == "local" { | |
err = startLocalProxy(o, listener) | |
} else { | |
err = startRemoteProxy(o, listener) | |
} | |
if err != nil { | |
log.Panic(err) | |
} | |
} | |
func startLocalProxy(o options, listener net.Listener) error { | |
return http.Serve(listener, &localProxy{remoteProxyAddr: o.remoteProxyAddr, secure: o.secure, secret: o.secret}) | |
} | |
func startRemoteProxy(o options, listener net.Listener) error { | |
var err error | |
if o.certFile != "" && o.keyFile != "" { | |
err = http.ServeTLS(listener, &remoteProxy{secret: o.secret}, o.certFile, o.keyFile) | |
} else { | |
err = http.Serve(listener, &remoteProxy{secret: o.secret}) | |
} | |
return err | |
} |
@TimothyYe 已修复
有人成功么?服务端程序总是报错 TLS 错误,我试了好多次了。是不是在部署上有什么细节需要注意的啊?
@EthsoLiu 很可能是证书文件传错了,假设你用的是最新 acme.sh 生成的,传 fullchain.cer 这个。
@fanpei91 真的,我传的就是 fullchain.cer 这个文件。我今天试了一上午。
直接从浏览器访问你域名,也是报这个错?
@fanpei91 不开任何代理,直接浏览器访问我的域名,出现你代码中给的镜像站点, https 也是对的,我可以看到我的证书和日期。如果以 misha 代理,就会出现,服务端输出:
2020/03/27 09:05:39 http: TLS handshake error from 183.250.162.101:20353: tls: first record does not look like a TLS handshake
2020/03/27 09:05:40 http: TLS handshake error from 183.250.162.101:20417: tls: first record does not look like a TLS handshake
2020/03/27 09:05:42 http: TLS handshake error from 183.250.162.101:21185: tls: first record does not look like a TLS handshake
2020/03/27 09:05:44 http: TLS handshake error from 183.250.162.101:22146: tls: first record does not look like a TLS handshake
2020/03/27 09:05:45 http: TLS handshake error from 183.250.162.101:22241: tls: first record does not look like a TLS handshake
2020/03/27 09:05:45 http: TLS handshake error from 183.250.162.101:22369: tls: first record does not look like a TLS handshake
2020/03/27 09:05:47 http: TLS handshake error from 183.250.162.101:23937: tls: first record does not look like a TLS handshake
那看起来是 local proxy 没有指定 -secure=true
了。
我修改这个参数默认为 true 了,可重新用这最新代码再编译一次了,或者手动指定为 true.
@fanpei91 兄弟,可以了,真的是强!还有一点,访问国内网站也会直接走代理是吧?
@ethsonliu 嗯,这么120 行代码的简单的东西,肯定没有区分国内外,不过我用的版本有区分,还有其他更丰富的功能,我可能会写个博客讲如何实现。你可以继续关注,半个月内吧应该。
或者你用 switchyomega 插件来管理哪些都走代理,哪些不走代理。(我没用这个,我的实现比它还要更方便,简单,全面
@fanpei91 必须关注!
学习的时候遇到一个问题,https://gist.github.com/fanpei91/b1c4daf375fd84d7fca079e16e5cf836#file-misha-proxy-go-L44 这行代码为什么要加上,上一句 req.Write(remoteProxy)
不是已经发送了么?
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
导致的结果是请求其他网站的时候,请求头仍然包含了这个crossWall的一长串字符,把这串秘密字符给暴露了……