add new file

This commit is contained in:
he liu
2022-01-23 17:30:38 +08:00
parent 05b2e55f39
commit c482967f8c
125 changed files with 9688 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
package process
import (
"bufio"
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/logger"
"encoding/base64"
"github.com/pkg/errors"
"go.uber.org/zap"
"net/http"
"strconv"
"strings"
)
type HttpProxyProcess struct {
DefaultProcess
BasicAuth map[string]string `json:"basic_auth" placeholder:"username1 password1\nusername2 password2" zh_name:"basic认证"`
}
func (hpp *HttpProxyProcess) GetName() string {
return "http_proxy"
}
func (hpp *HttpProxyProcess) GetZhName() string {
return "http代理"
}
func (hpp *HttpProxyProcess) ProcessConn(c enet.Conn) (bool, error) {
r, err := http.ReadRequest(bufio.NewReader(c))
if err != nil {
return false, errors.Wrap(err, "read proxy request")
}
if len(hpp.BasicAuth) != 0 && !hpp.checkAuth(r) {
return true, hpp.response(http.StatusProxyAuthRequired, map[string]string{"Proxy-Authenticate": "Basic realm=" + strconv.Quote("nps")}, c)
}
if r.Method == "CONNECT" {
err = hpp.response(200, map[string]string{}, c)
if err != nil {
return true, errors.Wrap(err, "http proxy response")
}
} else if err = c.Reset(0); err != nil {
logger.Warn("reset enet.Conn error", zap.Error(err))
return true, err
}
address := r.Host
if !strings.Contains(r.Host, ":") {
if r.URL.Scheme == "https" {
address = r.Host + ":443"
} else {
address = r.Host + ":80"
}
}
return true, hpp.ac.RunConnWithAddr(c, address)
}
func (hpp *HttpProxyProcess) response(statusCode int, headers map[string]string, c enet.Conn) error {
resp := &http.Response{
Status: http.StatusText(statusCode),
StatusCode: statusCode,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: http.Header{},
}
for k, v := range headers {
resp.Header.Set(k, v)
}
return resp.Write(c)
}
func (hpp *HttpProxyProcess) checkAuth(r *http.Request) bool {
s := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
if len(s) != 2 {
s = strings.SplitN(r.Header.Get("Proxy-Authorization"), " ", 2)
if len(s) != 2 {
return false
}
}
b, err := base64.StdEncoding.DecodeString(s[1])
if err != nil {
return false
}
pair := strings.SplitN(string(b), ":", 2)
if len(pair) != 2 {
return false
}
for u, p := range hpp.BasicAuth {
if pair[0] == u && pair[1] == p {
return true
}
}
return false
}

View File

@@ -0,0 +1,93 @@
package process
import (
"crypto/tls"
"ehang.io/nps/core/action"
"ehang.io/nps/lib/enet"
"fmt"
"github.com/stretchr/testify/assert"
"net"
"net/http"
"net/url"
"testing"
)
func TestHttpProxyProcess(t *testing.T) {
sAddr, err := startHttps(t)
assert.NoError(t, err)
hsAddr, err := startHttp(t)
assert.NoError(t, err)
h := HttpProxyProcess{
DefaultProcess: DefaultProcess{},
}
ac := &action.LocalAction{}
ac.Init()
assert.NoError(t, h.Init(ac))
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
go h.ProcessConn(enet.NewReaderConn(c))
}
}()
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
Proxy: func(_ *http.Request) (*url.URL, error) {
return url.Parse(fmt.Sprintf("http://%s", ln.Addr().String()))
},
}
client := &http.Client{Transport: transport}
resp, err := client.Get(fmt.Sprintf("https://%s/now", sAddr))
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
resp, err = client.Get(fmt.Sprintf("http://%s/now", hsAddr))
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
}
func TestHttpProxyProcessBasic(t *testing.T) {
sAddr, err := startHttps(t)
h := HttpProxyProcess{
DefaultProcess: DefaultProcess{},
BasicAuth: map[string]string{"aaa": "bbb"},
}
ac := &action.LocalAction{}
ac.Init()
assert.NoError(t, h.Init(ac))
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
go h.ProcessConn(enet.NewReaderConn(c))
}
}()
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
Proxy: func(_ *http.Request) (*url.URL, error) {
return url.Parse(fmt.Sprintf("http://%s", ln.Addr().String()))
},
}
client := &http.Client{Transport: transport}
resp, err := client.Get(fmt.Sprintf("https://%s/now", sAddr))
assert.Error(t, err)
transport.Proxy = func(_ *http.Request) (*url.URL, error) {
return url.Parse(fmt.Sprintf("http://%s:%s@%s", "aaa", "bbb", ln.Addr().String()))
}
resp, err = client.Get(fmt.Sprintf("https://%s/now", sAddr))
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
}

View File

@@ -0,0 +1,79 @@
package process
import (
"bufio"
"ehang.io/nps/core/action"
"ehang.io/nps/lib/common"
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/logger"
"github.com/pkg/errors"
"go.uber.org/zap"
"net"
"net/http"
"strings"
"time"
)
// HttpServeProcess is proxy and modify http request
type HttpServeProcess struct {
DefaultProcess
tls bool
Host string `json:"host" required:"true" placeholder:"eg: www.nps.com or *.nps.com" zh_name:"域名"`
RouteUrl string `json:"route_url" placeholder:"/api" zh_name:"匹配路径"`
HeaderModify map[string]string `json:"header_modify" placeholder:"字段 修改值\nHost www.nps-change.com\nAccept */*" zh_name:"请求头修改"`
HostModify string `json:"host_modify" placeholder:"www.nps-changed.com" zh_name:"请求域名"`
AddOrigin bool `json:"add_origin" zh_name:"添加来源"`
CacheTime int64 `json:"cache_time" placeholder:"600s" zh_name:"缓存时间"`
CachePath []string `json:"cache_path" placeholder:".jd\n.css\n.png" zh_name:"缓存路径"`
BasicAuth map[string]string `json:"basic_auth" placeholder:"username1 password1\nusername2 password2" zh_name:"basic认证"`
httpServe *HttpServe
ln *enet.Listener
}
func (hp *HttpServeProcess) GetName() string {
return "http_serve"
}
func (hp *HttpServeProcess) GetZhName() string {
return "http服务"
}
// Init the action of process
func (hp *HttpServeProcess) Init(ac action.Action) error {
hp.ac = ac
hp.ln = enet.NewListener()
hp.httpServe = NewHttpServe(hp.ln, ac)
hp.httpServe.SetModify(hp.HeaderModify, hp.HostModify, hp.AddOrigin)
if hp.CacheTime > 0 {
hp.httpServe.SetCache(hp.CachePath, time.Duration(hp.CacheTime)*time.Second)
}
if len(hp.BasicAuth) != 0 {
hp.httpServe.SetBasicAuth(hp.BasicAuth)
}
if !hp.tls {
go hp.httpServe.Serve()
}
return nil
}
// ProcessConn is used to determine whether to hit the rule
// If true, send enet to httpServe
func (hp *HttpServeProcess) ProcessConn(c enet.Conn) (bool, error) {
req, err := http.ReadRequest(bufio.NewReader(c))
if err != nil {
return false, errors.Wrap(err, "read request")
}
host, _, err := net.SplitHostPort(req.Host)
if err != nil {
return false, errors.Wrap(err, "split host")
}
if !(common.HostContains(hp.Host, host) && (hp.RouteUrl == "" || strings.HasPrefix(req.URL.Path, hp.RouteUrl))) {
logger.Debug("do http proxy failed", zap.String("host", host), zap.String("url", hp.RouteUrl))
return false, nil
}
logger.Debug("do http proxy", zap.String("host", host), zap.String("url", hp.RouteUrl))
if err := c.Reset(0); err != nil {
return true, errors.Wrap(err, "reset connection data")
}
return true, hp.ln.SendConn(c)
}

View File

@@ -0,0 +1,74 @@
package process
import (
"ehang.io/nps/core/action"
"ehang.io/nps/lib/enet"
"fmt"
"github.com/stretchr/testify/assert"
"net"
"testing"
)
func TestHttpServeProcess(t *testing.T) {
sAddr, err := startHttp(t)
assert.NoError(t, err)
h := &HttpServeProcess{
DefaultProcess: DefaultProcess{},
Host: "127.0.0.1",
RouteUrl: "",
HeaderModify: map[string]string{"modify": "nps"},
HostModify: "ehang.io",
AddOrigin: true,
}
ac := &action.LocalAction{
DefaultAction: action.DefaultAction{},
TargetAddr: []string{sAddr},
}
ac.Init()
err = h.Init(ac)
assert.NoError(t, err)
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
go h.ProcessConn(enet.NewReaderConn(c))
}
}()
rep, err := doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/header/modify"))
assert.NoError(t, err)
assert.Equal(t, "nps", rep)
rep, err = doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/host"))
assert.NoError(t, err)
assert.Equal(t, "ehang.io", rep)
rep, err = doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/origin/xff"))
assert.NoError(t, err)
assert.Equal(t, "127.0.0.1", rep)
rep, err = doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/origin/xri"))
assert.NoError(t, err)
assert.Equal(t, "127.0.0.1", rep)
h.BasicAuth = map[string]string{"aaa": "bbb"}
h.Init(ac)
rep, err = doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/now"))
assert.Error(t, err)
_, err = doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/now"), "aaa", "bbb")
assert.NoError(t, err)
h.BasicAuth = map[string]string{}
h.CacheTime = 100
h.CachePath = []string{"/now"}
h.Init(ac)
var time1, time2 string
time1, err = doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/now"))
assert.NoError(t, err)
time2, err = doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/now"))
assert.NoError(t, err)
assert.NotEmpty(t, time1)
assert.Equal(t, time1, time2)
}

View File

@@ -0,0 +1,36 @@
package process
import (
"crypto/tls"
"ehang.io/nps/core/action"
"ehang.io/nps/lib/enet"
)
type HttpsProxyProcess struct {
CertFile string `json:"cert_file" required:"true" placeholder:"/var/cert/cert.pem" zh_name:"cert文件路径"`
KeyFile string `json:"key_file" required:"true" placeholder:"/var/cert/key.pem" zh_name:"key文件路径"`
config *tls.Config
HttpProxyProcess
}
func (hpp *HttpsProxyProcess) GetName() string {
return "https_proxy"
}
func (hpp *HttpsProxyProcess) GetZhName() string {
return "https代理"
}
func (hpp *HttpsProxyProcess) Init(ac action.Action) error {
cer, err := tls.LoadX509KeyPair(hpp.CertFile, hpp.KeyFile)
if err != nil {
return err
}
hpp.config = &tls.Config{Certificates: []tls.Certificate{cer}}
hpp.ac = ac
return nil
}
func (hpp *HttpsProxyProcess) ProcessConn(c enet.Conn) (bool, error) {
return hpp.HttpProxyProcess.ProcessConn(enet.NewReaderConn(tls.Server(c, hpp.config)))
}

View File

@@ -0,0 +1,120 @@
package process
import (
"crypto/tls"
"crypto/x509/pkix"
"ehang.io/nps/core/action"
"ehang.io/nps/lib/cert"
"ehang.io/nps/lib/enet"
"fmt"
"github.com/stretchr/testify/assert"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"sync"
"testing"
)
var createCertOnce sync.Once
func createCertFile(t *testing.T) (string, string) {
createCertOnce.Do(func() {
g := cert.NewX509Generator(pkix.Name{
Country: []string{"CN"},
Organization: []string{"Ehang.io"},
OrganizationalUnit: []string{"nps"},
Province: []string{"Beijing"},
CommonName: "nps",
Locality: []string{"Beijing"},
})
cert, key, err := g.CreateRootCa()
assert.NoError(t, err)
assert.NoError(t, os.WriteFile(filepath.Join(os.TempDir(), "cert.pem"), cert, 0600))
assert.NoError(t, os.WriteFile(filepath.Join(os.TempDir(), "key.pem"), key, 0600))
})
return filepath.Join(os.TempDir(), "cert.pem"), filepath.Join(os.TempDir(), "key.pem")
}
func TestHttpsProxyProcess(t *testing.T) {
sAddr, err := startHttps(t)
certFilePath, keyFilePath := createCertFile(t)
h := HttpsProxyProcess{
HttpProxyProcess: HttpProxyProcess{},
CertFile: certFilePath,
KeyFile: keyFilePath,
}
ac := &action.LocalAction{}
ac.Init()
assert.NoError(t, h.Init(ac))
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
go h.ProcessConn(enet.NewReaderConn(c))
}
}()
transport := &http.Transport{
Proxy: func(_ *http.Request) (*url.URL, error) {
return url.Parse(fmt.Sprintf("https://%s", ln.Addr().String()))
},
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{Transport: transport}
resp, err := client.Get(fmt.Sprintf("https://%s/now", sAddr))
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
}
func TestHttpsProxyProcessBasic(t *testing.T) {
certFilePath, keyFilePath := createCertFile(t)
sAddr, err := startHttps(t)
h := HttpsProxyProcess{
HttpProxyProcess: HttpProxyProcess{
BasicAuth: map[string]string{"aaa": "bbb"},
},
CertFile: certFilePath,
KeyFile: keyFilePath,
}
ac := &action.LocalAction{}
ac.Init()
assert.NoError(t, h.Init(ac))
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
go h.ProcessConn(enet.NewReaderConn(c))
}
}()
transport := &http.Transport{
Proxy: func(_ *http.Request) (*url.URL, error) {
return url.Parse(fmt.Sprintf("https://%s", ln.Addr().String()))
},
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := &http.Client{Transport: transport}
resp, err := client.Get(fmt.Sprintf("https://%s/now", sAddr))
assert.Error(t, err)
transport.Proxy = func(_ *http.Request) (*url.URL, error) {
return url.Parse(fmt.Sprintf("https://%s:%s@%s", "aaa", "bbb", ln.Addr().String()))
}
resp, err = client.Get(fmt.Sprintf("https://%s/now", sAddr))
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
}

View File

@@ -0,0 +1,41 @@
package process
import (
"ehang.io/nps/lib/cert"
"ehang.io/nps/lib/common"
"ehang.io/nps/lib/enet"
"github.com/pkg/errors"
)
// HttpsRedirectProcess is used to forward https request by ClientHelloMsg
type HttpsRedirectProcess struct {
DefaultProcess
Host string `json:"host" required:"true" placeholder:"https.nps.com" zh_name:"域名"`
}
func (hrp *HttpsRedirectProcess) GetName() string {
return "https_redirect"
}
func (hrp *HttpsRedirectProcess) GetZhName() string {
return "https透传"
}
// ProcessConn is used to determine whether to hit the host rule
func (hrp *HttpsRedirectProcess) ProcessConn(c enet.Conn) (bool, error) {
clientMsg := cert.ClientHelloMsg{}
buf, err := c.AllBytes()
if err != nil {
return false, errors.Wrap(err, "get bytes")
}
if !clientMsg.Unmarshal(buf[5:]) {
return false, errors.New("can not unmarshal client hello message")
}
if common.HostContains(hrp.Host, clientMsg.GetServerName()) {
if err = c.Reset(0); err != nil {
return false, errors.Wrap(err, "reset reader connection")
}
return true, errors.Wrap(hrp.ac.RunConn(c), "run action")
}
return false, nil
}

View File

@@ -0,0 +1,43 @@
package process
import (
"ehang.io/nps/core/action"
"ehang.io/nps/lib/enet"
"fmt"
"github.com/stretchr/testify/assert"
"net"
"testing"
)
func TestHttpsRedirectProcess(t *testing.T) {
sAddr, err := startHttps(t)
assert.NoError(t, err)
h := &HttpsRedirectProcess{
Host: "ehang.io",
}
ac := &action.LocalAction{
DefaultAction: action.DefaultAction{},
TargetAddr: []string{sAddr},
}
ac.Init()
err = h.Init(ac)
assert.NoError(t, err)
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
go func() {
_, _ = h.ProcessConn(enet.NewReaderConn(c))
_ = c.Close()
}()
}
}()
_, err = doRequest(fmt.Sprintf("https://%s%s", ln.Addr().String(), "/now"))
assert.Error(t, err)
h.Host = "*.github.com"
_, err = doRequest(fmt.Sprintf("https://%s%s", ln.Addr().String(), "/now"))
assert.NoError(t, err)
}

View File

@@ -0,0 +1,51 @@
package process
import (
"ehang.io/nps/core/action"
"ehang.io/nps/lib/cert"
"ehang.io/nps/lib/common"
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/logger"
"github.com/pkg/errors"
"go.uber.org/zap"
)
type HttpsServeProcess struct {
CertFile string `json:"cert_file" required:"true" placeholder:"/var/cert/cert.pem" zh_name:"cert文件路径"`
KeyFile string `json:"key_file" required:"true" placeholder:"/var/cert/key.pem" zh_name:"key文件路径"`
HttpServeProcess
}
func (hsp *HttpsServeProcess) GetName() string {
return "https_serve"
}
func (hsp *HttpsServeProcess) GetZhName() string {
return "https服务"
}
func (hsp *HttpsServeProcess) Init(ac action.Action) error {
hsp.tls = true
err := hsp.HttpServeProcess.Init(ac)
go hsp.httpServe.ServeTLS(hsp.CertFile, hsp.KeyFile)
return err
}
func (hsp *HttpsServeProcess) ProcessConn(c enet.Conn) (bool, error) {
clientMsg := cert.ClientHelloMsg{}
b, err := c.AllBytes()
if err != nil {
return false, errors.Wrap(err, "get bytes")
}
if !clientMsg.Unmarshal(b[5:]) {
return false, errors.New("can not unmarshal client hello message")
}
if common.HostContains(hsp.Host, clientMsg.GetServerName()) {
logger.Debug("do https serve failed", zap.String("host", clientMsg.GetServerName()), zap.String("url", hsp.RouteUrl))
if err := c.Reset(0); err != nil {
return true, errors.Wrap(err, "reset reader connection")
}
return true, hsp.HttpServeProcess.ln.SendConn(c)
}
return false, nil
}

View File

@@ -0,0 +1,78 @@
package process
import (
"ehang.io/nps/core/action"
"ehang.io/nps/lib/enet"
"fmt"
"github.com/stretchr/testify/assert"
"net"
"testing"
)
func TestHttpsProcess(t *testing.T) {
certFile, keyFile := createCertFile(t)
sAddr, err := startHttp(t)
assert.NoError(t, err)
h := &HttpsServeProcess{
HttpServeProcess: HttpServeProcess{
Host: "www.github.com",
RouteUrl: "",
HeaderModify: map[string]string{"modify": "nps"},
HostModify: "ehang.io",
AddOrigin: true,
},
CertFile: certFile,
KeyFile: keyFile,
}
ac := &action.LocalAction{
DefaultAction: action.DefaultAction{},
TargetAddr: []string{sAddr},
}
ac.Init()
err = h.Init(ac)
assert.NoError(t, err)
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
go h.ProcessConn(enet.NewReaderConn(c))
}
}()
rep, err := doRequest(fmt.Sprintf("https://%s%s", ln.Addr().String(), "/header/modify"))
assert.NoError(t, err)
assert.Equal(t, "nps", rep)
rep, err = doRequest(fmt.Sprintf("https://%s%s", ln.Addr().String(), "/host"))
assert.NoError(t, err)
assert.Equal(t, "ehang.io", rep)
rep, err = doRequest(fmt.Sprintf("https://%s%s", ln.Addr().String(), "/origin/xff"))
assert.NoError(t, err)
assert.Equal(t, "127.0.0.1", rep)
rep, err = doRequest(fmt.Sprintf("https://%s%s", ln.Addr().String(), "/origin/xri"))
assert.NoError(t, err)
assert.Equal(t, "127.0.0.1", rep)
h.BasicAuth = map[string]string{"aaa": "bbb"}
assert.NoError(t, h.Init(ac))
rep, err = doRequest(fmt.Sprintf("https://%s%s", ln.Addr().String(), "/now"))
assert.Error(t, err)
_, err = doRequest(fmt.Sprintf("https://%s%s", ln.Addr().String(), "/now"), "aaa", "bbb")
assert.NoError(t, err)
h.BasicAuth = map[string]string{}
h.CacheTime = 100
h.CachePath = []string{"/now"}
assert.NoError(t, h.Init(ac))
var time1, time2 string
time1, err = doRequest(fmt.Sprintf("https://%s%s", ln.Addr().String(), "/now"))
assert.NoError(t, err)
time2, err = doRequest(fmt.Sprintf("https://%s%s", ln.Addr().String(), "/now"))
assert.NoError(t, err)
assert.NotEmpty(t, time1)
assert.Equal(t, time1, time2)
}

45
core/process/pb_app.go Normal file
View File

@@ -0,0 +1,45 @@
package process
import (
"ehang.io/nps/core/action"
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/pb"
"github.com/pkg/errors"
)
type PbAppProcessor struct {
DefaultProcess
}
func (pp *PbAppProcessor) GetName() string {
return "pb_app"
}
func (pp *PbAppProcessor) ProcessConn(c enet.Conn) (bool, error) {
m := &pb.ClientRequest{}
n, err := pb.ReadMessage(c, m)
if err != nil {
return false, nil
}
if _, ok := m.ConnType.(*pb.ClientRequest_AppInfo); !ok {
return false, nil
}
if err := c.Reset(n + 4); err != nil {
return true, errors.Wrap(err, "reset connection data")
}
switch m.GetAppInfo().GetConnType() {
case pb.ConnType_udp:
return true, pp.RunUdp(c)
case pb.ConnType_tcp:
return true, pp.ac.RunConnWithAddr(c, m.GetAppInfo().GetAppAddr())
case pb.ConnType_unix:
ac := &action.LocalAction{TargetAddr: []string{m.GetAppInfo().GetAppAddr()}, UnixSocket: true}
_ = ac.Init()
return true, ac.RunConn(c)
}
return true, errors.Errorf("can not support the conn type(%d)", m.GetAppInfo().GetConnType())
}
func (pp *PbAppProcessor) RunUdp(c enet.Conn) error {
return pp.ac.RunPacketConn(enet.NewTcpPacketConn(c))
}

113
core/process/pb_app_test.go Normal file
View File

@@ -0,0 +1,113 @@
package process
import (
"context"
"crypto/tls"
"ehang.io/nps/core/action"
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/pb"
"fmt"
"github.com/stretchr/testify/assert"
"net"
"net/http"
"testing"
"time"
)
func TestProtobufProcess(t *testing.T) {
sAddr, err := startHttps(t)
assert.NoError(t, err)
h := &PbAppProcessor{}
ac := &action.LocalAction{
DefaultAction: action.DefaultAction{},
TargetAddr: []string{sAddr},
}
ac.Init()
err = h.Init(ac)
assert.NoError(t, err)
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
go func() {
_, _ = h.ProcessConn(enet.NewReaderConn(c))
_ = c.Close()
}()
}
}()
client := http.Client{Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
MaxIdleConns: 10000,
IdleConnTimeout: 30 * time.Second,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
conn, err := net.Dial("tcp", ln.Addr().String())
_, err = pb.WriteMessage(conn, &pb.AppInfo{AppAddr: sAddr})
return conn, err
},
}}
resp, err := client.Get(fmt.Sprintf("https://%s%s", ln.Addr().String(), "/now"))
assert.NoError(t, err)
assert.NotEmpty(t, resp)
}
func TestProtobufUdpProcess(t *testing.T) {
finish := make(chan struct{}, 0)
lAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
assert.NoError(t, err)
udpServer, err := net.ListenUDP("udp", lAddr)
assert.NoError(t, err)
h := &PbAppProcessor{}
ac := &action.LocalAction{
DefaultAction: action.DefaultAction{},
TargetAddr: []string{udpServer.LocalAddr().String()},
}
ac.Init()
err = h.Init(ac)
assert.NoError(t, err)
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
go func() {
_, _ = h.ProcessConn(enet.NewReaderConn(c))
_ = c.Close()
}()
}
}()
data := []byte{1, 2, 3, 4}
dataReturn := []byte{4, 5, 6, 7}
conn, err := net.Dial("tcp", ln.Addr().String())
_, err = pb.WriteMessage(conn, &pb.AppInfo{AppAddr: udpServer.LocalAddr().String(), ConnType: pb.ConnType_udp})
go func() {
b := make([]byte, 1024)
n, addr, err := udpServer.ReadFrom(b)
assert.NoError(t, err)
assert.Equal(t, b[:n], data)
_, err = udpServer.WriteTo(dataReturn, addr)
assert.NoError(t, err)
finish <- struct{}{}
}()
c := enet.NewTcpPacketConn(conn)
_, err = c.WriteTo(data, udpServer.LocalAddr())
assert.NoError(t, err)
<-finish
b := make([]byte, 1024)
n, addr, err := c.ReadFrom(b)
assert.NoError(t, err)
assert.Equal(t, dataReturn, b[:n])
assert.Equal(t, addr.String(), udpServer.LocalAddr().String())
}

29
core/process/pb_ping.go Normal file
View File

@@ -0,0 +1,29 @@
package process
import (
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/pb"
"time"
)
type PbPingProcessor struct {
DefaultProcess
}
func (pp *PbPingProcessor) GetName() string {
return "pb_ping"
}
func (pp *PbPingProcessor) ProcessConn(c enet.Conn) (bool, error) {
m := &pb.ClientRequest{}
_, err := pb.ReadMessage(c, m)
if err != nil {
return false, nil
}
if _, ok := m.ConnType.(*pb.ClientRequest_Ping); !ok {
return false, nil
}
m.GetPing().Now = time.Now().String()
_, err = pb.WriteMessage(c, m)
return true, err
}

View File

@@ -0,0 +1,43 @@
package process
import (
"ehang.io/nps/core/action"
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/pb"
"github.com/stretchr/testify/assert"
"net"
"testing"
"time"
)
func TestPbPingProcess(t *testing.T) {
h := &PbPingProcessor{}
ac := &action.LocalAction{
DefaultAction: action.DefaultAction{},
TargetAddr: []string{},
}
ac.Init()
err = h.Init(ac)
assert.NoError(t, err)
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
go func() {
_, _ = h.ProcessConn(enet.NewReaderConn(c))
_ = c.Close()
}()
}
}()
conn, err := net.Dial("tcp", ln.Addr().String())
assert.NoError(t, err)
_, err = pb.WriteMessage(conn, &pb.Ping{Now: time.Now().String()})
assert.NoError(t, err)
m := &pb.Ping{}
_, err = pb.ReadMessage(conn, m)
assert.NoError(t, err)
assert.NotEmpty(t, m.Now)
}

51
core/process/process.go Normal file
View File

@@ -0,0 +1,51 @@
package process
import (
"ehang.io/nps/core/action"
"ehang.io/nps/lib/enet"
)
var (
_ Process = (*DefaultProcess)(nil)
_ Process = (*HttpServeProcess)(nil)
_ Process = (*HttpsServeProcess)(nil)
_ Process = (*HttpProxyProcess)(nil)
_ Process = (*HttpsProxyProcess)(nil)
_ Process = (*HttpsRedirectProcess)(nil)
_ Process = (*Socks5Process)(nil)
_ Process = (*TransparentProcess)(nil)
)
type Process interface {
Init(action action.Action) error
GetName() string
GetZhName() string
ProcessConn(enet.Conn) (bool, error)
ProcessPacketConn(enet.PacketConn) (bool, error)
}
type DefaultProcess struct {
ac action.Action
}
func (bp *DefaultProcess) ProcessConn(c enet.Conn) (bool, error) {
return true, bp.ac.RunConn(c)
}
func (bp *DefaultProcess) GetName() string {
return "default"
}
func (bp *DefaultProcess) GetZhName() string {
return "默认"
}
// Init the action of process
func (bp *DefaultProcess) Init(ac action.Action) error {
bp.ac = ac
return nil
}
func (bp *DefaultProcess) ProcessPacketConn(pc enet.PacketConn) (bool, error) {
return true, bp.ac.RunPacketConn(pc)
}

169
core/process/serve.go Normal file
View File

@@ -0,0 +1,169 @@
package process
import (
"context"
"crypto/tls"
"ehang.io/nps/core/action"
"ehang.io/nps/lib/logger"
"github.com/gin-contrib/cache"
"github.com/gin-contrib/cache/persistence"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"io"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"time"
)
type HttpServe struct {
engine *gin.Engine
ln net.Listener
ac action.Action
httpServe *http.Server
cacheStore *persistence.InMemoryStore
cacheTime time.Duration
cachePath []string
headerModify map[string]string
hostModify string
addOrigin bool
reverseProxy *httputil.ReverseProxy
}
func NewHttpServe(ln net.Listener, ac action.Action) *HttpServe {
gin.SetMode(gin.ReleaseMode)
hs := &HttpServe{
ln: ln,
ac: ac,
engine: gin.New(),
}
hs.httpServe = &http.Server{
Handler: hs.engine,
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
}
hs.reverseProxy = &httputil.ReverseProxy{
Director: func(request *http.Request) {
_ = hs.transport(request)
hs.doModify(request)
},
Transport: &http.Transport{
MaxIdleConns: 10000,
IdleConnTimeout: 30 * time.Second,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return ac.GetServerConn()
},
},
}
serverHttp := func(w http.ResponseWriter, r *http.Request) {
hs.reverseProxy.ServeHTTP(w, r)
}
hs.engine.NoRoute(func(c *gin.Context) {
cached := false
for _, p := range hs.cachePath {
if strings.Contains(c.Request.RequestURI, p) {
cached = true
cache.CachePage(hs.cacheStore, hs.cacheTime, func(c *gin.Context) {
serverHttp(c.Writer, c.Request)
})(c)
}
}
if !cached {
serverHttp(c.Writer, c.Request)
}
})
hs.engine.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
logger.Debug("http request",
zap.String("client_ip", param.ClientIP),
zap.String("method", param.Method),
zap.String("path", param.Path),
zap.String("proto", param.Request.Proto),
zap.Duration("latency", param.Latency),
zap.String("user_agent", param.Request.UserAgent()),
zap.String("error_message", param.ErrorMessage),
zap.Int("response_code", param.StatusCode),
)
return ""
}))
return hs
}
func (hs *HttpServe) transport(req *http.Request) error {
ruri := req.URL.RequestURI()
req.URL.Scheme = "http"
if req.URL.Scheme != "" && req.URL.Opaque == "" {
ruri = req.URL.Scheme + "://" + req.Host + ruri
} else if req.Method == "CONNECT" && req.URL.Path == "" {
// CONNECT requests normally give just the host and port, not a full URL.
ruri = req.Host
if req.URL.Opaque != "" {
ruri = req.URL.Opaque
}
}
req.RequestURI = ""
var err error
req.URL, err = url.Parse(ruri)
return err
}
func (hs *HttpServe) SetBasicAuth(accounts map[string]string) {
hs.engine.Use(gin.BasicAuth(accounts), gin.Recovery())
}
func (hs *HttpServe) SetCache(cachePath []string, cacheTime time.Duration) {
hs.cachePath = cachePath
hs.cacheTime = cacheTime
hs.cacheStore = persistence.NewInMemoryStore(cacheTime * time.Second)
}
func (hs *HttpServe) SetModify(headerModify map[string]string, hostModify string, addOrigin bool) {
hs.headerModify = headerModify
hs.hostModify = hostModify
hs.addOrigin = addOrigin
return
}
func (hs *HttpServe) Serve() error {
return hs.httpServe.Serve(hs.ln)
}
func (hs *HttpServe) ServeTLS(certFile string, keyFile string) error {
return hs.httpServe.ServeTLS(hs.ln, certFile, keyFile)
}
// doModify is used to modify http request
func (hs *HttpServe) doModify(req *http.Request) {
if hs.hostModify != "" {
req.Host = hs.hostModify
}
for k, v := range hs.headerModify {
req.Header.Set(k, v)
}
addr := strings.Split(req.RemoteAddr, ":")[0]
if hs.addOrigin {
// XFF is setting in reverseProxy
req.Header.Set("X-Real-IP", addr)
}
}
func writeContentType(w http.ResponseWriter, value []string) {
header := w.Header()
if val := header["Content-Type"]; len(val) == 0 {
header["Content-Type"] = value
}
}
type render struct {
resp *http.Response
}
func (r *render) Render(writer http.ResponseWriter) error {
_, err := io.Copy(writer, r.resp.Body)
return err
}
func (r *render) WriteContentType(w http.ResponseWriter) {
writeContentType(w, []string{r.resp.Header.Get("Content-Type")})
}

299
core/process/serve_test.go Normal file
View File

@@ -0,0 +1,299 @@
package process
import (
"context"
"crypto/tls"
"ehang.io/nps/core/action"
"fmt"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"golang.org/x/net/websocket"
"io/ioutil"
"net"
"net/http"
"sync"
"testing"
"time"
)
var startHttpOnce sync.Once
var startHttpsOnce sync.Once
var handleOnce sync.Once
var ln net.Listener
var lns net.Listener
var err error
func registerHandle() {
handleOnce.Do(func() {
http.Handle("/ws", websocket.Handler(func(ws *websocket.Conn) {
msg := make([]byte, 512)
n, err := ws.Read(msg)
if err != nil {
return
}
ws.Write(msg[:n])
}))
http.HandleFunc("/now", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(time.Now().String()))
})
http.HandleFunc("/host", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(r.Host))
})
http.HandleFunc("/header/modify", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(r.Header.Get("modify")))
})
http.HandleFunc("/origin/xff", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(r.Header.Get("X-Forwarded-For")))
})
http.HandleFunc("/origin/xri", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(r.Header.Get("X-Real-IP")))
})
})
}
func startHttp(t *testing.T) (string, error) {
startHttpOnce.Do(func() {
ln, err = net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return
}
registerHandle()
go http.Serve(ln, nil)
})
return ln.Addr().String(), err
}
func startHttps(t *testing.T) (string, error) {
startHttpsOnce.Do(func() {
lns, err = net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return
}
registerHandle()
certFilePath, keyFilePath := createCertFile(t)
go http.ServeTLS(lns, nil, certFilePath, keyFilePath)
})
return lns.Addr().String(), err
}
func doRequest(params ...string) (string, error) {
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
req, err := http.NewRequest("GET", params[0], nil)
if err != nil {
return "", err
}
req.Header.Set("Connection", "close")
if len(params) >= 3 && params[1] != "" {
req.SetBasicAuth(params[1], params[2])
}
if req.URL.Scheme == "https" {
client.Transport = &http.Transport{
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return tls.Dial(network, addr, &tls.Config{
InsecureSkipVerify: true,
ServerName: "www.github.com",
})
},
}
}
resp, err := client.Do(req)
if err != nil {
return "", err
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode != 200 {
return "0", errors.Errorf("respond error, code %d", resp.StatusCode)
}
return string(b), nil
}
func createHttpServe(serverAddr string) (*HttpServe, error) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, err
}
ac := &action.LocalAction{
DefaultAction: action.DefaultAction{},
TargetAddr: []string{serverAddr},
}
ac.Init()
return NewHttpServe(ln, ac), nil
}
func TestHttpServeWebsocket(t *testing.T) {
serverAddr, err := startHttp(t)
assert.NoError(t, err)
hs, err := createHttpServe(serverAddr)
assert.NoError(t, err)
go hs.Serve()
ws, err := websocket.Dial(fmt.Sprintf("ws://%s/ws", hs.ln.Addr().String()), "", fmt.Sprintf("http://%s/ws", hs.ln.Addr().String()))
assert.NoError(t, err)
defer ws.Close() //关闭连接
sendMsg := []byte("nps")
_, err = ws.Write(sendMsg)
assert.NoError(t, err)
msg := make([]byte, 512)
m, err := ws.Read(msg)
assert.NoError(t, err)
assert.Equal(t, sendMsg, msg[:m])
}
func TestHttpsServeWebsocket(t *testing.T) {
serverAddr, err := startHttp(t)
assert.NoError(t, err)
hs, err := createHttpServe(serverAddr)
assert.NoError(t, err)
cert, key := createCertFile(t)
go hs.ServeTLS(cert, key)
config, err := websocket.NewConfig(fmt.Sprintf("wss://%s/ws", hs.ln.Addr().String()), fmt.Sprintf("https://%s/ws", hs.ln.Addr().String()))
assert.NoError(t, err)
config.TlsConfig = &tls.Config{InsecureSkipVerify: true}
ws, err := websocket.DialConfig(config)
assert.NoError(t, err)
defer ws.Close() //关闭连接
sendMsg := []byte("nps")
_, err = ws.Write(sendMsg)
assert.NoError(t, err)
msg := make([]byte, 512)
m, err := ws.Read(msg)
assert.NoError(t, err)
assert.Equal(t, sendMsg, msg[:m])
}
func TestHttpServeModify(t *testing.T) {
serverAddr, err := startHttp(t)
assert.NoError(t, err)
hs, err := createHttpServe(serverAddr)
assert.NoError(t, err)
go hs.Serve()
hs.SetModify(map[string]string{"modify": "test"}, "ehang.io", true)
rep, err := doRequest(fmt.Sprintf("http://%s%s", hs.ln.Addr().String(), "/header/modify"))
assert.NoError(t, err)
assert.Equal(t, "test", rep)
rep, err = doRequest(fmt.Sprintf("http://%s%s", hs.ln.Addr().String(), "/host"))
assert.NoError(t, err)
assert.Equal(t, "ehang.io", rep)
rep, err = doRequest(fmt.Sprintf("http://%s%s", hs.ln.Addr().String(), "/origin/xff"))
assert.NoError(t, err)
assert.Equal(t, "127.0.0.1", rep)
rep, err = doRequest(fmt.Sprintf("http://%s%s", hs.ln.Addr().String(), "/origin/xri"))
assert.NoError(t, err)
assert.Equal(t, "127.0.0.1", rep)
}
func TestHttpsServeModify(t *testing.T) {
serverAddr, err := startHttp(t)
assert.NoError(t, err)
hs, err := createHttpServe(serverAddr)
assert.NoError(t, err)
cert, key := createCertFile(t)
go hs.ServeTLS(cert, key)
hs.SetModify(map[string]string{"modify": "test"}, "ehang.io", true)
rep, err := doRequest(fmt.Sprintf("https://%s%s", hs.ln.Addr().String(), "/header/modify"))
assert.NoError(t, err)
assert.Equal(t, "test", rep)
rep, err = doRequest(fmt.Sprintf("https://%s%s", hs.ln.Addr().String(), "/host"))
assert.NoError(t, err)
assert.Equal(t, "ehang.io", rep)
rep, err = doRequest(fmt.Sprintf("https://%s%s", hs.ln.Addr().String(), "/origin/xff"))
assert.NoError(t, err)
assert.Equal(t, "127.0.0.1", rep)
rep, err = doRequest(fmt.Sprintf("https://%s%s", hs.ln.Addr().String(), "/origin/xri"))
assert.NoError(t, err)
assert.Equal(t, "127.0.0.1", rep)
}
func TestHttpServeCache(t *testing.T) {
serverAddr, err := startHttp(t)
assert.NoError(t, err)
hs, err := createHttpServe(serverAddr)
assert.NoError(t, err)
go hs.Serve()
hs.SetCache([]string{"now"}, time.Second*10)
var time1, time2 string
time1, err = doRequest(fmt.Sprintf("http://%s%s", hs.ln.Addr().String(), "/now"))
assert.NoError(t, err)
time2, err = doRequest(fmt.Sprintf("http://%s%s", hs.ln.Addr().String(), "/now"))
assert.NoError(t, err)
assert.NotEmpty(t, time1)
assert.Equal(t, time1, time2)
}
func TestHttpsServeCache(t *testing.T) {
serverAddr, err := startHttp(t)
assert.NoError(t, err)
hs, err := createHttpServe(serverAddr)
assert.NoError(t, err)
cert, key := createCertFile(t)
go hs.ServeTLS(cert, key)
hs.SetCache([]string{"now"}, time.Second*10)
var time1, time2 string
time1, err = doRequest(fmt.Sprintf("https://%s%s", hs.ln.Addr().String(), "/now"))
assert.NoError(t, err)
time2, err = doRequest(fmt.Sprintf("https://%s%s", hs.ln.Addr().String(), "/now"))
assert.NoError(t, err)
assert.NotEmpty(t, time1)
assert.Equal(t, time1, time2)
}
func TestHttpServeBasicAuth(t *testing.T) {
serverAddr, err := startHttp(t)
assert.NoError(t, err)
hs, err := createHttpServe(serverAddr)
assert.NoError(t, err)
go hs.Serve()
hs.SetBasicAuth(map[string]string{"aaa": "bbb"})
_, err = doRequest(fmt.Sprintf("http://%s%s", hs.ln.Addr().String(), "/now"), "aaa", "bbb")
assert.NoError(t, err)
}
func TestHttpsServeBasicAuth(t *testing.T) {
serverAddr, err := startHttp(t)
assert.NoError(t, err)
hs, err := createHttpServe(serverAddr)
assert.NoError(t, err)
cert, key := createCertFile(t)
go hs.ServeTLS(cert, key)
hs.SetBasicAuth(map[string]string{"aaa": "bbb"})
_, err = doRequest(fmt.Sprintf("https://%s%s", hs.ln.Addr().String(), "/now"), "aaa", "bbb")
assert.NoError(t, err)
}

219
core/process/socks5.go Normal file
View File

@@ -0,0 +1,219 @@
package process
import (
"ehang.io/nps/core/action"
"ehang.io/nps/lib/common"
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/logger"
"encoding/binary"
"github.com/pkg/errors"
"github.com/robfig/go-cache"
"go.uber.org/zap"
"io"
"net"
"strconv"
"time"
)
type Socks5Process struct {
DefaultProcess
Accounts map[string]string `json:"accounts" placeholder:"username1 password1\nusername2 password2" zh_name:"授权账号密码"`
ServerIp string `json:"server_ip" placeholder:"123.123.123.123" zh_name:"udp连接地址"`
ipStore *cache.Cache
}
const (
ipV4 = 1
domainName = 3
ipV6 = 4
connectMethod = 1
bindMethod = 2
associateMethod = 3
// The maximum packet size of any udp Associate packet, based on ethernet's max size,
// minus the IP and UDP headers5. IPv4 has a 20 byte header, UDP adds an
// additional 4 bytes5. This is a total overhead of 24 bytes5. Ethernet's
// max packet size is 1500 bytes, 1500 - 24 = 1476.
maxUDPPacketSize = 1476
)
const (
succeeded uint8 = iota
serverFailure
notAllowed
networkUnreachable
hostUnreachable
connectionRefused
ttlExpired
commandNotSupported
addrTypeNotSupported
)
const (
UserPassAuth = uint8(2)
userAuthVersion = uint8(1)
authSuccess = uint8(0)
authFailure = uint8(1)
)
func (s5 *Socks5Process) GetName() string {
return "socks5"
}
func (s5 *Socks5Process) GetZhName() string {
return "socks5代理"
}
func (s5 *Socks5Process) Init(ac action.Action) error {
s5.ipStore = cache.New(time.Minute, time.Minute*2)
return s5.DefaultProcess.Init(ac)
}
func (s5 *Socks5Process) ProcessConn(c enet.Conn) (bool, error) {
return true, s5.handleConn(c)
}
func (s5 *Socks5Process) ProcessPacketConn(pc enet.PacketConn) (bool, error) {
ip, _, _ := net.SplitHostPort(pc.LocalAddr().String())
if _, ok := s5.ipStore.Get(ip); !ok {
return false, nil
}
_, addr, err := pc.FirstPacket()
if err != nil {
return false, errors.New("addr not found")
}
return true, s5.ac.RunPacketConn(enet.NewS5PacketConn(pc, addr))
}
func (s5 *Socks5Process) handleConn(c enet.Conn) error {
buf := make([]byte, 2)
if _, err := io.ReadFull(c, buf); err != nil {
return err
}
if version := buf[0]; version != 5 {
return errors.New("only support socks5")
}
nMethods := buf[1]
methods := make([]byte, nMethods)
if l, err := c.Read(methods); l != int(nMethods) || err != nil {
return errors.New("wrong method")
}
if len(s5.Accounts) > 0 {
buf[1] = UserPassAuth
_, err := c.Write(buf)
if err != nil {
return err
}
if err := s5.Auth(c); err != nil {
return errors.Wrap(err, "auth failed")
}
} else {
buf[1] = 0
_, _ = c.Write(buf)
}
return s5.handleRequest(c)
}
func (s5 *Socks5Process) Auth(c enet.Conn) error {
header := []byte{0, 0}
if _, err := io.ReadAtLeast(c, header, 2); err != nil {
return err
}
if header[0] != userAuthVersion {
return errors.New("auth type not support")
}
userLen := int(header[1])
user := make([]byte, userLen)
if _, err := io.ReadAtLeast(c, user, userLen); err != nil {
return err
}
if _, err := c.Read(header[:1]); err != nil {
return errors.New("the length of password is incorrect")
}
passLen := int(header[0])
pass := make([]byte, passLen)
if _, err := io.ReadAtLeast(c, pass, passLen); err != nil {
return err
}
p := s5.Accounts[string(user)]
if p == "" || string(pass) != p {
_, _ = c.Write([]byte{userAuthVersion, authFailure})
return errors.New("auth failure")
}
if _, err := c.Write([]byte{userAuthVersion, authSuccess}); err != nil {
return errors.Wrap(err, "write auth success")
}
return nil
}
func (s5 *Socks5Process) handleRequest(c enet.Conn) error {
header := make([]byte, 3)
_, err := io.ReadFull(c, header)
if err != nil {
return err
}
switch header[1] {
case connectMethod:
s5.handleConnect(c)
case associateMethod:
s5.handleUDP(c)
default:
s5.sendReply(c, commandNotSupported)
c.Close()
}
return nil
}
//enet
func (s5 *Socks5Process) handleConnect(c enet.Conn) {
addr, err := common.ReadAddr(c)
if err != nil {
s5.sendReply(c, addrTypeNotSupported)
logger.Warn("read socks addr error", zap.Error(err))
return
}
s5.sendReply(c, succeeded)
_ = s5.ac.RunConnWithAddr(c, addr.String())
return
}
func (s5 *Socks5Process) handleUDP(c net.Conn) {
_, err := common.ReadAddr(c)
if err != nil {
s5.sendReply(c, addrTypeNotSupported)
logger.Warn("read socks addr error", zap.Error(err))
return
}
ip, _, _ := net.SplitHostPort(c.RemoteAddr().String())
s5.ipStore.Set(ip, true, time.Minute)
s5.sendReply(c, succeeded)
}
func (s5 *Socks5Process) sendReply(c net.Conn, rep uint8) {
reply := []byte{
5,
rep,
0,
1,
}
localHost, localPort, _ := net.SplitHostPort(c.LocalAddr().String())
if s5.ServerIp != "" {
localHost = s5.ServerIp
}
ipBytes := net.ParseIP(localHost).To4()
nPort, _ := strconv.Atoi(localPort)
reply = append(reply, ipBytes...)
portBytes := make([]byte, 2)
binary.BigEndian.PutUint16(portBytes, uint16(nPort))
reply = append(reply, portBytes...)
_, _ = c.Write(reply)
}

