Skip to content

Instantly share code, notes, and snippets.

@ayanamist
Last active August 17, 2021 06:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ayanamist/cb8f2e09fd2f4541052ae4739766acf1 to your computer and use it in GitHub Desktop.
Save ayanamist/cb8f2e09fd2f4541052ae4739766acf1 to your computer and use it in GitHub Desktop.
FCM proxy
package main
import (
"bufio"
"errors"
"flag"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
"os/signal"
"runtime"
"strings"
"syscall"
"time"
"github.com/hashicorp/yamux"
"github.com/polvi/sni"
ss "github.com/shadowsocks/shadowsocks-go/shadowsocks"
"golang.org/x/net/proxy"
)
const (
connectTimeout = 30 * time.Second
)
var (
dialer = &net.Dialer{
Timeout: connectTimeout,
}
ciph *ss.Cipher
proxyDial = dialer.Dial
localPort uint
remotePort uint
)
// httpProxy is a HTTP/HTTPS connect proxy.
type httpProxy struct {
host string
haveAuth bool
username string
password string
forward proxy.Dialer
}
func (s *httpProxy) Dial(network, addr string) (net.Conn, error) {
// Dial and create the https client connection.
c, err := s.forward.Dial("tcp", s.host)
if err != nil {
return nil, err
}
// HACK. http.ReadRequest also does this.
reqURL, err := url.Parse("http://" + addr)
if err != nil {
c.Close()
return nil, err
}
reqURL.Scheme = ""
req, err := http.NewRequest("CONNECT", reqURL.String(), nil)
if err != nil {
c.Close()
return nil, err
}
req.Close = false
if s.haveAuth {
req.SetBasicAuth(s.username, s.password)
}
err = req.Write(c)
if err != nil {
c.Close()
return nil, err
}
resp, err := http.ReadResponse(bufio.NewReader(c), req)
if err != nil {
if resp != nil {
resp.Body.Close()
}
c.Close()
return nil, err
}
resp.Body.Close()
if resp.StatusCode != 200 {
c.Close()
err = fmt.Errorf("connect server using proxy error %s", resp.Status)
return nil, err
}
return c, nil
}
func newClientProxyFunc(srvAddrs []string) func(net.Conn) {
return func(conn net.Conn) {
localConn := conn
defer localConn.Close()
sniHost, localConn, err := sniServerNameFromConn(localConn)
if err != nil {
log.Printf("sniServerNameFromConn from %s error: %v", conn.RemoteAddr(), err)
return
}
log.Printf("sniServernameFromConn got %s from %s", sniHost, localConn.RemoteAddr())
var remoteConn net.Conn
connCh := make(chan net.Conn)
closeCh := make(chan struct{})
connectTimer := time.NewTimer(2 * connectTimeout)
delayTimer := time.NewTimer(0)
LOOP:
for _, srvAddr := range srvAddrs {
select {
case <-delayTimer.C:
delayTimer.Reset(time.Second)
log.Printf("try serverConnect %s from %s", srvAddr, localConn.RemoteAddr())
go serverConnect(localConn.RemoteAddr().String(), srvAddr, connCh, closeCh)
case remoteConn = <-connCh:
delayTimer.Stop()
close(closeCh)
break LOOP
}
}
if remoteConn == nil {
select {
case <-connectTimer.C:
log.Printf("remote conn not got, local conn: %s", localConn.RemoteAddr())
close(closeCh)
return
case remoteConn = <-connCh:
close(closeCh)
}
}
connectTimer.Stop()
defer remoteConn.Close()
go relay(localConn, remoteConn)
relay(remoteConn, localConn)
}
}
func serverConnect(localAddr, remoteAddr string, connCh chan<- net.Conn, closeCh <-chan struct{}) {
remoteConn, err := proxyDial("tcp", remoteAddr)
if err != nil {
log.Printf("dial from %s to %s error: %v", localAddr, remoteAddr, err)
return
}
remoteConn = ss.NewConn(remoteConn, ciph.Copy())
sess, _ := yamux.Client(remoteConn, yamux.DefaultConfig())
if _, err := sess.Ping(); err != nil {
log.Printf("sess.Ping from %s to %s error: %v", localAddr, remoteAddr, err)
sess.Close()
return
}
stream, err := sess.Open()
if err != nil {
log.Printf("sess.Open from %s to %s error: %v", localAddr, remoteAddr, err)
sess.Close()
return
}
select {
case <-closeCh:
sess.Close()
case connCh <- stream:
}
}
func sniServerNameFromConn(conn net.Conn) (string, net.Conn, error) {
host, conn, err := sni.ServerNameFromConn(conn)
if err != nil {
return "", nil, fmt.Errorf("invalid sni: %v", err)
}
if !strings.Contains(host, ":") {
host = fmt.Sprintf("%s:%d", host, remotePort)
}
if !strings.Contains(host, ".google.com:") {
return "", nil, fmt.Errorf("not allowed host: %s", host)
}
return host, conn, nil
}
func serverProxyFunc(localConn net.Conn) {
defer localConn.Close()
localConn = ss.NewConn(localConn, ciph.Copy())
sess, _ := yamux.Server(localConn, yamux.DefaultConfig())
defer sess.Close()
localStream, err := sess.Accept()
if err != nil {
log.Printf("sess.Accept from %s error: %v", localConn.RemoteAddr(), err)
return
}
remoteAddr, localStream, err := sniServerNameFromConn(localStream)
if err != nil {
log.Printf("sniServerNameFromConn from %s error: %v", localConn.RemoteAddr(), err)
return
}
log.Printf("sniConnect got %s from %s", remoteAddr, localConn.RemoteAddr())
remoteConn, err := dialer.Dial("tcp", remoteAddr)
if err != nil {
log.Printf("dial from %s to %s error: %v", localConn.RemoteAddr(), remoteAddr, err)
return
}
defer remoteConn.Close()
if c, ok := remoteConn.(*net.TCPConn); ok {
c.SetNoDelay(true)
}
go relay(localStream, remoteConn)
relay(remoteConn, localStream)
}
func sigQuitHandle() {
ch := make(chan os.Signal, 5)
signal.Notify(ch, syscall.SIGQUIT)
for range ch {
buf := make([]byte, 1024*1024)
n := runtime.Stack(buf, true)
buf = buf[:n]
log.Printf("SIGQUIT got, stack:\n%s", buf)
}
}
func relay(src, dst net.Conn) {
log.Printf("relay %s <-> %s start", src.RemoteAddr(), dst.RemoteAddr())
n, err := io.Copy(dst, src)
log.Printf("relay %s <-> %s %d bytes, error: %v", src.RemoteAddr(), dst.RemoteAddr(), n, err)
src.Close()
dst.Close()
}
type ssProxyDialer struct {
server string
ciph *ss.Cipher
upstreamDialer proxy.Dialer
}
func (s *ssProxyDialer) Dial(network, addr string) (net.Conn, error) {
rawaddr, err := ss.RawAddr(addr)
if err != nil {
return nil, err
}
conn, err := s.upstreamDialer.Dial("tcp", s.server)
if err != nil {
return nil, err
}
if c, ok := conn.(*net.TCPConn); ok {
c.SetNoDelay(true)
}
ssConn := ss.NewConn(conn, s.ciph.Copy())
if _, err := ssConn.Write(rawaddr); err != nil {
conn.Close()
return nil, err
}
log.Printf("dial %s via shadowsocks %s", addr, s.server)
return ssConn, nil
}
func ssDial(u *url.URL, d proxy.Dialer) (proxy.Dialer, error) {
if u.User == nil {
return nil, errors.New("no shadowsocks method")
}
method := u.User.Username()
p, _ := u.User.Password()
ciph, err := ss.NewCipher(method, p)
if err != nil {
return nil, err
}
log.Printf("using proxy ss://%s:%s@%s", method, p, u.Host)
return &ssProxyDialer{
server: u.Host,
ciph: ciph,
upstreamDialer: d,
}, nil
}
func init() {
proxy.RegisterDialerType("http", func(u *url.URL, forward proxy.Dialer) (d proxy.Dialer, err error) {
s := new(httpProxy)
s.host = u.Host
s.forward = forward
if u.User != nil {
s.haveAuth = true
s.username = u.User.Username()
s.password, _ = u.User.Password()
}
return s, nil
})
proxy.RegisterDialerType("ss", ssDial)
}
func main() {
log.SetFlags(log.Lmicroseconds)
go sigQuitHandle()
var (
proxyStr string
shadowPass string
)
flag.StringVar(&proxyStr, "proxy", "", "Set proxy url")
flag.UintVar(&localPort, "port", 5228, "Local port")
flag.UintVar(&remotePort, "remote-port", 5228, "Remote port")
flag.StringVar(&shadowPass, "key", "fcmproxy", "Encrypt key")
flag.Parse()
if proxyStr != "" {
u, err := url.Parse(proxyStr)
if err != nil {
log.Fatalf("invalid proxy url: %v", err)
}
d, err := proxy.FromURL(u, dialer)
if err != nil {
log.Fatalf("parse proxy failed: %v", err)
}
proxyDial = d.Dial
}
ciph, _ = ss.NewCipher("rc4-md5", shadowPass)
srvAddr := flag.Args()
var proxyFunc func(net.Conn)
if len(srvAddr) > 0 {
proxyFunc = newClientProxyFunc(srvAddr)
log.Printf("working on client, servers: %v", srvAddr)
} else {
proxyFunc = serverProxyFunc
log.Printf("working on server")
}
l, err := net.Listen("tcp", fmt.Sprintf(":%d", localPort))
if err != nil {
log.Fatalf("listen: %v", err)
}
for {
conn, err := l.Accept()
if err != nil {
log.Fatalf("accept: %v", err)
}
if c, ok := conn.(*net.TCPConn); ok {
c.SetNoDelay(true)
}
go proxyFunc(conn)
}
}
[Unit]
Description=Google DOH proxy
After=network.target
After=v2ray.service
[Service]
User=doh-proxy
Type=simple
Restart=always
EnvironmentFile=/etc/default/doh-proxy
ExecStart=/usr/local/bin/doh-proxy $ARGS
LimitNOFILE=65536
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
module test/golang/fcmproxy
go 1.16
require (
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
github.com/hashicorp/yamux v0.0.0-20210707203944-259a57b3608c
github.com/polvi/sni v0.0.0-20151108162443-a08e954206fd
github.com/shadowsocks/shadowsocks-go v0.0.0-20200409064450-3e585ff90601
golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e // indirect
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d
golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912 // indirect
)
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864 h1:Y4V+SFe7d3iH+9pJCoeWIOS5/xBJIFsltS7E+KJSsJY=
github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/hashicorp/yamux v0.0.0-20210707203944-259a57b3608c h1:nqkErwUGfpZZMqj29WZ9U/wz2OpJVDuiokLhE/3Y7IQ=
github.com/hashicorp/yamux v0.0.0-20210707203944-259a57b3608c/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
github.com/polvi/sni v0.0.0-20151108162443-a08e954206fd h1:moB2BJUF99MbSLkvr+Y++oiZvadc9ToYX4p5oHRMWlQ=
github.com/polvi/sni v0.0.0-20151108162443-a08e954206fd/go.mod h1:0tydZ0+WbhXnymEaRDY/DzO3GvKQAiYune6xvHRFzSw=
github.com/shadowsocks/shadowsocks-go v0.0.0-20200409064450-3e585ff90601 h1:XU9hik0exChEmY92ALW4l9WnDodxLVS9yOSNh2SizaQ=
github.com/shadowsocks/shadowsocks-go v0.0.0-20200409064450-3e585ff90601/go.mod h1:mttDPaeLm87u74HMrP+n2tugXvIKWcwff/cqSX0lehY=
golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf h1:B2n+Zi5QeYRDAEodEu72OS36gmTWjgpXr2+cWcBW90o=
golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e h1:VvfwVmMH40bpMeizC9/K7ipM5Qjucuu16RWfneFPyhQ=
golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912 h1:uCLL3g5wH2xjxVREVuAbP9JM5PPKjRbXKRa6IBjkzmU=
golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@phoenixxie0
Copy link

