diff --git a/README.md b/README.md deleted file mode 100644 index f1cf98e..0000000 --- a/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# IMPORTANT NOTE - -It's a demo project. Don't use for production. - -本项目仅为演示,不保证可用性。 - -# 这是什么 - -备用代理,以防失联,适用于电脑端。 - -与 Nekogram X 的公共代理一致。 - -# 如何自行搭建 WSS 中继 - -需要自行转发 https / websocket (以使用 CDN 为例) - -1. 准备一个域名。如 tg.gov.cn - -2. 根据 [这里](https://github.com/arm64v8a/NekoXProxy/blob/master/tg.go#L30) 的记录,设置相应数量的子域名记录(目前为 8 个) - -如 `a.tg.gov.cn -> 149.154.175.5` `b.tg.gov.cn -> 95.161.76.100` ...... - -3. 开启 CDN 加速,打开 tls 以及 websocket 支持 - -4. 按照顺序构造 payload 得到 `a,b,c,d,e,f,g,h` - -5. 将 payload 进行 base64 编码得到 `YSxiLGMsZCxlLGYsZyxo` - -6. 构造 WS Relay 链接: `wss://tg.gov.cn?payload=YSxiLGMsZCxlLGYsZyxo` - -以上链接纯属虚构,请勿尝试使用。 - -# English translated by Google - -# What is this - -Backup agent, in case of loss of connection, suitable for computer side. - -Consistent with Nekogram X's public agency. - -# How to setup WSS Realy by yourself - -Need to forward https / websocket by yourself (using CDN as an example) - -1. Prepare a domain name. Such as tg.gov.cn - -2. According to the records of [here](https://github.com/arm64v8a/NekoXProxy/blob/master/tg.go#L30), set the corresponding number of subdomain records (currently 8) - -Such as `a.tg.gov.cn -> 149.154.175.5` `b.tg.gov.cn -> 95.161.76.100` ...... - -3. Turn on CDN acceleration, turn on tls and websocket support - -4. Construct the payload in order to get `a, b, c, d, e, f, g, h` - -5. Encode the payload with base64 to get `YSxiLGMsZCxlLGYsZyxo` - -6. Construct WS Relay link: `wss://tg.gov.cn?payload=YSxiLGMsZCxlLGYsZyxo` - -The above link is purely fictitious, please do not try to use it. diff --git a/go.mod b/go.mod index deacac7..5eb86b1 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,14 @@ module NekoXProxy -go 1.16 +go 1.18 require ( - github.com/gorilla/websocket v1.4.2 + github.com/asergeyev/nradix v0.0.0-20170505151046-3872ab85bb56 + nhooyr.io/websocket v1.8.7 + tailscale.com v1.34.2 +) + +require ( + github.com/klauspost/compress v1.15.4 // indirect + golang.org/x/sys v0.3.1-0.20221220025402-2204b6615fb8 // indirect ) diff --git a/go.sum b/go.sum index 59cba31..4295a91 100644 --- a/go.sum +++ b/go.sum @@ -1 +1,73 @@ -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/asergeyev/nradix v0.0.0-20170505151046-3872ab85bb56 h1:Wi5Tgn8K+jDcBYL+dIMS1+qXYH2r7tpRAyBgqrWfQtw= +github.com/asergeyev/nradix v0.0.0-20170505151046-3872ab85bb56/go.mod h1:8BhOLuqtSuT5NZtZMwfvEibi09RO3u79uqfHZzfDTR4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.15.4 h1:1kn4/7MepF/CHmYub99/nNX8az0IJjfSOU/jbnTVfqQ= +github.com/klauspost/compress v1.15.4/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 h1:Ha8xCaq6ln1a+R91Km45Oq6lPXj2Mla6CRJYcuV2h1w= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +go4.org/mem v0.0.0-20210711025021-927187094b94 h1:OAAkygi2Js191AJP1Ds42MhJRgeofeKGjuoUqNp1QC4= +golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.3.1-0.20221220025402-2204b6615fb8 h1:/VqMvhQCyzfuc826eNrpWmMb3AwD2Sxz/HMsYIhwcIs= +golang.org/x/sys v0.3.1-0.20221220025402-2204b6615fb8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +tailscale.com v1.34.2 h1:g6n9HEZjQfTQfXyeiGeuxPPqSQKhtdmXIbyUxd76O9U= +tailscale.com v1.34.2/go.mod h1:xA1XYcaHK0BIyCETEkTuaj3M3uOe/zNJ5G/Sb9oNU3k= diff --git a/main.go b/main.go index dfb92d3..2d83037 100644 --- a/main.go +++ b/main.go @@ -1,46 +1,149 @@ package main import ( + "context" "encoding/base64" "flag" + "fmt" "log" - "net/http" + "math" + "net" + "net/url" + "os" + "strings" "time" -) - -var nekoXProxyString string -var nekoXProxyBaseDomain string -var nekoXProxyDomains []string - -var client *http.Client + "github.com/asergeyev/nradix" + "nhooyr.io/websocket" +) func main() { - listen := flag.String("l", "127.0.0.1:26641", "HttpProxy listen port") - _nekoXProxyString := flag.String("p", "", "NekoX Proxy URL (keep empty if you don't know)") + listen := flag.String("l", "127.0.0.1:26641", "listen address") flag.Parse() - var ok bool - if *_nekoXProxyString != "" { - ok = parseNekoXString(base64.RawURLEncoding.EncodeToString([]byte(*_nekoXProxyString))) - } else { - log.Println("Getting NekoX public proxy...") - ok = parseNekoXString(getNekoXString()) + url := flag.Arg(0) + if url == "" { + fmt.Fprintln(os.Stderr, "NekoX Proxy URL is required") + return } - if !ok { - log.Println("Failed to parse NekoX proxy.") + router, err := NewRouter(url) + if err != nil { + fmt.Fprintln(os.Stderr, err) return } - client = &http.Client{} + dialer := func(ctx context.Context, network, addr string) (net.Conn, error) { + if network != "tcp" { + return nil, fmt.Errorf("tcp only: %s", network) + } + wsurl, found := router.IP2URL(addr) + if !found { + return nil, fmt.Errorf("New DC address found: %s", addr) + } + + dial_ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + c, _, err := websocket.Dial(dial_ctx, wsurl, + &websocket.DialOptions{ + Subprotocols: []string{"binary"}, + // disable compression for faster media download + CompressionMode: websocket.CompressionDisabled, + }) + if err != nil { + return nil, err + } + c.SetReadLimit(math.MaxInt64 - 1) // disable the buggy limitReader + return websocket.NetConn(ctx, c, websocket.MessageBinary), nil + } + + server := Server{Dialer: dialer} + + l, err := net.Listen("tcp", *listen) + if err != nil { + log.Fatal(err) + } + defer l.Close() + log.Println("Telegram Socks5 Proxy started at", *listen) + log.Fatal(server.Serve(l)) +} + +type Router struct { + baseDomain string + subDomainMapper []string + dcMapper *nradix.Tree +} - http.HandleFunc("/", relay) - server := &http.Server{ - Addr: *listen, - WriteTimeout: 10 * time.Second, +func NewRouter(nekoXURL string) (*Router, error) { + url, err := url.ParseRequestURI(nekoXURL) + if err != nil { + return nil, err + } + + pldstr, _ := base64.RawURLEncoding.DecodeString(url.Query().Get("payload")) + + router := Router{ + baseDomain: url.Host, + subDomainMapper: strings.Split(string(pldstr), ","), + dcMapper: nradix.NewTree(0), + } + + router.initDcMapper() + return &router, nil +} + +func (router *Router) initDcMapper() { + router.dcMapper.AddCIDR("149.154.175.5", 1) + router.dcMapper.AddCIDR("95.161.76.100", 2) + router.dcMapper.AddCIDR("149.154.175.100", 3) + router.dcMapper.AddCIDR("149.154.167.91", 4) + router.dcMapper.AddCIDR("149.154.167.92", 4) + router.dcMapper.AddCIDR("149.154.171.5", 5) + router.dcMapper.AddCIDR("2001:b28:f23d:f001::a", 1) + router.dcMapper.AddCIDR("2001:67c:4e8:f002::a", 2) + router.dcMapper.AddCIDR("2001:b28:f23d:f003::a", 3) + router.dcMapper.AddCIDR("2001:67c:4e8:f004::a", 4) + router.dcMapper.AddCIDR("2001:b28:f23f:f005::a", 5) + router.dcMapper.AddCIDR("149.154.161.144", 2) + router.dcMapper.AddCIDR("149.154.167.0/24", 2) + router.dcMapper.AddCIDR("149.154.175.1", 3) + router.dcMapper.AddCIDR("91.108.4.0/24", 4) + router.dcMapper.AddCIDR("149.154.164.0/24", 4) + router.dcMapper.AddCIDR("149.154.165.0/24", 4) + router.dcMapper.AddCIDR("149.154.166.0/24", 4) + router.dcMapper.AddCIDR("91.108.56.0/24", 5) + router.dcMapper.AddCIDR("2001:b28:f23d:f001::d", 1) + router.dcMapper.AddCIDR("2001:67c:4e8:f002::d", 2) + router.dcMapper.AddCIDR("2001:b28:f23d:f003::d", 3) + router.dcMapper.AddCIDR("2001:67c:4e8:f004::d", 4) + router.dcMapper.AddCIDR("2001:b28:f23f:f005::d", 5) + router.dcMapper.AddCIDR("149.154.175.40", 6) + router.dcMapper.AddCIDR("149.154.167.40", 7) + router.dcMapper.AddCIDR("149.154.175.117", 8) + router.dcMapper.AddCIDR("2001:b28:f23d:f001::e", 6) + router.dcMapper.AddCIDR("2001:67c:4e8:f002::e", 7) + router.dcMapper.AddCIDR("2001:b28:f23d:f003::e", 8) + + router.dcMapper.AddCIDR("2001:b28:f23d:f001::b", 1) + router.dcMapper.AddCIDR("2001:67c:4e8:f002::b", 2) + router.dcMapper.AddCIDR("2001:b28:f23d:f003::b", 3) + router.dcMapper.AddCIDR("2001:67c:4e8:f004::b", 4) + router.dcMapper.AddCIDR("2001:b28:f23f:f005::b", 5) + + router.dcMapper.AddCIDR("149.154.175.55", 1) +} + +func (router *Router) IP2URL(addr string) (string, bool) { + ip, _, err := net.SplitHostPort(addr) + if err != nil { + panic(err.Error()) + } + dc, err := router.dcMapper.FindCIDR(ip) + if dc == nil || err != nil { + return "", false } + url := fmt.Sprintf("wss://%s.%s/api", router.subDomainMapper[dc.(int)-1], router.baseDomain) + log.Printf("new connection to DC%d (%s)\n", dc, addr) - log.Println("Telegram HTTP Proxy started at", *listen) - server.ListenAndServe() + return url, true } diff --git a/relay.go b/relay.go deleted file mode 100644 index df61a45..0000000 --- a/relay.go +++ /dev/null @@ -1,40 +0,0 @@ -package main - -import ( - "fmt" - "io" - "net/http" - "strings" -) - -func relay(w http.ResponseWriter, r *http.Request) { - u := r.URL - reqip := strings.Split(u.Host, ":")[0] - wsurl := ip2wsurl(reqip) - // fmt.Println(wsurl) - - var body io.Reader - if r.Method == "POST" { - body = r.Body - } - - req, _ := http.NewRequest(r.Method, wsurl, body) - a, err := client.Do(req) - if err != nil { - w.WriteHeader(502) - fmt.Println(502, err) - return - } - - for k, _ := range a.Header { - w.Header().Set(k, a.Header.Get(k)) - } - - // if a.StatusCode != 200 { - // log.Println(a.StatusCode, reqip, ip2dc(reqip), wsurl) - // } - - w.WriteHeader(a.StatusCode) - - io.Copy(w, a.Body) -} diff --git a/socks5.go b/socks5.go new file mode 100644 index 0000000..8337134 --- /dev/null +++ b/socks5.go @@ -0,0 +1,380 @@ +// FROM: https://github.com/tailscale/tailscale/blob/v1.34.2/net/socks5/socks5.go +// modifications: see DEL: + +// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package socks5 is a SOCKS5 server implementation. +// +// This is used for userspace networking in Tailscale. Specifically, +// this is used for dialing out of the machine to other nodes, without +// the host kernel's involvement, so it doesn't proper routing tables, +// TUN, IPv6, etc. This package is meant to only handle the SOCKS5 protocol +// details and not any integration with Tailscale internals itself. +// +// The glue between this package and Tailscale is in net/socks5/tssocks. +package main + +import ( + "context" + "encoding/binary" + "fmt" + "io" + "log" + "net" + "strconv" + // DEL: "time" + + "tailscale.com/types/logger" +) + +const ( + noAuthRequired byte = 0 + noAcceptableAuth byte = 255 + + // socks5Version is the byte that represents the SOCKS version + // in requests. + socks5Version byte = 5 +) + +// commandType are the bytes sent in SOCKS5 packets +// that represent the kind of connection the client needs. +type commandType byte + +// The set of valid SOCKS5 commands as described in RFC 1928. +const ( + connect commandType = 1 + bind commandType = 2 + udpAssociate commandType = 3 +) + +// addrType are the bytes sent in SOCKS5 packets +// that represent particular address types. +type addrType byte + +// The set of valid SOCKS5 address types as defined in RFC 1928. +const ( + ipv4 addrType = 1 + domainName addrType = 3 + ipv6 addrType = 4 +) + +// replyCode are the bytes sent in SOCKS5 packets +// that represent replies from the server to a client +// request. +type replyCode byte + +// The set of valid SOCKS5 reply types as per the RFC 1928. +const ( + success replyCode = 0 + generalFailure replyCode = 1 + connectionNotAllowed replyCode = 2 + networkUnreachable replyCode = 3 + hostUnreachable replyCode = 4 + connectionRefused replyCode = 5 + ttlExpired replyCode = 6 + commandNotSupported replyCode = 7 + addrTypeNotSupported replyCode = 8 +) + +// Server is a SOCKS5 proxy server. +type Server struct { + // Logf optionally specifies the logger to use. + // If nil, the standard logger is used. + Logf logger.Logf + + // Dialer optionally specifies the dialer to use for outgoing connections. + // If nil, the net package's standard dialer is used. + Dialer func(ctx context.Context, network, addr string) (net.Conn, error) +} + +func (s *Server) dial(ctx context.Context, network, addr string) (net.Conn, error) { + dial := s.Dialer + if dial == nil { + dialer := &net.Dialer{} + dial = dialer.DialContext + } + return dial(ctx, network, addr) +} + +func (s *Server) logf(format string, args ...any) { + logf := s.Logf + if logf == nil { + logf = log.Printf + } + logf(format, args...) +} + +// Serve accepts and handles incoming connections on the given listener. +func (s *Server) Serve(l net.Listener) error { + defer l.Close() + for { + c, err := l.Accept() + if err != nil { + return err + } + go func() { + defer c.Close() + conn := &Socks5Conn{clientConn: c, srv: s} + err := conn.Run() + if err != nil { + s.logf("client connection failed: %v", err) + } + }() + } +} + +// Socks5Conn is a SOCKS5 connection for client to reach +// server. +type Socks5Conn struct { + // The struct is filled by each of the internal + // methods in turn as the transaction progresses. + + srv *Server + clientConn net.Conn + request *request +} + +// Run starts the new connection. +func (c *Socks5Conn) Run() error { + err := parseClientGreeting(c.clientConn) + if err != nil { + c.clientConn.Write([]byte{socks5Version, noAcceptableAuth}) + return err + } + c.clientConn.Write([]byte{socks5Version, noAuthRequired}) + return c.handleRequest() +} + +func (c *Socks5Conn) handleRequest() error { + req, err := parseClientRequest(c.clientConn) + if err != nil { + res := &response{reply: generalFailure} + buf, _ := res.marshal() + c.clientConn.Write(buf) + return err + } + if req.command != connect { + res := &response{reply: commandNotSupported} + buf, _ := res.marshal() + c.clientConn.Write(buf) + return fmt.Errorf("unsupported command %v", req.command) + } + c.request = req + + // DEL: ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + srv, err := c.srv.dial( + ctx, + "tcp", + net.JoinHostPort(c.request.destination, strconv.Itoa(int(c.request.port))), + ) + if err != nil { + res := &response{reply: generalFailure} + buf, _ := res.marshal() + c.clientConn.Write(buf) + return err + } + defer srv.Close() + // DEL: + //serverAddr, serverPortStr, err := net.SplitHostPort(srv.LocalAddr().String()) + //if err != nil { + // return err + //} + //serverPort, _ := strconv.Atoi(serverPortStr) + //var bindAddrType addrType + //if ip := net.ParseIP(serverAddr); ip != nil { + // if ip.To4() != nil { + // bindAddrType = ipv4 + // } else { + // bindAddrType = ipv6 + // } + //} else { + // bindAddrType = domainName + //} + //res := &response{ + // reply: success, + // bindAddrType: bindAddrType, + // bindAddr: serverAddr, + // bindPort: uint16(serverPort), + //} + + res := &response{ + reply: success, + bindAddrType: c.request.destAddrType, + bindAddr: c.request.destination, + bindPort: c.request.port, + } + buf, err := res.marshal() + if err != nil { + res = &response{reply: generalFailure} + buf, _ = res.marshal() + } + c.clientConn.Write(buf) + + errc := make(chan error, 2) + go func() { + _, err := io.Copy(c.clientConn, srv) + if err != nil { + err = fmt.Errorf("from backend to client: %w", err) + } + errc <- err + }() + go func() { + _, err := io.Copy(srv, c.clientConn) + if err != nil { + err = fmt.Errorf("from client to backend: %w", err) + } + errc <- err + }() + return <-errc +} + +// parseClientGreeting parses a request initiation packet +// and returns a slice that contains the acceptable auth methods +// for the client. +func parseClientGreeting(r io.Reader) error { + var hdr [2]byte + _, err := io.ReadFull(r, hdr[:]) + if err != nil { + return fmt.Errorf("could not read packet header") + } + if hdr[0] != socks5Version { + return fmt.Errorf("incompatible SOCKS version") + } + count := int(hdr[1]) + methods := make([]byte, count) + _, err = io.ReadFull(r, methods) + if err != nil { + return fmt.Errorf("could not read methods") + } + for _, m := range methods { + if m == noAuthRequired { + return nil + } + } + return fmt.Errorf("no acceptable auth methods") +} + +// request represents data contained within a SOCKS5 +// connection request packet. +type request struct { + command commandType + destination string + port uint16 + destAddrType addrType +} + +// parseClientRequest converts raw packet bytes into a +// SOCKS5Request struct. +func parseClientRequest(r io.Reader) (*request, error) { + var hdr [4]byte + _, err := io.ReadFull(r, hdr[:]) + if err != nil { + return nil, fmt.Errorf("could not read packet header") + } + cmd := hdr[1] + destAddrType := addrType(hdr[3]) + + var destination string + var port uint16 + + if destAddrType == ipv4 { + var ip [4]byte + _, err = io.ReadFull(r, ip[:]) + if err != nil { + return nil, fmt.Errorf("could not read IPv4 address") + } + destination = net.IP(ip[:]).String() + } else if destAddrType == domainName { + var dstSizeByte [1]byte + _, err = io.ReadFull(r, dstSizeByte[:]) + if err != nil { + return nil, fmt.Errorf("could not read domain name size") + } + dstSize := int(dstSizeByte[0]) + domainName := make([]byte, dstSize) + _, err = io.ReadFull(r, domainName) + if err != nil { + return nil, fmt.Errorf("could not read domain name") + } + destination = string(domainName) + } else if destAddrType == ipv6 { + var ip [16]byte + _, err = io.ReadFull(r, ip[:]) + if err != nil { + return nil, fmt.Errorf("could not read IPv6 address") + } + destination = net.IP(ip[:]).String() + } else { + return nil, fmt.Errorf("unsupported address type") + } + var portBytes [2]byte + _, err = io.ReadFull(r, portBytes[:]) + if err != nil { + return nil, fmt.Errorf("could not read port") + } + port = binary.BigEndian.Uint16(portBytes[:]) + + return &request{ + command: commandType(cmd), + destination: destination, + port: port, + destAddrType: destAddrType, + }, nil +} + +// response contains the contents of +// a response packet sent from the proxy +// to the client. +type response struct { + reply replyCode + bindAddrType addrType + bindAddr string + bindPort uint16 +} + +// marshal converts a SOCKS5Response struct into +// a packet. If res.reply == Success, it may throw an error on +// receiving an invalid bind address. Otherwise, it will not throw. +func (res *response) marshal() ([]byte, error) { + pkt := make([]byte, 4) + pkt[0] = socks5Version + pkt[1] = byte(res.reply) + pkt[2] = 0 // null reserved byte + pkt[3] = byte(res.bindAddrType) + + if res.reply != success { + return pkt, nil + } + + var addr []byte + switch res.bindAddrType { + case ipv4: + addr = net.ParseIP(res.bindAddr).To4() + if addr == nil { + return nil, fmt.Errorf("invalid IPv4 address for binding") + } + case domainName: + if len(res.bindAddr) > 255 { + return nil, fmt.Errorf("invalid domain name for binding") + } + addr = make([]byte, 0, len(res.bindAddr)+1) + addr = append(addr, byte(len(res.bindAddr))) + addr = append(addr, []byte(res.bindAddr)...) + case ipv6: + addr = net.ParseIP(res.bindAddr).To16() + if addr == nil { + return nil, fmt.Errorf("invalid IPv6 address for binding") + } + default: + return nil, fmt.Errorf("unsupported address type") + } + + pkt = append(pkt, addr...) + pkt = binary.BigEndian.AppendUint16(pkt, uint16(res.bindPort)) + + return pkt, nil +} diff --git a/subscribe.go b/subscribe.go deleted file mode 100644 index ec8b0e1..0000000 --- a/subscribe.go +++ /dev/null @@ -1,98 +0,0 @@ -package main - -import ( - "context" - "encoding/base64" - "io/ioutil" - "net/http" - "strings" - "sync/atomic" - "time" -) - -var nekoXSubscriptionDomain = "nachonekodayo.sekai.icu" -var nekoXSubscriptionDohs = []string{ - "https://1.1.1.1/dns-query", - "https://1.0.0.1/dns-query", - "https://101.101.101.101/dns-query", - "https://8.8.8.8/resolve", - "https://8.8.4.4/resolve", - "https://[2606:4700:4700::1111]/dns-query", -} - -var _subscribeGood int32 = 0 -var _subscribeBad int32 = 0 - -func getNekoXString() string { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - - in := make(chan string, len(nekoXSubscriptionDohs)) - out := make(chan string, 0) - - for i := 0; i < 10; i++ { - go getNekoXStringWorker(ctx, in, out, cancel) - } - - go func() { - for _, doh := range nekoXSubscriptionDohs { - in <- doh - } - }() - - return <-out -} - -func getNekoXStringWorker(ctx context.Context, in, out chan string, cancel context.CancelFunc) { - for { - select { - case <-ctx.Done(): - return - case doh := <-in: - ret := getTXTUsingDoH(ctx, doh) - if _, err := base64.RawURLEncoding.DecodeString(ret); err != nil || ret == "" { - // fmt.Println(err, ret, doh) - if atomic.AddInt32(&_subscribeBad, 1) == int32(len(nekoXSubscriptionDohs)) { - cancel() - out <- "" - return - } - continue - } - if atomic.AddInt32(&_subscribeGood, 1) == 1 { - cancel() - out <- ret - } - } - } -} - -func getTXTUsingDoH(ctx context.Context, doh string) string { - dohURL := doh + "?name=" + nekoXSubscriptionDomain + "&type=TXT" - req, _ := http.NewRequestWithContext(ctx, "GET", dohURL, nil) - req.Header.Set("accept", "application/dns-json") - resp, err := http.DefaultClient.Do(req) - if err != nil { - return "" - } - data, _ := ioutil.ReadAll(resp.Body) - data2 := strings.ReplaceAll(string(data), "\\\"", "") - data2 = strings.ReplaceAll(data2, " ", "") - return between(data2, "#NekoXStart#", "#NekoXEnd#") -} - -func between(value string, a string, b string) string { - // Get substring between two strings. - posFirst := strings.Index(value, a) - if posFirst == -1 { - return "" - } - posLast := strings.Index(value, b) - if posLast == -1 { - return "" - } - posFirstAdjusted := posFirst + len(a) - if posFirstAdjusted >= posLast { - return "" - } - return value[posFirstAdjusted:posLast] -} diff --git a/tg.go b/tg.go deleted file mode 100644 index 5594547..0000000 --- a/tg.go +++ /dev/null @@ -1,87 +0,0 @@ -package main - -import ( - "encoding/base64" - "fmt" - "net/url" - "strings" -) - -var mapper = make(map[string]int) - -func parseNekoXString(a string) bool { - // fmt.Println(a) - b, _ := base64.RawURLEncoding.DecodeString(a) - url, err := url.Parse(string(b)) - if err != nil { - return false - } - - pldstr, _ := base64.RawURLEncoding.DecodeString(url.Query().Get("payload")) - plds := strings.Split(string(pldstr), ",") - - nekoXProxyBaseDomain = url.Host - nekoXProxyDomains = append([]string{""}, plds...) - - putmapper := func(ip string, dc int) { - mapper[ip] = dc - } - - putmapper("149.154.175.5", 1) - putmapper("95.161.76.100", 2) - putmapper("149.154.175.100", 3) - putmapper("149.154.167.91", 4) - putmapper("149.154.167.92", 4) - putmapper("149.154.171.5", 5) - putmapper("2001:b28:f23d:f001:0000:0000:0000:000a", 1) - putmapper("2001:67c:4e8:f002:0000:0000:0000:000a", 2) - putmapper("2001:b28:f23d:f003:0000:0000:0000:000a", 3) - putmapper("2001:67c:4e8:f004:0000:0000:0000:000a", 4) - putmapper("2001:b28:f23f:f005:0000:0000:0000:000a", 5) - putmapper("149.154.161.144", 2) - putmapper("149.154.167.", 2) - putmapper("149.154.175.1", 3) - putmapper("91.108.4.", 4) - putmapper("149.154.164.", 4) - putmapper("149.154.165.", 4) - putmapper("149.154.166.", 4) - putmapper("91.108.56.", 5) - putmapper("2001:b28:f23d:f001:0000:0000:0000:000d", 1) - putmapper("2001:67c:4e8:f002:0000:0000:0000:000d", 2) - putmapper("2001:b28:f23d:f003:0000:0000:0000:000d", 3) - putmapper("2001:67c:4e8:f004:0000:0000:0000:000d", 4) - putmapper("2001:b28:f23f:f005:0000:0000:0000:000d", 5) - putmapper("149.154.175.40", 6) - putmapper("149.154.167.40", 7) - putmapper("149.154.175.117", 8) - putmapper("2001:b28:f23d:f001:0000:0000:0000:000e", 6) - putmapper("2001:67c:4e8:f002:0000:0000:0000:000e", 7) - putmapper("2001:b28:f23d:f003:0000:0000:0000:000e", 8) - - return true -} - -func dc2wsurl(dc int) string { - if dc == 0 { - return "" - } - return fmt.Sprintf("https://%s.%s/api", nekoXProxyDomains[dc], nekoXProxyBaseDomain) -} - -func ip2dc(ip string) int { - for k, v := range mapper { - if ip == k { - return v - } - } - for k, v := range mapper { - if strings.HasPrefix(ip, k) { - return v - } - } - return 0 -} - -func ip2wsurl(ip string) string { - return dc2wsurl(ip2dc(ip)) -}