152
core/process/socks5_test.go Normal file
View File

@@ -0,0 +1,152 @@
package process
import (
"crypto/tls"
"ehang.io/nps/core/action"
"ehang.io/nps/lib/common"
"ehang.io/nps/lib/enet"
"fmt"
"github.com/stretchr/testify/assert"
"golang.org/x/net/proxy"
"net"
"net/http"
"net/url"
"testing"
"time"
)
func TestSocks5ProxyProcess(t *testing.T) {
sAddr, err := startHttps(t)
assert.NoError(t, err)
h := Socks5Process{
DefaultProcess: DefaultProcess{},
}
ac := &action.LocalAction{}
ac.Init()
assert.NoError(t, h.Init(ac))
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
go h.ProcessConn(enet.NewReaderConn(c))
}
}()
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
Proxy: func(_ *http.Request) (*url.URL, error) {
return url.Parse(fmt.Sprintf("socks5://%s", ln.Addr().String()))
},
}
client := &http.Client{Transport: transport}
resp, err := client.Get(fmt.Sprintf("https://%s/now", sAddr))
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
}
func TestSocks5ProxyProcessAuth(t *testing.T) {
sAddr, err := startHttps(t)
h := Socks5Process{
DefaultProcess: DefaultProcess{},
Accounts: map[string]string{"aaa": "bbb"},
}
ac := &action.LocalAction{}
ac.Init()
assert.NoError(t, h.Init(ac))
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
go func() {
for {
c, err := ln.Accept()
assert.NoError(t, err)
go func() {
_, _ = h.ProcessConn(enet.NewReaderConn(c))
_ = c.Close()
}()
}
}()
auth := proxy.Auth{
User: "aaa",
Password: "bbb",
}
dialer, err := proxy.SOCKS5("tcp", ln.Addr().String(), nil, proxy.Direct)
assert.NoError(t, err)
tr := &http.Transport{Dial: dialer.Dial, TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
client := &http.Client{
Transport: tr,
}
resp, err := client.Get(fmt.Sprintf("https://%s/now", sAddr))
assert.Error(t, err)
dialer, err = proxy.SOCKS5("tcp", ln.Addr().String(), &auth, proxy.Direct)
assert.NoError(t, err)
tr = &http.Transport{Dial: dialer.Dial, TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
client = &http.Client{
Transport: tr,
}
resp, err = client.Get(fmt.Sprintf("https://%s/now", sAddr))
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
}
func TestSocks5ProxyProcessUdp(t *testing.T) {
h := Socks5Process{
DefaultProcess: DefaultProcess{},
}
ac := &action.LocalAction{}
ac.Init()
assert.NoError(t, h.Init(ac))
h.ipStore.Set("127.0.0.1", true, time.Minute)
serverPc, err := net.ListenPacket("udp", "127.0.0.1:0")
assert.NoError(t, err)
localPc, err := net.ListenPacket("udp", "127.0.0.1:0")
assert.NoError(t, err)
appPc, err := net.ListenPacket("udp", "127.0.0.1:0")
assert.NoError(t, err)
data := []byte("test")
go func() {
p := make([]byte, 1500)
n, addr, err := appPc.ReadFrom(p)
assert.NoError(t, err)
assert.Equal(t, p[:n], data)
_, err = appPc.WriteTo(data, addr)
assert.NoError(t, err)
}()
go func() {
p := make([]byte, 1500)
n, addr, err := serverPc.ReadFrom(p)
assert.NoError(t, err)
pc := enet.NewReaderPacketConn(serverPc, p[:n], addr)
err = pc.SendPacket(p[:n], addr)
assert.NoError(t, err)
b, err := h.ProcessPacketConn(pc)
assert.Equal(t, b, true)
assert.NoError(t, err)
}()
b := []byte{0, 0, 0}
pAddr, err := common.ParseAddr(appPc.LocalAddr().String())
assert.NoError(t, err)
b = append(b, pAddr...)
b = append(b, data...)
_, err = localPc.WriteTo(b, serverPc.LocalAddr())
assert.NoError(t, err)
p := make([]byte, 1500)
n, _, err := localPc.ReadFrom(p)
assert.NoError(t, err)
respAddr, err := common.SplitAddr(p[3:])
assert.NoError(t, err)
assert.Equal(t, respAddr.String(), appPc.LocalAddr().String())
assert.Equal(t, p[3+len(respAddr):n], data)
}

View File

@@ -0,0 +1,56 @@
package process
import (
"ehang.io/nps/lib/enet"
"ehang.io/nps/lib/logger"
"go.uber.org/zap"
"net"
"strconv"
"syscall"
)
const SO_ORIGINAL_DST = 80
type TransparentProcess struct {
DefaultProcess
}
func (tp *TransparentProcess) GetName() string {
return "transparent"
}
func (tp *TransparentProcess) GetZhName() string {
return "透明代理"
}
func (tp *TransparentProcess) ProcessConn(c enet.Conn) (bool, error) {
addr, err := tp.getAddress(c)
if err != nil {
logger.Debug("get syscall error", zap.Error(err))
return false, nil
}
return true, tp.ac.RunConnWithAddr(c, addr)
}
func (tp *TransparentProcess) getAddress(conn net.Conn) (string, error) {
// TODO: IPV6 support
sysrawConn, f := conn.(syscall.Conn)
if !f {
return "", nil
}
rawConn, err := sysrawConn.SyscallConn()
if err != nil {
return "", nil
}
var ip string
var port uint16
err = rawConn.Control(func(fd uintptr) {
addr, err := syscall.GetsockoptIPv6Mreq(int(fd), syscall.IPPROTO_IP, SO_ORIGINAL_DST)
if err != nil {
return
}
ip = net.IP(addr.Multiaddr[4:8]).String()
port = uint16(addr.Multiaddr[2])<<8 + uint16(addr.Multiaddr[3])
})
return net.JoinHostPort(ip, strconv.Itoa(int(port))), nil
}

View File

@@ -0,0 +1,23 @@
// +build !linux
package process
import (
"ehang.io/nps/lib/enet"
)
type TransparentProcess struct {
DefaultProcess
}
func (tp *TransparentProcess) GetName() string {
return "transparent"
}
func (tp *TransparentProcess) GetZhName() string {
return "透明代理"
}
func (tp *TransparentProcess) ProcessConn(c enet.Conn) (bool, error) {
return false, nil
}