当第一个server1重启的时候,境内端会出错退出。错误log
`use of closed network connection
00:42:56.916414 sniServernameFromConn got mtalk.google.com:5228 from 124.226.57.185:56500
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x18 pc=0x46b809]

goroutine 27 [running]:
io.ReadAtLeast(0x0, 0x0, 0xc00001a6f0, 0x10, 0x10, 0x10, 0x53ef20, 0x523401, 0xc00001a6f0)
/usr/local/go/src/io/io.go:310 +0x59
io.ReadFull(0x0, 0x0, 0xc00001a6f0, 0x10, 0x10, 0x10, 0x43be4e, 0xc00002ef58)
/usr/local/go/src/io/io.go:329 +0x58
github.com/shadowsocks/shadowsocks-go/shadowsocks.(*Conn).Read(0xc000072a50, 0xc0000f5000, 0x1000, 0x1000, 0xc0000b6c00, 0x4, 0x5998b0)
/root/go/src/github.com/shadowsocks/shadowsocks-go/shadowsocks/conn.go:96 +0x21d
bufio.(*Reader).Read(0xc000055680, 0xc00001a6e0, 0xc, 0xc, 0xc, 0xc00001a6e0, 0x0)
/usr/local/go/src/bufio/bufio.go:216 +0x22f
io.ReadAtLeast(0x59a0a0, 0xc000055680, 0xc00001a6e0, 0xc, 0xc, 0xc, 0x53ef20, 0xc000055501, 0xc00001a6e0)
/usr/local/go/src/io/io.go:310 +0x88
io.ReadFull(0x59a0a0, 0xc000055680, 0xc00001a6e0, 0xc, 0xc, 0xc, 0x0, 0x0)
/usr/local/go/src/io/io.go:329 +0x58
github.com/hashicorp/yamux.(*Session).recvLoop(0xc0000da0b0, 0x0, 0x0)
/root/go/src/github.com/hashicorp/yamux/session.go:458 +0xef
github.com/hashicorp/yamux.(*Session).recv(0xc0000da0b0)
/root/go/src/github.com/hashicorp/yamux/session.go:437 +0x2b
created by github.com/hashicorp/yamux.newSession
/root/go/src/github.com/hashicorp/yamux/session.go:113 +0x2aa`

@ayanamist
Copy link
Author

看了下栈,没有我自己写的代码,都是三方库的代码,另外看了下报错的地方,不知道是什么go版本,go1.11这里不应该会报错。
实际在我自己环境里测了下,怎么重启也无法复现。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment