From c482967f8c585a7171b5c04ec0f26e5b8a798618 Mon Sep 17 00:00:00 2001 From: he liu Date: Sun, 23 Jan 2022 17:30:38 +0800 Subject: [PATCH] add new file --- component/bridge/bridge.go | 23 + component/bridge/client.go | 102 ++++ component/bridge/manager.go | 60 ++ component/bridge/quic.go | 91 +++ component/bridge/tcp.go | 116 ++++ component/client/client.go | 129 +++++ component/client/conn.go | 69 +++ component/client/npc.go | 44 ++ component/component.go | 1 + component/component_test.go | 193 +++++++ component/controller/asset | 1 + component/controller/cert.go | 92 +++ component/controller/config.go | 50 ++ component/controller/controller.go | 101 ++++ component/controller/controller_test.go | 149 +++++ component/controller/jwt.go | 105 ++++ component/controller/rule.go | 151 +++++ component/controller/status.go | 152 +++++ component/service/service.go | 11 + core/action/action.go | 110 ++++ core/action/admin.go | 32 ++ core/action/admin_test.go | 29 + core/action/bridge.go | 61 ++ core/action/bridge_test.go | 73 +++ core/action/local.go | 77 +++ core/action/local_test.go | 1 + core/action/npc.go | 84 +++ core/action/npc_test.go | 1 + core/handler/default.go | 71 +++ core/handler/dns.go | 35 ++ core/handler/dns_test.go | 46 ++ core/handler/handler.go | 11 + core/handler/http.go | 30 + core/handler/http_test.go | 27 + core/handler/https.go | 33 ++ core/handler/https_test.go | 39 ++ core/handler/p2p.go | 32 ++ core/handler/p2p_test.go | 26 + core/handler/quic.go | 32 ++ core/handler/quic_test.go | 35 ++ core/handler/rdp.go | 24 + core/handler/rdp_test.go | 37 ++ core/handler/redis.go | 22 + core/handler/redis_test.go | 40 ++ core/handler/socks5.go | 22 + core/handler/socks5_test.go | 47 ++ core/handler/socks5_udp.go | 26 + core/handler/socks5_udp_test.go | 44 ++ core/handler/transparent.go | 21 + core/limiter/conn_num.go | 43 ++ core/limiter/conn_num_test.go | 35 ++ core/limiter/flow.go | 87 +++ core/limiter/flow_test.go | 59 ++ core/limiter/ip_conn_num.go | 99 ++++ core/limiter/ip_conn_num_test.go | 35 ++ core/limiter/limiter.go | 26 + core/limiter/rate.go | 67 +++ core/limiter/rate_test.go | 46 ++ core/process/http_proxy.go | 94 +++ core/process/http_proxy_test.go | 93 +++ core/process/http_serve.go | 79 +++ core/process/http_serve_test.go | 74 +++ core/process/https_proxy.go | 36 ++ core/process/https_proxy_test.go | 120 ++++ core/process/https_redirect.go | 41 ++ core/process/https_redirect_test.go | 43 ++ core/process/https_serve.go | 51 ++ core/process/https_serve_test.go | 78 +++ core/process/pb_app.go | 45 ++ core/process/pb_app_test.go | 113 ++++ core/process/pb_ping.go | 29 + core/process/pb_ping_test.go | 43 ++ core/process/process.go | 51 ++ core/process/serve.go | 169 ++++++ core/process/serve_test.go | 299 ++++++++++ core/process/socks5.go | 219 +++++++ core/process/socks5_test.go | 152 +++++ core/process/transparent_linux.go | 56 ++ core/process/transparent_others.go | 23 + core/rule/list.go | 138 +++++ core/rule/list_test.go | 13 + core/rule/rule.go | 71 +++ core/rule/rule_json.go | 92 +++ core/rule/rule_json_test.go | 58 ++ core/rule/rule_test.go | 45 ++ core/rule/sort.go | 30 + core/rule/sort_test.go | 27 + core/server/server.go | 32 ++ core/server/tcp.go | 97 ++++ core/server/udp.go | 80 +++ db/db.go | 216 +++++++ db/db_test.go | 73 +++ go.mod | 43 ++ go.sum | 546 ++++++++++++++++++ lib/cert/cert.go | 34 ++ lib/cert/cert_test.go | 42 ++ lib/cert/client_hello.go | 253 ++++++++ lib/cert/generate.go | 108 ++++ lib/cert/generate_test.go | 62 ++ lib/common/addr.go | 141 +++++ lib/common/utils.go | 75 +++ lib/enet/conn.go | 119 ++++ lib/enet/conn_test.go | 81 +++ lib/enet/listener.go | 62 ++ lib/enet/paket.go | 161 ++++++ lib/enet/paket_test.go | 78 +++ lib/enet/s5_packet.go | 55 ++ lib/enet/s5_packet_test.go | 50 ++ lib/lb/algo.go | 96 ++++ lib/lb/lb.go | 59 ++ lib/lb/lb_test.go | 49 ++ lib/logger/logger.go | 93 +++ lib/logger/logger_others.go | 38 ++ lib/logger/logger_windows.go | 7 + lib/pb/bridge.pb.go | 736 ++++++++++++++++++++++++ lib/pb/bridge.proto | 51 ++ lib/pb/pb.go | 29 + lib/pb/pb_test.go | 25 + lib/pool/goroutine.go | 76 +++ lib/pool/pool.go | 32 ++ lib/rate/rate.go | 111 ++++ lib/rate/rate_test.go | 52 ++ transport/quic.go | 71 +++ transport/transport.go | 17 + transport/yamux.go | 51 ++ 125 files changed, 9688 insertions(+) create mode 100644 component/bridge/bridge.go create mode 100644 component/bridge/client.go create mode 100644 component/bridge/manager.go create mode 100644 component/bridge/quic.go create mode 100644 component/bridge/tcp.go create mode 100644 component/client/client.go create mode 100644 component/client/conn.go create mode 100644 component/client/npc.go create mode 100644 component/component.go create mode 100644 component/component_test.go create mode 160000 component/controller/asset create mode 100644 component/controller/cert.go create mode 100644 component/controller/config.go create mode 100644 component/controller/controller.go create mode 100644 component/controller/controller_test.go create mode 100644 component/controller/jwt.go create mode 100644 component/controller/rule.go create mode 100644 component/controller/status.go create mode 100644 component/service/service.go create mode 100644 core/action/action.go create mode 100644 core/action/admin.go create mode 100644 core/action/admin_test.go create mode 100644 core/action/bridge.go create mode 100644 core/action/bridge_test.go create mode 100644 core/action/local.go create mode 100644 core/action/local_test.go create mode 100644 core/action/npc.go create mode 100644 core/action/npc_test.go create mode 100644 core/handler/default.go create mode 100644 core/handler/dns.go create mode 100644 core/handler/dns_test.go create mode 100644 core/handler/handler.go create mode 100644 core/handler/http.go create mode 100644 core/handler/http_test.go create mode 100644 core/handler/https.go create mode 100644 core/handler/https_test.go create mode 100644 core/handler/p2p.go create mode 100644 core/handler/p2p_test.go create mode 100644 core/handler/quic.go create mode 100644 core/handler/quic_test.go create mode 100644 core/handler/rdp.go create mode 100644 core/handler/rdp_test.go create mode 100644 core/handler/redis.go create mode 100644 core/handler/redis_test.go create mode 100644 core/handler/socks5.go create mode 100644 core/handler/socks5_test.go create mode 100644 core/handler/socks5_udp.go create mode 100644 core/handler/socks5_udp_test.go create mode 100644 core/handler/transparent.go create mode 100644 core/limiter/conn_num.go create mode 100644 core/limiter/conn_num_test.go create mode 100644 core/limiter/flow.go create mode 100644 core/limiter/flow_test.go create mode 100644 core/limiter/ip_conn_num.go create mode 100644 core/limiter/ip_conn_num_test.go create mode 100644 core/limiter/limiter.go create mode 100644 core/limiter/rate.go create mode 100644 core/limiter/rate_test.go create mode 100644 core/process/http_proxy.go create mode 100644 core/process/http_proxy_test.go create mode 100644 core/process/http_serve.go create mode 100644 core/process/http_serve_test.go create mode 100644 core/process/https_proxy.go create mode 100644 core/process/https_proxy_test.go create mode 100644 core/process/https_redirect.go create mode 100644 core/process/https_redirect_test.go create mode 100644 core/process/https_serve.go create mode 100644 core/process/https_serve_test.go create mode 100644 core/process/pb_app.go create mode 100644 core/process/pb_app_test.go create mode 100644 core/process/pb_ping.go create mode 100644 core/process/pb_ping_test.go create mode 100644 core/process/process.go create mode 100644 core/process/serve.go create mode 100644 core/process/serve_test.go create mode 100644 core/process/socks5.go create mode 100644 core/process/socks5_test.go create mode 100644 core/process/transparent_linux.go create mode 100644 core/process/transparent_others.go create mode 100644 core/rule/list.go create mode 100644 core/rule/list_test.go create mode 100644 core/rule/rule.go create mode 100644 core/rule/rule_json.go create mode 100644 core/rule/rule_json_test.go create mode 100644 core/rule/rule_test.go create mode 100644 core/rule/sort.go create mode 100644 core/rule/sort_test.go create mode 100644 core/server/server.go create mode 100644 core/server/tcp.go create mode 100644 core/server/udp.go create mode 100644 db/db.go create mode 100644 db/db_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 lib/cert/cert.go create mode 100644 lib/cert/cert_test.go create mode 100644 lib/cert/client_hello.go create mode 100644 lib/cert/generate.go create mode 100644 lib/cert/generate_test.go create mode 100644 lib/common/addr.go create mode 100644 lib/common/utils.go create mode 100644 lib/enet/conn.go create mode 100644 lib/enet/conn_test.go create mode 100644 lib/enet/listener.go create mode 100644 lib/enet/paket.go create mode 100644 lib/enet/paket_test.go create mode 100644 lib/enet/s5_packet.go create mode 100644 lib/enet/s5_packet_test.go create mode 100644 lib/lb/algo.go create mode 100644 lib/lb/lb.go create mode 100644 lib/lb/lb_test.go create mode 100644 lib/logger/logger.go create mode 100644 lib/logger/logger_others.go create mode 100644 lib/logger/logger_windows.go create mode 100644 lib/pb/bridge.pb.go create mode 100644 lib/pb/bridge.proto create mode 100644 lib/pb/pb.go create mode 100644 lib/pb/pb_test.go create mode 100644 lib/pool/goroutine.go create mode 100644 lib/pool/pool.go create mode 100644 lib/rate/rate.go create mode 100644 lib/rate/rate_test.go create mode 100644 transport/quic.go create mode 100644 transport/transport.go create mode 100644 transport/yamux.go diff --git a/component/bridge/bridge.go b/component/bridge/bridge.go new file mode 100644 index 0000000..aeaf8a9 --- /dev/null +++ b/component/bridge/bridge.go @@ -0,0 +1,23 @@ +package bridge + +import ( + "crypto/tls" + "github.com/lucas-clemente/quic-go" + "net" +) + +func StartTcpBridge(ln net.Listener, config *tls.Config, serverCheck, clientCheck func(string) bool) error { + h, err := NewTcpServer(ln, config, serverCheck, clientCheck) + if err != nil { + return err + } + return h.run() +} + +func StartQUICBridge(ln net.PacketConn, config *tls.Config, quicConfig *quic.Config, clientCheck func(string) bool) error { + h, err := NewQUICServer(ln, config, quicConfig, clientCheck) + if err != nil { + return err + } + return h.run() +} diff --git a/component/bridge/client.go b/component/bridge/client.go new file mode 100644 index 0000000..526e94c --- /dev/null +++ b/component/bridge/client.go @@ -0,0 +1,102 @@ +package bridge + +import ( + "context" + "ehang.io/nps/lib/logger" + "ehang.io/nps/lib/pb" + "ehang.io/nps/transport" + "github.com/pkg/errors" + "go.uber.org/zap" + "net" + "net/http" + "time" +) + +type client struct { + *manager + tunnelId string + clientId string + control transport.Conn + data transport.Conn + httpClient *http.Client + pingErrTimes int +} + +func NewClient(tunnelId string, clientId string, mg *manager) *client { + c := &client{tunnelId: tunnelId, clientId: clientId, manager: mg} + c.httpClient = &http.Client{Transport: &http.Transport{DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return c.NewControlConn() + }}} + go c.doPing() + return c +} + +func (c *client) SetTunnel(conn transport.Conn, control bool) { + if control { + c.control = conn + } else { + c.data = conn + } +} + +func (c *client) NewDataConn() (net.Conn, error) { + if c.data == nil { + return nil, errors.New("the data tunnel is not exist") + } + return c.data.Open() +} + +func (c *client) NewControlConn() (net.Conn, error) { + if c.control == nil { + return nil, errors.New("the data tunnel is not exist") + } + return c.control.Open() +} + +func (c *client) doPing() { + for range time.NewTicker(time.Second * 5).C { + if err := c.ping(); err != nil { + c.pingErrTimes++ + logger.Error("do ping error", zap.Error(err)) + } else { + logger.Debug("do ping success", zap.String("client id", c.clientId), zap.String("tunnel id", c.tunnelId)) + c.pingErrTimes = 0 + } + if c.pingErrTimes > 3 { + logger.Error("ping failed, close") + c.close() + return + } + } +} + +func (c *client) close() { + if c.data != nil { + _ = c.data.Close() + } + if c.control != nil { + _ = c.control.Close() + } + _ = c.RemoveClient(c) +} + +func (c *client) ping() error { + conn, err := c.NewDataConn() + if err != nil { + return errors.Wrap(err, "data ping") + } + _, err = pb.WriteMessage(conn, &pb.ClientRequest{ConnType: &pb.ClientRequest_Ping{Ping: &pb.Ping{Now: time.Now().String()}}}) + if err != nil { + return errors.Wrap(err, "data ping") + } + _, err = pb.ReadMessage(conn, &pb.Ping{}) + if err != nil { + return errors.Wrap(err, "data ping") + } + resp, err := c.httpClient.Get("http://nps.ehang.io/ping") + if err != nil { + return errors.Wrap(err, "control ping") + } + _ = resp.Body.Close() + return nil +} diff --git a/component/bridge/manager.go b/component/bridge/manager.go new file mode 100644 index 0000000..590625a --- /dev/null +++ b/component/bridge/manager.go @@ -0,0 +1,60 @@ +package bridge + +import ( + "ehang.io/nps/lib/lb" + "ehang.io/nps/lib/logger" + "ehang.io/nps/transport" + "go.uber.org/zap" + "net" + "sync" +) + +type manager struct { + clients map[string]*client + clientLb *lb.LoadBalancer + sync.Mutex +} + +func NewManager() *manager { + return &manager{ + clients: make(map[string]*client), + clientLb: lb.NewLoadBalancer(), + } +} + +func (m *manager) SetClient(clientId string, tunnelId string, isControl bool, conn transport.Conn) error { + m.Lock() + defer m.Unlock() + client, ok := m.clients[tunnelId] + if !ok { + client = NewClient(tunnelId, clientId, m) + err := m.clientLb.SetClient(clientId, client) + if err != nil { + logger.Error("set client error", zap.Error(err), zap.String("clientId", clientId), zap.String("tunnelId", tunnelId)) + return err + } + m.clients[tunnelId] = client + } + client.SetTunnel(conn, isControl) + return nil +} + +func (m *manager) GetDataConn(clientId string) (net.Conn, error) { + c, err := m.clientLb.GetClient(clientId) + if err != nil { + return nil, err + } + return c.(*client).NewDataConn() +} + +func (m *manager) RemoveClient(client *client) error { + m.Lock() + defer m.Unlock() + err := m.clientLb.RemoveClient(client.clientId, client) + if err != nil { + logger.Error("remove client error", zap.Error(err), zap.String("clientId", client.clientId), zap.String("tunnelId", client.tunnelId)) + return err + } + delete(m.clients, client.tunnelId) + return nil +} diff --git a/component/bridge/quic.go b/component/bridge/quic.go new file mode 100644 index 0000000..f66abc1 --- /dev/null +++ b/component/bridge/quic.go @@ -0,0 +1,91 @@ +package bridge + +import ( + "context" + "crypto/tls" + "ehang.io/nps/lib/logger" + "ehang.io/nps/lib/pb" + "ehang.io/nps/transport" + "github.com/lucas-clemente/quic-go" + "github.com/panjf2000/ants/v2" + "go.uber.org/zap" + "io" + "net" +) + +type QUICServer struct { + packetConn net.PacketConn + tlsConfig *tls.Config + config *quic.Config + listener quic.Listener + gp *ants.PoolWithFunc + clientCheck func(string) bool + manager *manager +} + +func NewQUICServer(packetConn net.PacketConn, tlsConfig *tls.Config, config *quic.Config, clientCheck func(string) bool) (*QUICServer, error) { + qs := &QUICServer{ + packetConn: packetConn, + tlsConfig: tlsConfig, + config: config, + clientCheck: clientCheck, + manager: NewManager(), + } + var err error + if qs.listener, err = quic.Listen(packetConn, tlsConfig, config); err != nil { + return nil, err + } + qs.gp, err = ants.NewPoolWithFunc(1000000, func(i interface{}) { + session := i.(quic.Session) + logger.Debug("accept a session", zap.String("remote addr", session.RemoteAddr().String())) + stream, err := session.AcceptStream(context.Background()) + if err != nil { + logger.Warn("accept stream error", zap.Error(err)) + _ = session.CloseWithError(0, "check auth failed") + return + } + cr := &pb.ConnRequest{} + _, err = pb.ReadMessage(stream, cr) + if err != nil { + _ = session.CloseWithError(0, "check auth failed") + logger.Warn("read message error", zap.Error(err)) + return + } + if !qs.clientCheck(cr.GetId()) { + _ = session.CloseWithError(0, "check auth failed") + logger.Error("check server id error", zap.String("id", cr.GetId())) + _ = qs.responseClient(stream, false, "id check failed") + return + } + qc := transport.NewQUIC(session) + _ = qc.Client() + + _ = qs.responseClient(stream, true, "success") + err = qs.manager.SetClient(cr.GetId(), cr.GetNpcInfo().GetTunnelId(), cr.GetNpcInfo().GetIsControlTunnel(), qc) + if err != nil { + _ = session.CloseWithError(0, "check auth failed") + logger.Error("set client error", zap.Error(err), zap.String("info", cr.String())) + } + }) + return qs, err +} + +func (qs *QUICServer) responseClient(conn io.Writer, success bool, msg string) error { + _, err := pb.WriteMessage(conn, &pb.NpcResponse{Success: success, Message: msg}) + return err +} + +func (qs *QUICServer) run() error { + for { + session, err := qs.listener.Accept(context.Background()) + if err != nil { + logger.Error("accept connection failed", zap.Error(err)) + return err + } + err = qs.gp.Invoke(session) + if err != nil { + logger.Error("Invoke session error", zap.Error(err)) + continue + } + } +} diff --git a/component/bridge/tcp.go b/component/bridge/tcp.go new file mode 100644 index 0000000..34867ce --- /dev/null +++ b/component/bridge/tcp.go @@ -0,0 +1,116 @@ +package bridge + +import ( + "crypto/tls" + "ehang.io/nps/lib/logger" + "ehang.io/nps/lib/pb" + "ehang.io/nps/lib/pool" + "ehang.io/nps/transport" + "github.com/panjf2000/ants/v2" + "go.uber.org/zap" + "io" + "net" + "sync" +) + +type tcpServer struct { + config *tls.Config // config must contain root ca and server cert + ln net.Listener + gp *ants.PoolWithFunc + manager *manager + serverCheck func(id string) bool + clientCheck func(id string) bool +} + +func NewTcpServer(ln net.Listener, config *tls.Config, serverCheck, clientCheck func(string) bool) (*tcpServer, error) { + h := &tcpServer{ + config: config, + ln: ln, + serverCheck: serverCheck, + clientCheck: clientCheck, + manager: NewManager(), + } + var err error + h.gp, err = ants.NewPoolWithFunc(1000000, func(i interface{}) { + conn := i.(net.Conn) + cr := &pb.ConnRequest{} + _, err := pb.ReadMessage(conn, cr) + if err != nil { + _ = conn.Close() + logger.Warn("read message error", zap.Error(err)) + return + } + switch cr.ConnType.(type) { + case *pb.ConnRequest_AppInfo: + h.handleApp(cr, conn) + case *pb.ConnRequest_NpcInfo: + h.handleClient(cr, conn) + } + }) + return h, err +} + +func (h *tcpServer) handleApp(cr *pb.ConnRequest, conn net.Conn) { + if !h.serverCheck(cr.GetId()) { + _ = conn.Close() + logger.Error("check server id error", zap.String("id", cr.GetId())) + return + } + clientConn, err := h.manager.GetDataConn(cr.GetAppInfo().GetNpcId()) + if err != nil { + logger.Error("get client error", zap.Error(err), zap.String("app_info", cr.String())) + return + } + _, err = pb.WriteMessage(clientConn, &pb.ClientRequest{ConnType: &pb.ClientRequest_AppInfo{AppInfo: cr.GetAppInfo()}}) + if err != nil { + _ = clientConn.Close() + _ = conn.Close() + logger.Error("write app_info error", zap.Error(err)) + return + } + var wg sync.WaitGroup + wg.Add(2) + _ = pool.CopyConnGoroutinePool.Invoke(pool.CopyConnGpParams{Writer: conn, Reader: clientConn, Wg: &wg}) + _ = pool.CopyConnGoroutinePool.Invoke(pool.CopyConnGpParams{Writer: clientConn, Reader: conn, Wg: &wg}) + wg.Wait() +} + +func (h *tcpServer) responseClient(conn io.Writer, success bool, msg string) error { + _, err := pb.WriteMessage(conn, &pb.NpcResponse{Success: success, Message: msg}) + return err +} + +func (h *tcpServer) handleClient(cr *pb.ConnRequest, conn net.Conn) { + if !h.clientCheck(cr.GetId()) { + _ = conn.Close() + logger.Error("check server id error", zap.String("id", cr.GetId())) + _ = h.responseClient(conn, false, "id check failed") + return + } + yc := transport.NewYaMux(conn, nil) + err := yc.Client() + if err != nil { + _ = conn.Close() + _ = h.responseClient(conn, false, "client failed") + logger.Error("new yamux client error", zap.Error(err), zap.String("remote address", conn.RemoteAddr().String())) + return + } + _ = h.responseClient(conn, true, "success") + err = h.manager.SetClient(cr.GetId(), cr.GetNpcInfo().GetTunnelId(), cr.GetNpcInfo().GetIsControlTunnel(), yc) + if err != nil { + _ = conn.Close() + logger.Error("set client error", zap.Error(err), zap.String("info", cr.String())) + } +} + +func (h *tcpServer) run() error { + h.ln = tls.NewListener(h.ln, h.config) + for { + conn, err := h.ln.Accept() + if err != nil { + logger.Error("Accept conn error", zap.Error(err)) + return err + } + _ = h.gp.Invoke(conn) + } +} diff --git a/component/client/client.go b/component/client/client.go new file mode 100644 index 0000000..f6167f5 --- /dev/null +++ b/component/client/client.go @@ -0,0 +1,129 @@ +package client + +import ( + "ehang.io/nps/core/action" + "ehang.io/nps/core/handler" + "ehang.io/nps/core/process" + "ehang.io/nps/core/rule" + "ehang.io/nps/lib/enet" + "ehang.io/nps/lib/logger" + "go.uber.org/zap" + "io" + "net" + "net/http" + "sync" + "sync/atomic" + "time" +) + +type Client struct { + controlLn net.Listener + dataLn net.Listener + lastPongTime time.Time + mux *http.ServeMux + ticker *time.Ticker + closeCh chan struct{} + closed int32 + wg sync.WaitGroup +} + +func NewClient(controlLn, dataLn net.Listener) *Client { + return &Client{ + controlLn: controlLn, + dataLn: dataLn, + mux: &http.ServeMux{}, + ticker: time.NewTicker(time.Second * 5), + closeCh: make(chan struct{}, 0), + } +} + +func (c *Client) ping(writer http.ResponseWriter, request *http.Request) { + c.lastPongTime = time.Now() + _, err := io.WriteString(writer, "pong") + if err != nil { + logger.Warn("write pong error", zap.Error(err)) + return + } + logger.Debug("write pong success") +} + +func (c *Client) Run() { + c.mux.HandleFunc("/ping", c.ping) + c.wg.Add(3) + go c.handleControlConn() + go c.handleDataConn() + go c.checkPing() + c.wg.Wait() +} + +func (c *Client) HasPong() bool { + return time.Now().Sub(c.lastPongTime).Seconds() < 10 +} + +func (c *Client) checkPing() { + for { + select { + case <-c.ticker.C: + if !c.lastPongTime.IsZero() && time.Now().Sub(c.lastPongTime).Seconds() > 15 && c.controlLn != nil { + logger.Debug("close connection", zap.Time("lastPongTime", c.lastPongTime), zap.Time("now", time.Now())) + _ = c.controlLn.Close() + } + case <-c.closeCh: + c.wg.Done() + return + } + } +} + +func (c *Client) handleDataConn() { + h := &handler.DefaultHandler{} + ac := &action.LocalAction{} + err := ac.Init() + if err != nil { + logger.Warn("init action failed", zap.Error(err)) + return + } + appPr := &process.PbAppProcessor{} + _ = appPr.Init(ac) + h.AddRule(&rule.Rule{Handler: h, Process: appPr, Action: ac}) + + pingPr := &process.PbPingProcessor{} + _ = appPr.Init(ac) + h.AddRule(&rule.Rule{Handler: h, Process: pingPr, Action: ac}) + + var conn net.Conn + for { + conn, err = c.dataLn.Accept() + if err != nil { + logger.Error("accept connection failed", zap.Error(err)) + break + } + go func(conn net.Conn) { + _, err = h.HandleConn(nil, enet.NewReaderConn(conn)) + if err != nil { + logger.Warn("process failed", zap.Error(err)) + return + } + }(conn) + } + c.wg.Done() + c.Close() +} + +func (c *Client) handleControlConn() { + err := http.Serve(c.controlLn, c.mux) + if err != nil { + logger.Error("http error", zap.Error(err)) + } + c.wg.Done() + c.Close() +} + +func (c *Client) Close() { + if atomic.CompareAndSwapInt32(&c.closed, 0, 1) { + c.closeCh <- struct{}{} + c.ticker.Stop() + _ = c.controlLn.Close() + _ = c.dataLn.Close() + } +} diff --git a/component/client/conn.go b/component/client/conn.go new file mode 100644 index 0000000..557ea12 --- /dev/null +++ b/component/client/conn.go @@ -0,0 +1,69 @@ +package client + +import ( + "crypto/tls" + "ehang.io/nps/lib/pb" + "ehang.io/nps/transport" + "github.com/lucas-clemente/quic-go" + "github.com/pkg/errors" + "io" + "net" + "time" +) + +type TunnelCreator interface { + NewMux(bridgeAddr string, message *pb.ConnRequest, config *tls.Config) (net.Listener, error) +} + +type BaseTunnelCreator struct{} + +func (bc BaseTunnelCreator) handshake(npcInfo *pb.ConnRequest, rw io.ReadWriteCloser) error { + _, err := pb.WriteMessage(rw, npcInfo) + if err != nil { + return errors.Wrap(err, "write handshake message") + } + var resp pb.NpcResponse + _, err = pb.ReadMessage(rw, &resp) + if err != nil || !resp.Success { + return errors.Wrap(err, resp.Message) + } + return nil +} + +type TcpTunnelCreator struct{ BaseTunnelCreator } + +func (tc TcpTunnelCreator) NewMux(bridgeAddr string, message *pb.ConnRequest, config *tls.Config) (net.Listener, error) { + conn, err := tls.Dial("tcp", bridgeAddr, config) + if err != nil { + return nil, err + } + if err := tc.handshake(message, conn); err != nil { + return nil, err + } + server := transport.NewYaMux(conn, nil) + return server, server.Server() +} + +type QUICTunnelCreator struct{ BaseTunnelCreator } + +func (tc QUICTunnelCreator) NewMux(bridgeAddr string, message *pb.ConnRequest, config *tls.Config) (net.Listener, error) { + session, err := quic.DialAddr(bridgeAddr, config, &quic.Config{ + MaxIncomingStreams: 1000000, + MaxIncomingUniStreams: 1000000, + MaxIdleTimeout: time.Minute, + KeepAlive: true, + }) + if err != nil { + return nil, err + } + stream, err := session.OpenStream() + if err != nil { + return nil, err + } + err = tc.handshake(message, stream) + if err != nil { + return nil, err + } + server := transport.NewQUIC(session) + return server, server.Server() +} diff --git a/component/client/npc.go b/component/client/npc.go new file mode 100644 index 0000000..500bda4 --- /dev/null +++ b/component/client/npc.go @@ -0,0 +1,44 @@ +package client + +import ( + "crypto/tls" + "ehang.io/nps/lib/cert" + "ehang.io/nps/lib/logger" + "ehang.io/nps/lib/pb" + uuid "github.com/satori/go.uuid" + "go.uber.org/zap" +) + +// StartNpc is used to connect to bridge +// proto is quic or tcp +// tlsConfig must contain a npc cert +func StartNpc(proto string, bridgeAddr string, tlsConfig *tls.Config) error { + id, err := cert.GetCertSnFromConfig(tlsConfig) + if err != nil { + return err + } + var creator TunnelCreator + if proto == "quic" { + creator = QUICTunnelCreator{} + } else { + creator = TcpTunnelCreator{} + } + connId := uuid.NewV1().String() +retry: + logger.Info("start connecting to bridge") + controlLn, err := creator.NewMux(bridgeAddr, + &pb.ConnRequest{Id: id, ConnType: &pb.ConnRequest_NpcInfo{NpcInfo: &pb.NpcInfo{TunnelId: connId, IsControlTunnel: true}}}, tlsConfig) + if err != nil { + logger.Error("new control connection error", zap.Error(err)) + goto retry + } + dataLn, err := creator.NewMux(bridgeAddr, + &pb.ConnRequest{Id: id, ConnType: &pb.ConnRequest_NpcInfo{NpcInfo: &pb.NpcInfo{TunnelId: connId, IsControlTunnel: false}}}, tlsConfig) + if err != nil { + logger.Error("new data connection error", zap.Error(err)) + goto retry + } + c := NewClient(controlLn, dataLn) + c.Run() + goto retry +} diff --git a/component/component.go b/component/component.go new file mode 100644 index 0000000..cc264c9 --- /dev/null +++ b/component/component.go @@ -0,0 +1 @@ +package component diff --git a/component/component_test.go b/component/component_test.go new file mode 100644 index 0000000..9572277 --- /dev/null +++ b/component/component_test.go @@ -0,0 +1,193 @@ +package component + +import ( + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "ehang.io/nps/component/bridge" + "ehang.io/nps/component/client" + "ehang.io/nps/lib/cert" + "ehang.io/nps/lib/pb" + "github.com/lucas-clemente/quic-go" + uuid "github.com/satori/go.uuid" + "github.com/stretchr/testify/assert" + "io/ioutil" + "net" + "os" + "path/filepath" + "sync" + "testing" + "time" +) + +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(), "root_cert.pem"), cert, 0600)) + assert.NoError(t, os.WriteFile(filepath.Join(os.TempDir(), "root_key.pem"), key, 0600)) + assert.NoError(t, g.InitRootCa(cert, key)) + cert, key, err = g.CreateCert("bridge.nps.ehang.io") + assert.NoError(t, err) + assert.NoError(t, os.WriteFile(filepath.Join(os.TempDir(), "bridge_cert.pem"), cert, 0600)) + assert.NoError(t, os.WriteFile(filepath.Join(os.TempDir(), "bridge_key.pem"), key, 0600)) + cert, key, err = g.CreateCert("client.nps.ehang.io") + assert.NoError(t, err) + assert.NoError(t, os.WriteFile(filepath.Join(os.TempDir(), "client_cert.pem"), cert, 0600)) + assert.NoError(t, os.WriteFile(filepath.Join(os.TempDir(), "client_key.pem"), key, 0600)) + }) + return filepath.Join(os.TempDir(), "cert.pem"), filepath.Join(os.TempDir(), "key.pem") +} + +func TestTcpConnect(t *testing.T) { + createCertFile(t) + bridgeLn, err := net.Listen("tcp", "127.0.0.1:0") + assert.NoError(t, err) + + buf, err := ioutil.ReadFile(filepath.Join(os.TempDir(), "root_cert.pem")) + assert.NoError(t, err) + pool := x509.NewCertPool() + pool.AppendCertsFromPEM(buf) + + crt, err := tls.LoadX509KeyPair(filepath.Join(os.TempDir(), "bridge_cert.pem"), filepath.Join(os.TempDir(), "bridge_key.pem")) + assert.NoError(t, err) + + bridgeTlsConfig := &tls.Config{ + Certificates: []tls.Certificate{crt}, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: pool, + } + + crt, err = tls.LoadX509KeyPair(filepath.Join(os.TempDir(), "client_cert.pem"), filepath.Join(os.TempDir(), "client_key.pem")) + assert.NoError(t, err) + + clientConfig := &tls.Config{ + InsecureSkipVerify: true, + Certificates: []tls.Certificate{crt}, + } + go func() { + assert.NoError(t, bridge.StartTcpBridge(bridgeLn, bridgeTlsConfig, func(s string) bool { + return true + }, func(s string) bool { + sn, err := cert.GetCertSnFromConfig(clientConfig) + assert.NoError(t, err) + assert.Equal(t, sn, s) + return true + })) + }() + var c *client.Client + go func() { + id, err := cert.GetCertSnFromConfig(clientConfig) + assert.NoError(t, err) + + creator := client.TcpTunnelCreator{} + connId := uuid.NewV1().String() + controlLn, err := creator.NewMux(bridgeLn.Addr().String(), + &pb.ConnRequest{Id: id, ConnType: &pb.ConnRequest_NpcInfo{NpcInfo: &pb.NpcInfo{TunnelId: connId, IsControlTunnel: true}}}, clientConfig) + assert.NoError(t, err) + dataLn, err := creator.NewMux(bridgeLn.Addr().String(), + &pb.ConnRequest{Id: id, ConnType: &pb.ConnRequest_NpcInfo{NpcInfo: &pb.NpcInfo{TunnelId: connId, IsControlTunnel: false}}}, clientConfig) + assert.NoError(t, err) + c = client.NewClient(controlLn, dataLn) + c.Run() + }() + timeout := time.NewTimer(time.Second * 30) + ticker := time.NewTicker(time.Millisecond * 100) + for { + select { + case <-ticker.C: + if c != nil && c.HasPong() { + c.Close() + return + } + case <-timeout.C: + t.Fail() + return + } + } +} + +func TestQUICConnect(t *testing.T) { + createCertFile(t) + bridgePacketConn, err := net.ListenPacket("udp", "127.0.0.1:0") + assert.NoError(t, err) + + buf, err := ioutil.ReadFile(filepath.Join(os.TempDir(), "root_cert.pem")) + assert.NoError(t, err) + pool := x509.NewCertPool() + pool.AppendCertsFromPEM(buf) + + crt, err := tls.LoadX509KeyPair(filepath.Join(os.TempDir(), "bridge_cert.pem"), filepath.Join(os.TempDir(), "bridge_key.pem")) + assert.NoError(t, err) + + bridgeTlsConfig := &tls.Config{ + Certificates: []tls.Certificate{crt}, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: pool, + NextProtos: []string{"quic-nps"}, + } + + crt, err = tls.LoadX509KeyPair(filepath.Join(os.TempDir(), "client_cert.pem"), filepath.Join(os.TempDir(), "client_key.pem")) + assert.NoError(t, err) + + clientConfig := &tls.Config{ + InsecureSkipVerify: true, + Certificates: []tls.Certificate{crt}, + NextProtos: []string{"quic-nps"}, + } + go func() { + assert.NoError(t, bridge.StartQUICBridge(bridgePacketConn, bridgeTlsConfig, &quic.Config{ + MaxIncomingStreams: 1000000, + MaxIncomingUniStreams: 1000000, + MaxIdleTimeout: time.Minute, + KeepAlive: true, + }, func(s string) bool { + sn, err := cert.GetCertSnFromConfig(clientConfig) + assert.NoError(t, err) + assert.Equal(t, sn, s) + return true + })) + }() + var c *client.Client + go func() { + id, err := cert.GetCertSnFromConfig(clientConfig) + assert.NoError(t, err) + + creator := client.QUICTunnelCreator{} + connId := uuid.NewV1().String() + controlLn, err := creator.NewMux(bridgePacketConn.LocalAddr().String(), + &pb.ConnRequest{Id: id, ConnType: &pb.ConnRequest_NpcInfo{NpcInfo: &pb.NpcInfo{TunnelId: connId, IsControlTunnel: true}}}, clientConfig) + assert.NoError(t, err) + dataLn, err := creator.NewMux(bridgePacketConn.LocalAddr().String(), + &pb.ConnRequest{Id: id, ConnType: &pb.ConnRequest_NpcInfo{NpcInfo: &pb.NpcInfo{TunnelId: connId, IsControlTunnel: false}}}, clientConfig) + assert.NoError(t, err) + assert.NotEmpty(t, dataLn) + assert.NotEmpty(t, controlLn) + c = client.NewClient(controlLn, dataLn) + c.Run() + }() + timeout := time.NewTimer(time.Second * 30) + ticker := time.NewTicker(time.Millisecond * 100) + for { + select { + case <-ticker.C: + if c != nil && c.HasPong() { + c.Close() + return + } + case <-timeout.C: + t.Fail() + return + } + } +} diff --git a/component/controller/asset b/component/controller/asset new file mode 160000 index 0000000..07c2567 --- /dev/null +++ b/component/controller/asset @@ -0,0 +1 @@ +Subproject commit 07c2567751c711541a4ff24af00f2cd018c18eab diff --git a/component/controller/cert.go b/component/controller/cert.go new file mode 100644 index 0000000..89ac9ea --- /dev/null +++ b/component/controller/cert.go @@ -0,0 +1,92 @@ +package controller + +import ( + "crypto/x509/pkix" + "ehang.io/nps/lib/cert" + "encoding/json" + "fmt" + "github.com/gin-gonic/gin" + uuid "github.com/satori/go.uuid" + "net/http" +) + +type certServe struct { + baseController + cg *cert.X509Generator +} + +func (cs *certServe) Init(rootCert []byte, rootKey []byte) error { + cs.cg = cert.NewX509Generator(pkix.Name{ + Country: []string{"cn"}, + Organization: []string{"ehang"}, + OrganizationalUnit: []string{"nps"}, + Province: []string{"beijing"}, + CommonName: "nps", + Locality: []string{"beijing"}, + }) + return cs.cg.InitRootCa(rootCert, rootKey) +} + +type certInfo struct { + Name string `json:"name"` + Uuid string `json:"uuid"` + CertType string `json:"cert_type"` + Cert string `json:"cert"` + Key string `json:"key"` + Sn string `json:"sn"` + Remark string `json:"remark"` + Status int `json:"status"` +} + +// Create +// Cert type root|bridge|server|client|secret +func (cs *certServe) Create(c *gin.Context) { + var ci certInfo + err := c.BindJSON(&ci) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + crt, key, err := cs.cg.CreateCert(fmt.Sprintf("%s.nps.ehang.io", ci.CertType)) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + sn, err := cert.GetCertSnFromEncode(crt) + ci.Cert, ci.Key, ci.Sn, ci.Uuid = string(crt), string(key), sn, uuid.NewV4().String() + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + b, err := json.Marshal(ci) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + err = cs.db.Insert("cert", ci.Uuid, string(b)) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok"}) +} + +func (cs *certServe) Update(c *gin.Context) { + var ci certInfo + err := c.BindJSON(&ci) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + b, err := json.Marshal(ci) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + err = cs.db.Update(cs.tableName, ci.Uuid, string(b)) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok"}) +} diff --git a/component/controller/config.go b/component/controller/config.go new file mode 100644 index 0000000..3ba0936 --- /dev/null +++ b/component/controller/config.go @@ -0,0 +1,50 @@ +package controller + +import ( + "ehang.io/nps/db" + "fmt" + "github.com/gin-gonic/gin" + "net/http" +) + +type configServer struct { + db db.Db +} + +func (cs *configServer) ChangeSystemConfig(c *gin.Context) { + type config struct { + OldPassword string `json:"old_password"` + NewPassword string `json:"new_password"` + NewUsername string `json:"new_username"` + } + var cfg config + err := c.BindJSON(&cfg) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + oldPassword, err := cs.db.GetConfig("admin_pass") + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + fmt.Println(cfg, oldPassword) + if cfg.OldPassword != oldPassword { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": "old password does not match"}) + return + } + if err := cs.db.SetConfig("admin_pass", cfg.NewPassword); err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + if cfg.NewUsername != "" { + if err := cs.db.SetConfig("admin_pass", cfg.NewUsername); err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + } + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "message": "ok", + }) +} diff --git a/component/controller/controller.go b/component/controller/controller.go new file mode 100644 index 0000000..a0ff189 --- /dev/null +++ b/component/controller/controller.go @@ -0,0 +1,101 @@ +package controller + +import ( + "ehang.io/nps/db" + "ehang.io/nps/lib/logger" + jwt "github.com/appleboy/gin-jwt/v2" + "github.com/gin-gonic/gin" + "go.uber.org/zap" + "net" + "net/http" +) + +func StartController(ln net.Listener, db db.Db, rootCert []byte, rootKey []byte, staticRootPath string, pagePath string) error { + gin.SetMode(gin.ReleaseMode) + + cfgServer := &configServer{db: db} + crtServer := &certServe{baseController: baseController{db: db, tableName: "cert"}} + err := crtServer.Init(rootCert, rootKey) + if err != nil { + return err + } + ruleServer := &ruleServer{baseController: baseController{db: db, tableName: "rule"}} + + authMiddleware, err := newAuthMiddleware(db) + if err != nil { + return err + } + router := gin.New() + router.Use(CORSMiddleware(), 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 "" + })) + router.POST("/login", authMiddleware.LoginHandler) + + router.NoRoute(authMiddleware.MiddlewareFunc(), func(c *gin.Context) { + claims := jwt.ExtractClaims(c) + logger.Warn("NoRoute", zap.Any("claims", claims)) + c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"}) + }) + + auth := router.Group("/auth") + auth.Use(authMiddleware.MiddlewareFunc()) + auth.GET("/refresh_token", authMiddleware.RefreshHandler) + auth.GET("/userinfo", userinfo) + + v1 := router.Group("v1") + v1.Use(authMiddleware.MiddlewareFunc()) + { + v1.PUT("/config", cfgServer.ChangeSystemConfig) + + v1.GET("/status", status) + + v1.POST("/cert", crtServer.Create) + v1.DELETE("/cert", crtServer.Delete) + v1.GET("/cert/page", crtServer.Page) + v1.PUT("/cert", crtServer.Update) + + v1.POST("/rule", ruleServer.Create) + v1.DELETE("/rule", ruleServer.Delete) + v1.PUT("/rule", ruleServer.Update) + v1.GET("/rule", ruleServer.One) + v1.GET("/rule/page", ruleServer.Page) + v1.GET("/rule/field", ruleServer.Field) + v1.GET("/rule/limiter", ruleServer.Limiter) + } + router.Static("/static/", staticRootPath) + router.Static("/page/", pagePath) + + go storeSystemStatus() + + return router.RunListener(ln) +} + +func CORSMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + c.Writer.Header().Set("Access-Control-Allow-Origin", "*") + c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") + c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE") + + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + c.Next() + } +} + +func userinfo(c *gin.Context) { + c.Data(http.StatusOK, "application/json; charset=utf-8", + []byte(`{"code":0,"result":{"userId":"1","username":"vben","realName":"Vben Admin","avatar":"https://q1.qlogo.cn/g?b=qq&nk=190848757&s=640","desc":"manager","password":"123456","token":"fakeToken1","homePath":"/dashboard/analysis","roles":[{"roleName":"Super Admin","value":"super"}]},"message":"ok","type":"success"}`)) +} diff --git a/component/controller/controller_test.go b/component/controller/controller_test.go new file mode 100644 index 0000000..77d9827 --- /dev/null +++ b/component/controller/controller_test.go @@ -0,0 +1,149 @@ +package controller + +import ( + "bytes" + "crypto/x509/pkix" + "ehang.io/nps/core/action" + "ehang.io/nps/core/handler" + "ehang.io/nps/core/process" + "ehang.io/nps/core/rule" + "ehang.io/nps/core/server" + "ehang.io/nps/db" + "ehang.io/nps/lib/cert" + "encoding/json" + "errors" + "fmt" + "github.com/stretchr/testify/assert" + "github.com/tidwall/gjson" + "io/ioutil" + "net" + "net/http" + "os" + "path/filepath" + "sync" + "testing" + "time" +) + +func TestController(t *testing.T) { + ln, err := net.Listen("tcp", "127.0.0.1:3500") + assert.NoError(t, err) + err = os.Remove(filepath.Join(os.TempDir(), "test_control.db")) + d := db.NewSqliteDb(filepath.Join(os.TempDir(), "test_control.db")) + err = d.Init() + assert.NoError(t, err) + assert.NoError(t, d.SetConfig("admin_user", "admin")) + assert.NoError(t, d.SetConfig("admin_pass", "pass")) + cg := cert.NewX509Generator(pkix.Name{ + Country: []string{"cn"}, + Organization: []string{"ehang"}, + OrganizationalUnit: []string{"nps"}, + Province: []string{"beijing"}, + CommonName: "nps", + Locality: []string{"beijing"}, + }) + assert.NoError(t, err) + cert, key, err := cg.CreateRootCa() + assert.NoError(t, err) + go func() { + err = StartController(ln, d, cert, key, "./web/static/", "./web/views/") + assert.NoError(t, err) + }() + resp, err := doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/login"), "POST", `{"username": "admin","password": "pass"}`) + assert.NoError(t, err) + assert.Equal(t, int(gjson.Parse(resp).Get("code").Int()), 0) + + for i := 0; i < 18; i++ { + resp, err = doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/v1/cert"), "POST", fmt.Sprintf(`{"status":1,"name":"name_%d","cert_type": "client"}`, i)) + assert.NoError(t, err) + assert.Equal(t, int(gjson.Parse(resp).Get("code").Int()), 0) + } + + resp, err = doRequest(fmt.Sprintf("http://%s%s?page=%d&pageSize=%d", ln.Addr().String(), "/v1/cert/page", 4, 5), "GET", ``) + assert.NoError(t, err) + now := 2 + var lastUuid string + assert.Equal(t, len(gjson.Parse(resp).Get("result.items").Array()), 3) + for _, v := range gjson.Parse(resp).Get("result.items").Array() { + assert.Equal(t, v.Get("name").String(), fmt.Sprintf(`name_%d`, now)) + lastUuid = v.Get("uuid").String() + now-- + } + + resp, err = doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/v1/cert"), "DELETE", fmt.Sprintf(`{"uuid":"%s"}`, lastUuid)) + assert.NoError(t, err) + assert.Equal(t, int(gjson.Parse(resp).Get("code").Int()), 0) + + s := &server.TcpServer{ServerAddr: "127.0.0.1:0"} + h := &handler.DefaultHandler{} + p := &process.DefaultProcess{} + a := &action.LocalAction{} + rj := &rule.JsonRule{ + Name: "test", + Status: 1, + Server: rule.JsonData{ObjType: s.GetName(), ObjData: getJson(t, s)}, + Handler: rule.JsonData{ObjType: h.GetName(), ObjData: getJson(t, h)}, + Process: rule.JsonData{ObjType: p.GetName(), ObjData: getJson(t, p)}, + Action: rule.JsonData{ObjType: a.GetName(), ObjData: getJson(t, a)}, + Limiters: []rule.JsonData{}, + } + js := getJson(t, rj) + for i := 0; i < 18; i++ { + resp, err = doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/v1/rule"), "POST", js) + assert.NoError(t, err) + assert.Equal(t, int(gjson.Parse(resp).Get("code").Int()), 0) + } + resp, err = doRequest(fmt.Sprintf("http://%s%s?page=%d&pageSize=%d", ln.Addr().String(), "/v1/rule/page", 1, 10), "GET", ``) + assert.NoError(t, err) + assert.Equal(t, int(gjson.Parse(resp).Get("result.total").Int()), 18) + + uuid := gjson.Parse(resp).Get("result.items").Array()[0].Get("uuid").String() + resp, err = doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/v1/rule"), "GET", fmt.Sprintf(`{"uuid": "%s"}`, uuid)) + assert.NoError(t, err) + assert.Equal(t, gjson.Parse(resp).Get("result.uuid").String(), uuid) + + rj.Uuid = uuid + resp, err = doRequest(fmt.Sprintf("http://%s%s", ln.Addr().String(), "/v1/rule"), "PUT", getJson(t, rj)) + assert.NoError(t, err) + assert.Equal(t, int(gjson.Parse(resp).Get("code").Int()), 0) + time.Sleep(time.Minute * 600) +} + +func getJson(t *testing.T, i interface{}) string { + b, err := json.Marshal(i) + assert.NoError(t, err) + assert.NotEmpty(t, string(b)) + return string(b) +} + +var client *http.Client +var once sync.Once +var cookies []*http.Cookie + +func doRequest(url string, method string, body string) (string, error) { + once.Do(func() { + client = &http.Client{} + }) + payload := bytes.NewBufferString(body) + req, err := http.NewRequest(method, url, payload) + if err != nil { + return "", err + } + req.Header.Add("Content-Type", "application/json") + for _, c := range cookies { + req.AddCookie(c) + } + res, err := client.Do(req) + if err != nil { + return "", err + } + defer res.Body.Close() + b, err := ioutil.ReadAll(res.Body) + if len(res.Cookies()) > 0 { + cookies = res.Cookies() + } + if res.StatusCode != 200 { + return string(b), errors.New("bad doRequest") + } + return string(b), err +} diff --git a/component/controller/jwt.go b/component/controller/jwt.go new file mode 100644 index 0000000..a454c8d --- /dev/null +++ b/component/controller/jwt.go @@ -0,0 +1,105 @@ +package controller + +import ( + "ehang.io/nps/db" + "net/http" + "time" + + jwt "github.com/appleboy/gin-jwt/v2" + "github.com/gin-gonic/gin" +) + +type login struct { + Username string `form:"username" json:"username" binding:"required"` + Password string `form:"password" json:"password" binding:"required"` +} + +var identityKey = "id" + +type User struct { + UserName string +} + +func newAuthMiddleware(db db.Db) (authMiddleware *jwt.GinJWTMiddleware, err error) { + authMiddleware, err = jwt.New(&jwt.GinJWTMiddleware{ + Realm: "nps", + Key: []byte("secret key"), + Timeout: time.Hour * 24, + MaxRefresh: time.Hour * 72, + IdentityKey: identityKey, + SendCookie: true, + LoginResponse: func(c *gin.Context, code int, message string, time time.Time) { + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "result": gin.H{ + "token": message, + }, + "message": "ok", + }) + }, + PayloadFunc: func(data interface{}) jwt.MapClaims { + if v, ok := data.(*User); ok { + return jwt.MapClaims{ + identityKey: v.UserName, + } + } + return jwt.MapClaims{} + }, + IdentityHandler: func(c *gin.Context) interface{} { + claims := jwt.ExtractClaims(c) + return &User{ + UserName: claims[identityKey].(string), + } + }, + Authenticator: func(c *gin.Context) (interface{}, error) { + var loginVals login + if err := c.ShouldBind(&loginVals); err != nil { + return "", jwt.ErrMissingLoginValues + } + userID := loginVals.Username + password := loginVals.Password + adminUser, err := db.GetConfig("admin_user") + if err != nil { + return "", jwt.ErrFailedAuthentication + } + adminPass, err := db.GetConfig("admin_pass") + if err != nil { + return "", jwt.ErrFailedAuthentication + } + if userID == adminUser && password == adminPass { + return &User{ + UserName: userID, + }, nil + } + + return nil, jwt.ErrFailedAuthentication + }, + Authorizator: func(data interface{}, c *gin.Context) bool { + adminUser, err := db.GetConfig("admin_user") + if err != nil { + return false + } + if v, ok := data.(*User); ok && v.UserName ==adminUser { + return true + } + return false + }, + Unauthorized: func(c *gin.Context, code int, message string) { + c.JSON(code, gin.H{ + "code": code, + "message": message, + }) + }, + TokenLookup: "header: Authorization, query: token, cookie: jwt", + TokenHeadName: "Bearer", + TimeFunc: time.Now, + }) + if err != nil { + return + } + err = authMiddleware.MiddlewareInit() + if err != nil { + return + } + return +} diff --git a/component/controller/rule.go b/component/controller/rule.go new file mode 100644 index 0000000..84a2da4 --- /dev/null +++ b/component/controller/rule.go @@ -0,0 +1,151 @@ +package controller + +import ( + "ehang.io/nps/core/rule" + "ehang.io/nps/db" + "encoding/json" + "github.com/gin-gonic/gin" + uuid "github.com/satori/go.uuid" + "net/http" + "strconv" +) + +type baseController struct { + db db.Db + tableName string +} + +func (bc *baseController) Page(c *gin.Context) { + page, err := strconv.Atoi(c.Query("page")) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + pageSize, err := strconv.Atoi(c.Query("pageSize")) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + dataArr, err := bc.db.QueryPage(bc.tableName, pageSize, (page-1)*pageSize, c.Query("key")) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + list := make([]map[string]interface{}, 0) + for _, s := range dataArr { + dd := make(map[string]interface{}, 0) + err = json.Unmarshal([]byte(s), &dd) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + list = append(list, dd) + } + n, err := bc.db.Count(bc.tableName, c.Query("key")) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "message": "ok", + "result": gin.H{ + "items": list, + "total": n, + }, + }) +} + +func (bc *baseController) Delete(c *gin.Context) { + type uid struct { + Uuid string + } + var js uid + err := c.BindJSON(&js) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + err = bc.db.Delete(bc.tableName, js.Uuid) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok"}) +} + +type ruleServer struct { + baseController +} + +func (rs *ruleServer) Create(c *gin.Context) { + var jr rule.JsonRule + err := c.BindJSON(&jr) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + jr.Uuid = uuid.NewV4().String() + b, err := json.Marshal(jr) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + err = rs.db.Insert(rs.tableName, jr.Uuid, string(b)) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok"}) +} + +func (rs *ruleServer) Update(c *gin.Context) { + var jr rule.JsonRule + err := c.BindJSON(&jr) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + b, err := json.Marshal(jr) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + err = rs.db.Update(rs.tableName, jr.Uuid, string(b)) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"code": 0, "message": "ok"}) +} + +func (rs *ruleServer) One(c *gin.Context) { + var js map[string]string + err := c.BindJSON(&js) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + s, err := rs.db.QueryOne("rule", js["uuid"]) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + var r rule.JsonRule + err = json.Unmarshal([]byte(s), &r) + if err != nil { + c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()}) + return + } + c.JSON(http.StatusOK, gin.H{"code": 0, "result": r, "message": "ok"}) +} + +func (rs *ruleServer) Field(c *gin.Context) { + chains := rule.GetChains() + c.JSON(http.StatusOK, gin.H{"code": 0, "result": chains, "message": "ok"}) +} + +func (rs *ruleServer) Limiter(c *gin.Context) { + chains := rule.GetLimiters() + c.JSON(http.StatusOK, gin.H{"code": 0, "result": chains, "message": "ok"}) +} diff --git a/component/controller/status.go b/component/controller/status.go new file mode 100644 index 0000000..2aa8a09 --- /dev/null +++ b/component/controller/status.go @@ -0,0 +1,152 @@ +package controller + +import ( + "container/list" + "github.com/gin-gonic/gin" + "github.com/shirou/gopsutil/cpu" + "github.com/shirou/gopsutil/disk" + "github.com/shirou/gopsutil/load" + "github.com/shirou/gopsutil/mem" + "github.com/shirou/gopsutil/net" + "math" + "net/http" + "runtime" + "time" +) + +var ( + tData = list.New() +) + +type timeData struct { + now time.Time + cpuData float64 + load1Data float64 + load5Data float64 + load15Data float64 + swapData float64 + virtualData float64 + bandwidthRecvData float64 + bandwidthSendData float64 + tcpConnNumData float64 + udpConnNumData float64 + diskData float64 +} + +type dataAddr []float64 + +func status(c *gin.Context) { + timeArr := make([]string, 0) + dataMap := make(map[string][]float64, 0) + dataMap["cpu"] = make([]float64, 0) + dataMap["load1"] = make([]float64, 0) + dataMap["load5"] = make([]float64, 0) + dataMap["load15"] = make([]float64, 0) + dataMap["swap"] = make([]float64, 0) + dataMap["virtual"] = make([]float64, 0) + dataMap["bandwidthRecvData"] = make([]float64, 0) + dataMap["bandwidthSendData"] = make([]float64, 0) + dataMap["tcpConnNumData"] = make([]float64, 0) + dataMap["udpConnNumData"] = make([]float64, 0) + dataMap["disk"] = make([]float64, 0) + now := tData.Front() + for { + if now == nil { + break + } + data := now.Value.(*timeData) + timeArr = append(timeArr, data.now.Format("01-02 15:04")) + dataMap["cpu"] = append(dataMap["cpu"], data.cpuData) + dataMap["load1"] = append(dataMap["load1"], data.load15Data) + dataMap["load5"] = append(dataMap["load5"], data.load5Data) + dataMap["load15"] = append(dataMap["load15"], data.load15Data) + dataMap["swap"] = append(dataMap["swap"], data.swapData) + dataMap["virtual"] = append(dataMap["virtual"], data.virtualData) + dataMap["bandwidthSend"] = append(dataMap["bandwidthSend"], data.bandwidthRecvData) + dataMap["bandwidthRecv"] = append(dataMap["bandwidthRecv"], data.bandwidthSendData) + dataMap["tcp"] = append(dataMap["v"], data.tcpConnNumData) + dataMap["udp"] = append(dataMap["udp"], data.udpConnNumData) + dataMap["disk"] = append(dataMap["disk"], data.diskData) + now = now.Next() + } + + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "message": "ok", + "result": gin.H{ + "time": timeArr, + "data": dataMap, + }, + }) +} + +func storeSystemStatus() { + path := "/" + if runtime.GOOS == "windows" { + path = "C:" + } + + for range time.NewTicker(time.Second).C { + td := &timeData{now: time.Now()} + checkListLen(tData) + cpuPercent, err := cpu.Percent(0, true) + if err == nil { + var cpuAll float64 + for _, v := range cpuPercent { + cpuAll += v + } + td.cpuData = float64(len(cpuPercent)) + } + + loads, err := load.Avg() + if err == nil { + td.load1Data = loads.Load1 + td.load1Data = loads.Load5 + td.load15Data = loads.Load15 + } + + swap, err := mem.SwapMemory() + if err == nil { + td.swapData = math.Round(swap.UsedPercent) + } + vir, err := mem.VirtualMemory() + if err == nil { + td.virtualData = math.Round(vir.UsedPercent) + } + io1, err := net.IOCounters(false) + if err == nil { + time.Sleep(time.Millisecond * 500) + io2, err := net.IOCounters(false) + if err == nil && len(io2) > 0 && len(io1) > 0 { + td.bandwidthRecvData = float64((io2[0].BytesRecv-io1[0].BytesRecv)*2) / 1024 / 1024 + td.bandwidthSendData = float64((io2[0].BytesSent-io1[0].BytesSent)*2) / 1024 / 1024 + } + } + conn, err := net.ProtoCounters(nil) + if err == nil { + for _, v := range conn { + if v.Protocol == "tcp" { + td.tcpConnNumData = float64(v.Stats["CurrEstab"]) + } + if v.Protocol == "udp" { + td.udpConnNumData = float64(v.Stats["CurrEstab"]) + } + } + } + usage, err := disk.Usage(path) + if err == nil { + td.diskData = math.Round(usage.UsedPercent) + } + tData.PushBack(td) + } +} + +func checkListLen(lists ...*list.List) { + for _, l := range lists { + if l.Len() > 4320 { + if first := l.Front(); first != nil { + l.Remove(first) + } + } + } +} diff --git a/component/service/service.go b/component/service/service.go new file mode 100644 index 0000000..92a603d --- /dev/null +++ b/component/service/service.go @@ -0,0 +1,11 @@ +package service + +import "net" + +type HttpService struct { + ln net.Listener +} + +func NewHttpService(ln net.Listener) *HttpService { + return &HttpService{ln: ln} +} diff --git a/core/action/action.go b/core/action/action.go new file mode 100644 index 0000000..1931162 --- /dev/null +++ b/core/action/action.go @@ -0,0 +1,110 @@ +package action + +import ( + "ehang.io/nps/lib/common" + "ehang.io/nps/lib/logger" + "ehang.io/nps/lib/pool" + "errors" + "go.uber.org/zap" + "net" + "sync" +) + +var bp = pool.NewBufferPool(MaxReadSize) + +const MaxReadSize = 32 * 1024 + +var ( + _ Action = (*AdminAction)(nil) + _ Action = (*BridgeAction)(nil) + _ Action = (*LocalAction)(nil) + _ Action = (*NpcAction)(nil) +) + +type Action interface { + GetName() string + GetZhName() string + Init() error + RunConnWithAddr(net.Conn, string) error + RunConn(net.Conn) error + GetServeConnWithAddr(string) (net.Conn, error) + GetServerConn() (net.Conn, error) + CanServe() bool + RunPacketConn(conn net.PacketConn) error +} + +type DefaultAction struct { +} + +func (ba *DefaultAction) GetName() string { + return "default" +} + +func (ba *DefaultAction) GetZhName() string { + return "默认" +} + +func (ba *DefaultAction) Init() error { + return nil +} + +func (ba *DefaultAction) RunConn(clientConn net.Conn) error { + return errors.New("not supported") +} + +func (ba *DefaultAction) CanServe() bool { + return false +} + +func (ba *DefaultAction) RunPacketConn(conn net.PacketConn) error { + return errors.New("not supported") +} + +func (ba *DefaultAction) GetServerConn() (net.Conn, error) { + return nil, errors.New("can not get component connection") +} + +func (ba *DefaultAction) GetServeConnWithAddr(addr string) (net.Conn, error) { + return nil, errors.New("can not get component connection") +} + +func (ba *DefaultAction) startCopy(c1 net.Conn, c2 net.Conn) { + var wg sync.WaitGroup + wg.Add(2) + err := pool.CopyConnGoroutinePool.Invoke(&pool.CopyConnGpParams{ + Reader: c2, + Writer: c1, + Wg: &wg, + }) + if err != nil { + logger.Error("Invoke goroutine failed", zap.Error(err)) + return + } + buf := bp.Get() + _, _ = common.CopyBuffer(c2, c1, buf) + bp.Put(buf) + if v, ok := c1.(*net.TCPConn); ok { + _ = v.CloseRead() + } + if v, ok := c2.(*net.TCPConn); ok { + _ = v.CloseWrite() + } + wg.Wait() +} + +func (ba *DefaultAction) startCopyPacketConn(p1 net.PacketConn, p2 net.PacketConn) error { + var wg sync.WaitGroup + wg.Add(2) + _ = pool.CopyPacketGoroutinePool.Invoke(&pool.CopyPacketGpParams{ + RPacket: p1, + WPacket: p2, + Wg: &wg, + }) + _ = pool.CopyPacketGoroutinePool.Invoke(&pool.CopyPacketGpParams{ + RPacket: p2, + WPacket: p1, + Wg: &wg, + }) + wg.Wait() + return nil +} diff --git a/core/action/admin.go b/core/action/admin.go new file mode 100644 index 0000000..8235fdd --- /dev/null +++ b/core/action/admin.go @@ -0,0 +1,32 @@ +package action + +import ( + "ehang.io/nps/lib/enet" + "net" +) + +var adminListener = enet.NewListener() + +func GetAdminListener() net.Listener { + return adminListener +} + +type AdminAction struct { + DefaultAction +} + +func (la *AdminAction) GetName() string { + return "admin" +} + +func (la *AdminAction) GetZhName() string { + return "转发到控制台" +} + +func (la *AdminAction) RunConn(clientConn net.Conn) error { + return adminListener.SendConn(clientConn) +} + +func (la *AdminAction) RunConnWithAddr(clientConn net.Conn, addr string) error { + return adminListener.SendConn(clientConn) +} diff --git a/core/action/admin_test.go b/core/action/admin_test.go new file mode 100644 index 0000000..6cc2382 --- /dev/null +++ b/core/action/admin_test.go @@ -0,0 +1,29 @@ +package action + +import ( + "github.com/stretchr/testify/assert" + "net" + "testing" +) + +func TestAdminRunConn(t *testing.T) { + ac := &AdminAction{ + DefaultAction: DefaultAction{}, + } + finish := make(chan struct{}, 0) + go func() { + _, err := GetAdminListener().Accept() + assert.NoError(t, err) + finish <- struct{}{} + }() + ln, err := net.Listen("tcp", "127.0.0.1:0") + assert.NoError(t, err) + go func() { + conn, err := ln.Accept() + assert.NoError(t, err) + assert.NoError(t, ac.RunConn(conn)) + }() + _, err = net.Dial("tcp", ln.Addr().String()) + assert.NoError(t, err) + <-finish +} diff --git a/core/action/bridge.go b/core/action/bridge.go new file mode 100644 index 0000000..b03b480 --- /dev/null +++ b/core/action/bridge.go @@ -0,0 +1,61 @@ +package action + +import ( + "ehang.io/nps/lib/enet" + "ehang.io/nps/lib/pool" + "net" +) + +var bridgeListener = enet.NewListener() +var bridgePacketConn enet.PacketConn +var packetBp = pool.NewBufferPool(1500) + +func GetBridgeListener() net.Listener { + return bridgeListener +} + +func GetBridgePacketConn() net.PacketConn { + return bridgePacketConn +} + +type BridgeAction struct { + DefaultAction + WritePacketConn net.PacketConn `json:"-"` +} + +func (ba *BridgeAction) GetName() string { + return "bridge" +} + +func (ba *BridgeAction) GetZhName() string { + return "转发到网桥" +} + +func (ba *BridgeAction) Init() error { + bridgePacketConn = enet.NewReaderPacketConn(ba.WritePacketConn, nil, ba.WritePacketConn.LocalAddr()) + return nil +} + +func (ba *BridgeAction) RunConn(clientConn net.Conn) error { + return bridgeListener.SendConn(clientConn) +} + +func (ba *BridgeAction) RunConnWithAddr(clientConn net.Conn, addr string) error { + return bridgeListener.SendConn(clientConn) +} + +func (ba *BridgeAction) RunPacketConn(pc net.PacketConn) error { + b := packetBp.Get() + defer packetBp.Put(b) + for { + n, addr, err := pc.ReadFrom(b) + if err != nil { + break + } + err = bridgePacketConn.SendPacket(b[:n], addr) + if err != nil { + break + } + } + return nil +} diff --git a/core/action/bridge_test.go b/core/action/bridge_test.go new file mode 100644 index 0000000..20a87b9 --- /dev/null +++ b/core/action/bridge_test.go @@ -0,0 +1,73 @@ +package action + +import ( + "ehang.io/nps/lib/enet" + "github.com/stretchr/testify/assert" + "net" + "testing" +) + +func TestBridgeRunConn(t *testing.T) { + packetConn, err := net.ListenPacket("udp", "127.0.0.1:0") + assert.NoError(t, err) + ac := &BridgeAction{ + DefaultAction: DefaultAction{}, + WritePacketConn: packetConn, + } + finish := make(chan struct{}, 0) + go func() { + _, err := GetBridgeListener().Accept() + assert.NoError(t, err) + finish <- struct{}{} + }() + ln, err := net.Listen("tcp", "127.0.0.1:0") + assert.NoError(t, err) + go func() { + conn, err := ln.Accept() + assert.NoError(t, err) + assert.NoError(t, ac.RunConn(conn)) + }() + _, err = net.Dial("tcp", ln.Addr().String()) + assert.NoError(t, err) + <-finish +} + +func TestBridgeRunPacket(t *testing.T) { + packetConn, err := net.ListenPacket("udp", "127.0.0.1:0") + assert.NoError(t, err) + ac := &BridgeAction{ + DefaultAction: DefaultAction{}, + WritePacketConn: packetConn, + } + assert.NoError(t, ac.Init()) + go func() { + p := make([]byte, 1024) + pc := GetBridgePacketConn() + n, addr, err := pc.ReadFrom(p) + assert.NoError(t, err) + _, err = pc.WriteTo(p[:n], addr) + assert.NoError(t, err) + }() + go func() { + p := make([]byte, 1024) + n, addr, err := packetConn.ReadFrom(p) + assert.NoError(t, err) + bPacketConn := enet.NewReaderPacketConn(packetConn, p[:n], addr) + go func() { + err = ac.RunPacketConn(bPacketConn) + assert.NoError(t, err) + }() + err = bPacketConn.SendPacket(p[:n], addr) + assert.NoError(t, err) + }() + cPacketConn, err := net.ListenPacket("udp", "127.0.0.1:0") + assert.NoError(t, err) + b := []byte("12345") + _, err = cPacketConn.WriteTo(b, packetConn.LocalAddr()) + assert.NoError(t, err) + p := make([]byte, 1024) + n, addr, err := cPacketConn.ReadFrom(p) + assert.NoError(t, err) + assert.Equal(t, addr.String(), packetConn.LocalAddr().String()) + assert.Equal(t, p[:n], b) +} diff --git a/core/action/local.go b/core/action/local.go new file mode 100644 index 0000000..3150652 --- /dev/null +++ b/core/action/local.go @@ -0,0 +1,77 @@ +package action + +import ( + "ehang.io/nps/lib/lb" + "net" +) + +type LocalAction struct { + DefaultAction + TargetAddr []string `json:"target_addr" placeholder:"1.1.1.1:80\n1.1.1.2:80" zh_name:"目标地址"` + UnixSocket bool `json:"unix_sock" placeholder:"" zh_name:"转发到unix socket"` + networkTcp string + localLb lb.Algo +} + +func (la *LocalAction) GetName() string { + return "local" +} + +func (la *LocalAction) GetZhName() string { + return "转发到本地" +} + +func (la *LocalAction) Init() error { + la.localLb = lb.GetLbAlgo("roundRobin") + for _, v := range la.TargetAddr { + _ = la.localLb.Append(v) + } + la.networkTcp = "tcp" + if la.UnixSocket { + // just support unix + la.networkTcp = "unix" + } + return nil +} + +func (la *LocalAction) RunConn(clientConn net.Conn) error { + serverConn, err := la.GetServerConn() + if err != nil { + return err + } + la.startCopy(clientConn, serverConn) + return nil +} + +func (la *LocalAction) RunConnWithAddr(clientConn net.Conn, addr string) error { + serverConn, err := la.GetServeConnWithAddr(addr) + if err != nil { + return err + } + la.startCopy(clientConn, serverConn) + return nil +} + +func (la *LocalAction) CanServe() bool { + return true +} + +func (la *LocalAction) GetServerConn() (net.Conn, error) { + addr, err := la.localLb.Next() + if err != nil { + return nil, err + } + return la.GetServeConnWithAddr(addr.(string)) +} + +func (la *LocalAction) GetServeConnWithAddr(addr string) (net.Conn, error) { + return net.Dial(la.networkTcp, addr) +} + +func (la *LocalAction) RunPacketConn(pc net.PacketConn) error { + localPacketConn, err := net.ListenPacket("udp", "127.0.0.1:0") + if err != nil { + return err + } + return la.startCopyPacketConn(pc, localPacketConn) +} diff --git a/core/action/local_test.go b/core/action/local_test.go new file mode 100644 index 0000000..5e1133f --- /dev/null +++ b/core/action/local_test.go @@ -0,0 +1 @@ +package action diff --git a/core/action/npc.go b/core/action/npc.go new file mode 100644 index 0000000..c709705 --- /dev/null +++ b/core/action/npc.go @@ -0,0 +1,84 @@ +package action + +import ( + "crypto/tls" + "ehang.io/nps/lib/cert" + "ehang.io/nps/lib/enet" + "ehang.io/nps/lib/pb" + "github.com/golang/protobuf/proto" + "github.com/pkg/errors" + "net" +) + +type NpcAction struct { + NpcId string `json:"npc_id" required:"true" placeholder:"npc id" zh_name:"客户端"` + BridgeAddr string `json:"bridge_addr" placeholder:"127.0.0.1:8080" zh_name:"网桥地址"` + UnixSocket bool `json:"unix_sock" placeholder:"" zh_name:"转发到unix socket"` + networkTcp pb.ConnType + tlsConfig *tls.Config + connRequest *pb.ConnRequest + DefaultAction +} + +func (na *NpcAction) GetName() string { + return "npc" +} + +func (na *NpcAction) GetZhName() string { + return "转发到客户端" +} + +func (na *NpcAction) Init() error { + if na.tlsConfig == nil { + return errors.New("tls config is nil") + } + sn, err := cert.GetCertSnFromConfig(na.tlsConfig) + if err != nil { + return errors.Wrap(err, "get serial number") + } + na.connRequest = &pb.ConnRequest{Id: sn} + na.networkTcp = pb.ConnType_tcp + if na.UnixSocket { + // just support unix + na.networkTcp = pb.ConnType_unix + } + return nil +} + +func (na *NpcAction) RunConnWithAddr(clientConn net.Conn, addr string) error { + serverConn, err := na.GetServeConnWithAddr(addr) + if err != nil { + return err + } + na.startCopy(clientConn, serverConn) + return nil +} + +func (na *NpcAction) CanServe() bool { + return true +} + +func (na *NpcAction) GetServeConnWithAddr(addr string) (net.Conn, error) { + return dialBridge(na, na.networkTcp, addr) +} + +func (na *NpcAction) RunPacketConn(pc net.PacketConn) error { + serverPacketConn, err := dialBridge(na, pb.ConnType_udp, "") + if err != nil { + return err + } + return na.startCopyPacketConn(pc, enet.NewTcpPacketConn(serverPacketConn)) +} + +func dialBridge(npc *NpcAction, connType pb.ConnType, addr string) (net.Conn, error) { + tlsConn, err := tls.Dial("tcp", npc.BridgeAddr, npc.tlsConfig) + if err != nil { + return nil, errors.Wrap(err, "dial bridge tls") + } + cr := proto.Clone(npc.connRequest).(*pb.ConnRequest) + cr.ConnType = &pb.ConnRequest_AppInfo{AppInfo: &pb.AppInfo{ConnType: connType, AppAddr: addr, NpcId: npc.NpcId}} + if _, err = pb.WriteMessage(tlsConn, cr); err != nil { + return nil, errors.Wrap(err, "write enet request") + } + return tlsConn, err +} diff --git a/core/action/npc_test.go b/core/action/npc_test.go new file mode 100644 index 0000000..5e1133f --- /dev/null +++ b/core/action/npc_test.go @@ -0,0 +1 @@ +package action diff --git a/core/handler/default.go b/core/handler/default.go new file mode 100644 index 0000000..8283cc9 --- /dev/null +++ b/core/handler/default.go @@ -0,0 +1,71 @@ +package handler + +import ( + "ehang.io/nps/lib/enet" +) + +var ( + _ Handler = (*HttpHandler)(nil) + _ Handler = (*HttpsHandler)(nil) + _ Handler = (*RdpHandler)(nil) + _ Handler = (*RedisHandler)(nil) + _ Handler = (*Socks5Handler)(nil) + _ Handler = (*TransparentHandler)(nil) + _ Handler = (*DefaultHandler)(nil) + _ Handler = (*DnsHandler)(nil) + _ Handler = (*P2PHandler)(nil) + _ Handler = (*QUICHandler)(nil) + _ Handler = (*DefaultHandler)(nil) + _ Handler = (*Socks5UdpHandler)(nil) +) + +type RuleRun interface { + RunConn(enet.Conn) (bool, error) + RunPacketConn(enet.PacketConn) (bool, error) +} + +type DefaultHandler struct { + ruleList []RuleRun +} + +func NewBaseTcpHandler() *DefaultHandler { + return &DefaultHandler{ruleList: make([]RuleRun, 0)} +} + +func (b *DefaultHandler) GetName() string { + return "default" +} + +func (b *DefaultHandler) GetZhName() string { + return "默认" +} + +func (b *DefaultHandler) HandleConn(_ []byte, c enet.Conn) (bool, error) { + return b.processConn(c) +} + +func (b *DefaultHandler) AddRule(r RuleRun) { + b.ruleList = append(b.ruleList, r) +} + +func (b *DefaultHandler) HandlePacketConn(_ enet.PacketConn) (bool, error) { + return false, nil +} + +func (b *DefaultHandler) processConn(c enet.Conn) (bool, error) { + for _, r := range b.ruleList { + if ok, err := r.RunConn(c); err != nil || ok { + return ok, err + } + } + return false, nil +} + +func (b *DefaultHandler) processPacketConn(pc enet.PacketConn) (bool, error) { + for _, r := range b.ruleList { + if ok, err := r.RunPacketConn(pc); err != nil || ok { + return ok, err + } + } + return false, nil +} diff --git a/core/handler/dns.go b/core/handler/dns.go new file mode 100644 index 0000000..8d97cb4 --- /dev/null +++ b/core/handler/dns.go @@ -0,0 +1,35 @@ +package handler + +import ( + "ehang.io/nps/lib/enet" + "ehang.io/nps/lib/logger" + "github.com/miekg/dns" + "go.uber.org/zap" +) + +type DnsHandler struct { + DefaultHandler +} + +func (dh *DnsHandler) GetName() string { + return "dns" +} + +func (dh *DnsHandler) GetZhName() string { + return "dns协议" +} + +func (dh *DnsHandler) HandlePacketConn(pc enet.PacketConn) (bool, error) { + b, _, err := pc.FirstPacket() + if err != nil { + logger.Warn("firstPacket error", zap.Error(err)) + return false, nil + } + m := new(dns.Msg) + err = m.Unpack(b) + if err != nil { + logger.Debug("parse dns request error", zap.Error(err)) + return false, nil + } + return dh.processPacketConn(pc) +} diff --git a/core/handler/dns_test.go b/core/handler/dns_test.go new file mode 100644 index 0000000..2430891 --- /dev/null +++ b/core/handler/dns_test.go @@ -0,0 +1,46 @@ +package handler + +import ( + "ehang.io/nps/lib/enet" + "github.com/miekg/dns" + "github.com/stretchr/testify/assert" + "net" + "testing" +) + +type testRule struct { + run bool +} + +func (t *testRule) RunConn(c enet.Conn) (bool, error) { + t.run = true + return true, nil +} + +func (t *testRule) RunPacketConn(_ enet.PacketConn) (bool, error) { + t.run = true + return true, nil +} + +func TestHandleDnsPacket(t *testing.T) { + lPacketConn, err := net.ListenPacket("udp", "127.0.0.1:0") + assert.NoError(t, err) + h := DnsHandler{} + rule := &testRule{} + h.AddRule(rule) + + m := new(dns.Msg) + m.SetQuestion(dns.Fqdn("www.google.com"), dns.TypeA) + m.RecursionDesired = true + + b, err := m.Pack() + assert.NoError(t, err) + pc := enet.NewReaderPacketConn(nil, b, lPacketConn.LocalAddr()) + + assert.NoError(t, pc.SendPacket(b, nil)) + res, err := h.HandlePacketConn(pc) + + assert.NoError(t, err) + assert.Equal(t, true, res) + assert.Equal(t, true, rule.run) +} diff --git a/core/handler/handler.go b/core/handler/handler.go new file mode 100644 index 0000000..52dbeeb --- /dev/null +++ b/core/handler/handler.go @@ -0,0 +1,11 @@ +package handler + +import "ehang.io/nps/lib/enet" + +type Handler interface { + GetName() string + GetZhName() string + AddRule(RuleRun) + HandleConn([]byte, enet.Conn) (bool, error) + HandlePacketConn(enet.PacketConn) (bool, error) +} diff --git a/core/handler/http.go b/core/handler/http.go new file mode 100644 index 0000000..ef49603 --- /dev/null +++ b/core/handler/http.go @@ -0,0 +1,30 @@ +package handler + +import ( + "ehang.io/nps/lib/enet" + "net/http" +) + +type HttpHandler struct { + DefaultHandler +} + +func NewHttpHandler() *HttpHandler { + return &HttpHandler{} +} + +func (h *HttpHandler) GetName() string { + return "http" +} + +func (h *HttpHandler) GetZhName() string { + return "http协议" +} + +func (h *HttpHandler) HandleConn(b []byte, c enet.Conn) (bool, error) { + switch string(b[:3]) { + case http.MethodGet[:3], http.MethodHead[:3], http.MethodPost[:3], http.MethodPut[:3], http.MethodPatch[:3], http.MethodDelete[:3], http.MethodConnect[:3], http.MethodOptions[:3], http.MethodTrace[:3]: + return h.processConn(c) + } + return false, nil +} diff --git a/core/handler/http_test.go b/core/handler/http_test.go new file mode 100644 index 0000000..aaf81ad --- /dev/null +++ b/core/handler/http_test.go @@ -0,0 +1,27 @@ +package handler + +import ( + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httputil" + "testing" +) + +func TestHandleHttpConn(t *testing.T) { + + h := HttpHandler{} + rule := &testRule{} + h.AddRule(rule) + + r, err := http.NewRequest("GET", "/", nil) + assert.NoError(t, err) + + b, err := httputil.DumpRequest(r, false) + assert.NoError(t, err) + + res, err := h.HandleConn(b, nil) + + assert.NoError(t, err) + assert.Equal(t, true, res) + assert.Equal(t, true, rule.run) +} diff --git a/core/handler/https.go b/core/handler/https.go new file mode 100644 index 0000000..ce8f025 --- /dev/null +++ b/core/handler/https.go @@ -0,0 +1,33 @@ +package handler + +import ( + "ehang.io/nps/lib/enet" +) + +const ( + recordTypeHandshake uint8 = 22 + typeClientHello uint8 = 1 +) + +type HttpsHandler struct { + DefaultHandler +} + +func NewHttpsHandler() *HttpsHandler { + return &HttpsHandler{} +} + +func (h *HttpsHandler) GetName() string { + return "https" +} + +func (h *HttpsHandler) GetZhName() string { + return "https协议" +} + +func (h *HttpsHandler) HandleConn(b []byte, c enet.Conn) (bool, error) { + if b[0] == recordTypeHandshake{ + return h.processConn(c) + } + return false, nil +} diff --git a/core/handler/https_test.go b/core/handler/https_test.go new file mode 100644 index 0000000..922b88f --- /dev/null +++ b/core/handler/https_test.go @@ -0,0 +1,39 @@ +package handler + +import ( + "crypto/tls" + "ehang.io/nps/lib/enet" + "github.com/stretchr/testify/assert" + "net" + "testing" +) + +func TestHandleHttpsConn(t *testing.T) { + h := HttpsHandler{} + rule := &testRule{} + h.AddRule(rule) + + finish := make(chan struct{}, 0) + ln, err := net.Listen("tcp", "127.0.0.1:0") + assert.NoError(t, err) + go func() { + conn, err := ln.Accept() + assert.NoError(t, err) + buf := make([]byte, 1024) + n, err := conn.Read(buf) + assert.NoError(t, err) + res, err := h.HandleConn(buf[:n], enet.NewReaderConn(conn)) + assert.NoError(t, err) + assert.Equal(t, true, res) + assert.Equal(t, true, rule.run) + finish <- struct{}{} + }() + + go func() { + _, err = tls.Dial("tcp", ln.Addr().String(), &tls.Config{ + InsecureSkipVerify: true, + }) + assert.NoError(t, err) + }() + <-finish +} diff --git a/core/handler/p2p.go b/core/handler/p2p.go new file mode 100644 index 0000000..4168d14 --- /dev/null +++ b/core/handler/p2p.go @@ -0,0 +1,32 @@ +package handler + +import ( + "bytes" + "ehang.io/nps/lib/enet" + "ehang.io/nps/lib/logger" + "go.uber.org/zap" +) + +type P2PHandler struct { + DefaultHandler +} + +func (ph *P2PHandler) GetName() string { + return "p2p" +} + +func (ph *P2PHandler) GetZhName() string { + return "点对点协议" +} + +func (ph *P2PHandler) HandlePacketConn(pc enet.PacketConn) (bool, error) { + b, _, err := pc.FirstPacket() + if err != nil { + logger.Warn("firstPacket error", zap.Error(err)) + return false, nil + } + if bytes.HasPrefix(b, []byte("p2p")) { + return ph.processPacketConn(pc) + } + return false, nil +} diff --git a/core/handler/p2p_test.go b/core/handler/p2p_test.go new file mode 100644 index 0000000..ee1f4ec --- /dev/null +++ b/core/handler/p2p_test.go @@ -0,0 +1,26 @@ +package handler + +import ( + "ehang.io/nps/lib/enet" + "github.com/stretchr/testify/assert" + "net" + "testing" +) + +func TestHandleP2PPacket(t *testing.T) { + + h := P2PHandler{} + rule := &testRule{} + h.AddRule(rule) + addr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8080") + assert.NoError(t, err) + pc := enet.NewReaderPacketConn(nil, []byte("p2p xxxx"), addr) + + assert.NoError(t, pc.SendPacket([]byte("p2p xxxx"), nil)) + + res, err := h.HandlePacketConn(pc) + + assert.NoError(t, err) + assert.Equal(t, true, res) + assert.Equal(t, true, rule.run) +} diff --git a/core/handler/quic.go b/core/handler/quic.go new file mode 100644 index 0000000..83edb2a --- /dev/null +++ b/core/handler/quic.go @@ -0,0 +1,32 @@ +package handler + +import ( + "bytes" + "ehang.io/nps/lib/enet" + "ehang.io/nps/lib/logger" + "go.uber.org/zap" +) + +type QUICHandler struct { + DefaultHandler +} + +func (qh *QUICHandler) GetName() string { + return "quic" +} + +func (qh *QUICHandler) GetZhName() string { + return "quic协议" +} + +func (qh *QUICHandler) HandlePacketConn(pc enet.PacketConn) (bool, error) { + b, _, err := pc.FirstPacket() + if err != nil { + logger.Warn("firstPacket error", zap.Error(err)) + return false, nil + } + if len(b) >= 5 && bytes.HasPrefix(b[1:5], []byte{0, 0, 0, 1}) { + return qh.processPacketConn(pc) + } + return false, nil +} diff --git a/core/handler/quic_test.go b/core/handler/quic_test.go new file mode 100644 index 0000000..5113c54 --- /dev/null +++ b/core/handler/quic_test.go @@ -0,0 +1,35 @@ +package handler + +import ( + "crypto/tls" + "ehang.io/nps/lib/enet" + "github.com/lucas-clemente/quic-go" + "github.com/stretchr/testify/assert" + "net" + "testing" +) + +func TestHandleQUICPacket(t *testing.T) { + h := QUICHandler{} + rule := &testRule{} + h.AddRule(rule) + finish := make(chan struct{}, 0) + packetConn, err := net.ListenPacket("udp", "127.0.0.1:0") + assert.NoError(t, err) + + go func() { + b := make([]byte, 1500) + n, addr, err := packetConn.ReadFrom(b) + assert.NoError(t, err) + pc := enet.NewReaderPacketConn(nil, b[:n], packetConn.LocalAddr()) + assert.NoError(t, pc.SendPacket(b[:n], addr)) + res, err := h.HandlePacketConn(pc) + + assert.NoError(t, err) + assert.Equal(t, true, res) + assert.Equal(t, true, rule.run) + finish <- struct{}{} + }() + go quic.DialAddr(packetConn.LocalAddr().String(), &tls.Config{}, nil) + <-finish +} diff --git a/core/handler/rdp.go b/core/handler/rdp.go new file mode 100644 index 0000000..79d5ac6 --- /dev/null +++ b/core/handler/rdp.go @@ -0,0 +1,24 @@ +package handler + +import ( + "ehang.io/nps/lib/enet" +) + +type RdpHandler struct { + DefaultHandler +} + +func (rh *RdpHandler) GetName() string { + return "rdp" +} + +func (rh *RdpHandler) GetZhName() string { + return "rdp协议" +} + +func (rh *RdpHandler) HandleConn(b []byte, c enet.Conn) (bool, error) { + if b[0] == 3 && b[1] == 0 { + return rh.processConn(c) + } + return false, nil +} diff --git a/core/handler/rdp_test.go b/core/handler/rdp_test.go new file mode 100644 index 0000000..cbfbc58 --- /dev/null +++ b/core/handler/rdp_test.go @@ -0,0 +1,37 @@ +package handler + +import ( + "ehang.io/nps/lib/enet" + "github.com/icodeface/grdp" + "github.com/icodeface/grdp/glog" + "github.com/stretchr/testify/assert" + "net" + "testing" +) + +func TestHandleRdpConn(t *testing.T) { + h := RdpHandler{} + rule := &testRule{} + h.AddRule(rule) + + finish := make(chan struct{}, 0) + ln, err := net.Listen("tcp", "127.0.0.1:0") + assert.NoError(t, err) + go func() { + conn, err := ln.Accept() + assert.NoError(t, err) + buf := make([]byte, 1024) + n, err := conn.Read(buf) + assert.NoError(t, err) + res, err := h.HandleConn(buf[:n], enet.NewReaderConn(conn)) + assert.NoError(t, err) + assert.Equal(t, true, res) + assert.Equal(t, true, rule.run) + finish <- struct{}{} + }() + + go func() { + grdp.NewClient(ln.Addr().String(), glog.DEBUG).Login("Administrator", "123456") + }() + <-finish +} diff --git a/core/handler/redis.go b/core/handler/redis.go new file mode 100644 index 0000000..332ef2e --- /dev/null +++ b/core/handler/redis.go @@ -0,0 +1,22 @@ +package handler + +import "ehang.io/nps/lib/enet" + +type RedisHandler struct { + DefaultHandler +} + +func (rds *RedisHandler) GetName() string { + return "redis" +} + +func (rds *RedisHandler) GetZhName() string { + return "redis协议" +} + +func (rds *RedisHandler) HandleConn(b []byte, c enet.Conn) (bool, error) { + if b[0] == 42 && b[1] == 49 && b[2] == 13 { + return rds.processConn(c) + } + return false, nil +} diff --git a/core/handler/redis_test.go b/core/handler/redis_test.go new file mode 100644 index 0000000..fae24eb --- /dev/null +++ b/core/handler/redis_test.go @@ -0,0 +1,40 @@ +package handler + +import ( + "context" + "ehang.io/nps/lib/enet" + "github.com/go-redis/redis/v8" + "github.com/stretchr/testify/assert" + "net" + "testing" +) + +func TestHandleRedisConn(t *testing.T) { + h := RedisHandler{} + rule := &testRule{} + h.AddRule(rule) + + finish := make(chan struct{}, 0) + ln, err := net.Listen("tcp", "127.0.0.1:0") + assert.NoError(t, err) + go func() { + conn, err := ln.Accept() + assert.NoError(t, err) + buf := make([]byte, 1024) + n, err := conn.Read(buf) + assert.NoError(t, err) + res, err := h.HandleConn(buf[:n], enet.NewReaderConn(conn)) + assert.NoError(t, err) + assert.Equal(t, true, res) + assert.Equal(t, true, rule.run) + finish <- struct{}{} + }() + + go func() { + rdb := redis.NewClient(&redis.Options{ + Addr: ln.Addr().String(), + }) + rdb.Ping(context.Background()) + }() + <-finish +} diff --git a/core/handler/socks5.go b/core/handler/socks5.go new file mode 100644 index 0000000..3c3431d --- /dev/null +++ b/core/handler/socks5.go @@ -0,0 +1,22 @@ +package handler + +import "ehang.io/nps/lib/enet" + +type Socks5Handler struct { + DefaultHandler +} + +func (sh *Socks5Handler) GetName() string { + return "socks5" +} + +func (sh *Socks5Handler) GetZhName() string { + return "socks5协议" +} + +func (sh *Socks5Handler) HandleConn(b []byte, c enet.Conn) (bool, error) { + if b[0] == 5 { + return sh.processConn(c) + } + return false, nil +} diff --git a/core/handler/socks5_test.go b/core/handler/socks5_test.go new file mode 100644 index 0000000..d991c41 --- /dev/null +++ b/core/handler/socks5_test.go @@ -0,0 +1,47 @@ +package handler + +import ( + "crypto/tls" + "ehang.io/nps/lib/enet" + "fmt" + "github.com/stretchr/testify/assert" + "net" + "net/http" + "net/url" + "testing" +) + +func TestHandleSocks5Conn(t *testing.T) { + h := Socks5Handler{} + rule := &testRule{} + h.AddRule(rule) + + finish := make(chan struct{}, 0) + ln, err := net.Listen("tcp", "127.0.0.1:0") + assert.NoError(t, err) + go func() { + conn, err := ln.Accept() + assert.NoError(t, err) + buf := make([]byte, 1024) + n, err := conn.Read(buf) + assert.NoError(t, err) + res, err := h.HandleConn(buf[:n], enet.NewReaderConn(conn)) + assert.NoError(t, err) + assert.Equal(t, true, res) + assert.Equal(t, true, rule.run) + finish <- struct{}{} + }() + + go func() { + 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} + _, _ = client.Get("https://google.com/") + }() + <-finish +} diff --git a/core/handler/socks5_udp.go b/core/handler/socks5_udp.go new file mode 100644 index 0000000..9fc0fbb --- /dev/null +++ b/core/handler/socks5_udp.go @@ -0,0 +1,26 @@ +package handler + +import "ehang.io/nps/lib/enet" + +type Socks5UdpHandler struct { + DefaultHandler +} + +func (sh *Socks5UdpHandler) GetName() string { + return "socks5_udp" +} + +func (sh *Socks5UdpHandler) GetZhName() string { + return "socks5 udp协议" +} + +func (sh *Socks5UdpHandler) HandlePacketConn(pc enet.PacketConn) (bool, error) { + b, _, err := pc.FirstPacket() + if err != nil { + return true, err + } + if b[0] == 0 { + return sh.processPacketConn(pc) + } + return false, nil +} diff --git a/core/handler/socks5_udp_test.go b/core/handler/socks5_udp_test.go new file mode 100644 index 0000000..8871d50 --- /dev/null +++ b/core/handler/socks5_udp_test.go @@ -0,0 +1,44 @@ +package handler + +import ( + "ehang.io/nps/lib/common" + "ehang.io/nps/lib/enet" + "github.com/stretchr/testify/assert" + "net" + "testing" +) + +func TestSocks5Handle(t *testing.T) { + h := Socks5UdpHandler{} + rule := &testRule{} + h.AddRule(rule) + + finish := make(chan struct{}, 0) + pc, err := net.ListenPacket("udp", "127.0.0.1:0") + assert.NoError(t, err) + go func() { + buf := make([]byte, 1024) + n, addr, err := pc.ReadFrom(buf) + assert.NoError(t, err) + rPc := enet.NewReaderPacketConn(nil, buf[:n], addr) + res, err := h.HandlePacketConn(rPc) + + assert.NoError(t, err) + assert.Equal(t, true, res) + assert.Equal(t, true, rule.run) + finish <- struct{}{} + }() + + data := []byte("test") + go func() { + cPc, err := net.ListenPacket("udp", "127.0.0.1:0") + assert.NoError(t, err) + pAddr, err := common.ParseAddr("8.8.8.8:53") + assert.NoError(t, err) + b := append([]byte{0, 0, 0}, pAddr...) + b = append(b, data...) + _, err = cPc.WriteTo(b, pc.LocalAddr()) + assert.NoError(t, err) + }() + <-finish +} diff --git a/core/handler/transparent.go b/core/handler/transparent.go new file mode 100644 index 0000000..2e48228 --- /dev/null +++ b/core/handler/transparent.go @@ -0,0 +1,21 @@ +package handler + +import ( + "ehang.io/nps/lib/enet" +) + +type TransparentHandler struct { + DefaultHandler +} + +func (ts *TransparentHandler) GetName() string { + return "transparent" +} + +func (ts *TransparentHandler) GetZhName() string { + return "linux透明代理协议" +} + +func (ts *TransparentHandler) HandleConn(b []byte, c enet.Conn) (bool, error) { + return ts.processConn(c) +} diff --git a/core/limiter/conn_num.go b/core/limiter/conn_num.go new file mode 100644 index 0000000..d299e71 --- /dev/null +++ b/core/limiter/conn_num.go @@ -0,0 +1,43 @@ +package limiter + +import ( + "ehang.io/nps/lib/enet" + "errors" + "sync/atomic" +) + +// ConnNumLimiter is used to limit the connection num of a service +type ConnNumLimiter struct { + baseLimiter + nowNum int32 + MaxConnNum int32 `json:"max_conn_num" required:"true" placeholder:"10" zh_name:"最大连接数"` //0 means not limit +} + +func (cl *ConnNumLimiter) GetName() string { + return "conn_num" +} + +func (cl *ConnNumLimiter) GetZhName() string { + return "总连接数限制" +} + +// DoLimit return an error if the connection num exceed the maximum +func (cl *ConnNumLimiter) DoLimit(c enet.Conn) (enet.Conn, error) { + if atomic.AddInt32(&cl.nowNum, 1) > cl.MaxConnNum && cl.MaxConnNum > 0 { + atomic.AddInt32(&cl.nowNum, -1) + return nil, errors.New("exceed maximum number of connections") + } + return &connNumConn{nowNum: &cl.nowNum}, nil +} + +// connNumConn is an implementation of enet.Conn +type connNumConn struct { + nowNum *int32 + enet.Conn +} + +// Close decrease the connection num +func (cn *connNumConn) Close() error { + atomic.AddInt32(cn.nowNum, -1) + return cn.Conn.Close() +} diff --git a/core/limiter/conn_num_test.go b/core/limiter/conn_num_test.go new file mode 100644 index 0000000..044d8af --- /dev/null +++ b/core/limiter/conn_num_test.go @@ -0,0 +1,35 @@ +package limiter + +import ( + "ehang.io/nps/lib/enet" + "github.com/stretchr/testify/assert" + "net" + "testing" +) + +func TestConnNumLimiter(t *testing.T) { + cl := ConnNumLimiter{MaxConnNum: 5} + assert.NoError(t, cl.Init()) + ln, err := net.Listen("tcp", "127.0.0.1:0") + assert.NoError(t, err) + nowNum := 0 + close := make(chan struct{}) + go func() { + for { + c, err := ln.Accept() + assert.NoError(t, err) + nowNum++ + _, err = cl.DoLimit(enet.NewReaderConn(c)) + if nowNum > 5 { + assert.Error(t, err) + close <- struct{}{} + } else { + assert.NoError(t, err) + } + } + }() + for i := 6; i > 0; i-- { + go net.Dial("tcp", ln.Addr().String()) + } + <-close +} diff --git a/core/limiter/flow.go b/core/limiter/flow.go new file mode 100644 index 0000000..7681a86 --- /dev/null +++ b/core/limiter/flow.go @@ -0,0 +1,87 @@ +package limiter + +import ( + "ehang.io/nps/lib/enet" + "errors" + "sync/atomic" +) + +// FlowStore is an interface to store or get the flow now +type FlowStore interface { + GetOutIn() (uint32, uint32) + AddOut(out uint32) uint32 + AddIn(in uint32) uint32 +} + +// memStore is an implement of FlowStore +type memStore struct { + nowOut uint32 + nowIn uint32 +} + +// GetOutIn return out and in num 0 +func (m *memStore) GetOutIn() (uint32, uint32) { + return m.nowOut, m.nowIn +} + +// AddOut is used to add out now +func (m *memStore) AddOut(out uint32) uint32 { + return atomic.AddUint32(&m.nowOut, out) +} + +// AddIn is used to add in now +func (m *memStore) AddIn(in uint32) uint32 { + return atomic.AddUint32(&m.nowIn, in) +} + +// FlowLimiter is used to limit the flow of a service +type FlowLimiter struct { + Store FlowStore + OutLimit uint32 `json:"out_limit" required:"true" placeholder:"1024(kb)" zh_name:"出口最大流量"` //unit: kb, 0 means not limit + InLimit uint32 `json:"in_limit" required:"true" placeholder:"1024(kb)" zh_name:"入口最大流量"` //unit: kb, 0 means not limit +} + +func (f *FlowLimiter) GetName() string { + return "flow" +} + +func (f *FlowLimiter) GetZhName() string { + return "流量限制" +} + +// DoLimit return a flow limited enet.Conn +func (f *FlowLimiter) DoLimit(c enet.Conn) (enet.Conn, error) { + return &flowConn{fl: f, Conn: c}, nil +} + +// Init is used to set out or in num now +func (f *FlowLimiter) Init() error { + if f.Store == nil { + f.Store = &memStore{} + } + return nil +} + +// flowConn is an implement of +type flowConn struct { + enet.Conn + fl *FlowLimiter +} + +// Read add the in flow num of the service +func (fs *flowConn) Read(b []byte) (n int, err error) { + n, err = fs.Conn.Read(b) + if fs.fl.InLimit > 0 && fs.fl.Store.AddIn(uint32(n)) > fs.fl.InLimit { + err = errors.New("exceed the in flow limit") + } + return +} + +// Write add the out flow num of the service +func (fs *flowConn) Write(b []byte) (n int, err error) { + n, err = fs.Conn.Write(b) + if fs.fl.OutLimit > 0 && fs.fl.Store.AddOut(uint32(n)) > fs.fl.OutLimit { + err = errors.New("exceed the out flow limit") + } + return +} diff --git a/core/limiter/flow_test.go b/core/limiter/flow_test.go new file mode 100644 index 0000000..544eeb0 --- /dev/null +++ b/core/limiter/flow_test.go @@ -0,0 +1,59 @@ +package limiter + +import ( + "bytes" + "ehang.io/nps/lib/enet" + "github.com/stretchr/testify/assert" + "net" + "testing" +) + +func TestFlowLimiter(t *testing.T) { + cl := FlowLimiter{ + OutLimit: 100, + InLimit: 100, + } + assert.NoError(t, cl.Init()) + ln, err := net.Listen("tcp", "127.0.0.1:0") + assert.NoError(t, err) + nowBytes := 0 + close := make(chan struct{}) + go func() { + buf := make([]byte, 10) + c, err := ln.Accept() + assert.NoError(t, err) + c, err = cl.DoLimit(enet.NewReaderConn(c)) + for { + n, err := c.Read(buf) + nowBytes += n + if nowBytes > 100 { + assert.Error(t, err) + nowBytes = 0 + for i := 11; i > 0; i-- { + n, err = c.Write(bytes.Repeat([]byte{0}, 10)) + nowBytes += n + if nowBytes > 100 { + assert.Error(t, err) + close <- struct{}{} + } else { + assert.NoError(t, err) + } + } + } else { + assert.NoError(t, err) + } + } + }() + c, err := net.Dial("tcp", ln.Addr().String()) + assert.NoError(t, err) + for i := 11; i > 0; i-- { + _, err := c.Write(bytes.Repeat([]byte{0}, 10)) + assert.NoError(t, err) + } + buf := make([]byte, 10) + for i := 11; i > 0; i-- { + _, err := c.Read(buf) + assert.NoError(t, err) + } + <-close +} diff --git a/core/limiter/ip_conn_num.go b/core/limiter/ip_conn_num.go new file mode 100644 index 0000000..b8ce120 --- /dev/null +++ b/core/limiter/ip_conn_num.go @@ -0,0 +1,99 @@ +package limiter + +import ( + "ehang.io/nps/lib/enet" + "github.com/pkg/errors" + "net" + "sync" +) + +// ipNumMap is used to store the connection num of a ip address +type ipNumMap struct { + m map[string]int32 + sync.Mutex +} + +// AddOrSet is used to add connection num of a ip address +func (i *ipNumMap) AddOrSet(key string) { + i.Lock() + if v, ok := i.m[key]; ok { + i.m[key] = v + 1 + } else { + i.m[key] = 1 + } + i.Unlock() +} + +// SubOrDel is used to decrease connection of a ip address +func (i *ipNumMap) SubOrDel(key string) { + i.Lock() + if v, ok := i.m[key]; ok { + i.m[key] = v - 1 + if i.m[key] == 0 { + delete(i.m, key) + } + } + i.Unlock() +} + +// Get return the connection num of a ip +func (i *ipNumMap) Get(key string) int32 { + return i.m[key] +} + +// IpConnNumLimiter is used to limit the connection num of a service at the same time of same ip +type IpConnNumLimiter struct { + m *ipNumMap + MaxNum int32 `json:"max_num" required:"true" placeholder:"10" zh_name:"单ip最大连接数"` + sync.Mutex +} + +func (cl *IpConnNumLimiter) GetName() string { + return "ip_conn_num" +} + +func (cl *IpConnNumLimiter) GetZhName() string { + return "单ip连接数限制" +} + +// Init the ipNumMap +func (cl *IpConnNumLimiter) Init() error { + cl.m = &ipNumMap{m: make(map[string]int32)} + return nil +} + +// DoLimit reports whether the connection num of the ip exceed the maximum number +// If true, return error +func (cl *IpConnNumLimiter) DoLimit(c enet.Conn) (enet.Conn, error) { + ip, _, err := net.SplitHostPort(c.RemoteAddr().String()) + if err != nil { + return c, errors.Wrap(err, "split ip addr") + } + if cl.m.Get(ip) >= cl.MaxNum { + return c, errors.Errorf("the ip(%s) exceed the maximum number(%d)", ip, cl.MaxNum) + } + return NewNumConn(c, ip, cl.m), nil +} + +// numConn is an implement of enet.Conn +type numConn struct { + key string + m *ipNumMap + enet.Conn +} + +// NewNumConn return a numConn +func NewNumConn(c enet.Conn, key string, m *ipNumMap) *numConn { + m.AddOrSet(key) + return &numConn{ + m: m, + key: key, + Conn: c, + } +} + +// Close is used to decrease the connection num of a ip when connection closing +func (n *numConn) Close() error { + n.m.SubOrDel(n.key) + return n.Conn.Close() +} diff --git a/core/limiter/ip_conn_num_test.go b/core/limiter/ip_conn_num_test.go new file mode 100644 index 0000000..a198a34 --- /dev/null +++ b/core/limiter/ip_conn_num_test.go @@ -0,0 +1,35 @@ +package limiter + +import ( + "ehang.io/nps/lib/enet" + "github.com/stretchr/testify/assert" + "net" + "testing" +) + +func TestIpConnNumLimiter(t *testing.T) { + cl := IpConnNumLimiter{MaxNum: 5} + assert.NoError(t, cl.Init()) + ln, err := net.Listen("tcp", "127.0.0.1:0") + assert.NoError(t, err) + nowNum := 0 + close := make(chan struct{}) + go func() { + for { + c, err := ln.Accept() + assert.NoError(t, err) + nowNum++ + _, err = cl.DoLimit(enet.NewReaderConn(c)) + if nowNum > 5 { + assert.Error(t, err) + close <- struct{}{} + } else { + assert.NoError(t, err) + } + } + }() + for i := 6; i > 0; i-- { + go net.Dial("tcp", ln.Addr().String()) + } + <-close +} diff --git a/core/limiter/limiter.go b/core/limiter/limiter.go new file mode 100644 index 0000000..74a4e8f --- /dev/null +++ b/core/limiter/limiter.go @@ -0,0 +1,26 @@ +package limiter + +import ( + "ehang.io/nps/lib/enet" +) + +var ( + _ Limiter = (*RateLimiter)(nil) + _ Limiter = (*ConnNumLimiter)(nil) + _ Limiter = (*IpConnNumLimiter)(nil) + _ Limiter = (*FlowLimiter)(nil) +) + +type Limiter interface { + DoLimit(conn enet.Conn) (enet.Conn, error) + Init() error + GetName() string + GetZhName() string +} + +type baseLimiter struct { +} + +func (bl *baseLimiter) Init() error { + return nil +} diff --git a/core/limiter/rate.go b/core/limiter/rate.go new file mode 100644 index 0000000..5d1762d --- /dev/null +++ b/core/limiter/rate.go @@ -0,0 +1,67 @@ +package limiter + +import ( + "ehang.io/nps/lib/enet" + "ehang.io/nps/lib/rate" +) + +// RateLimiter is used to limit the speed of transport +type RateLimiter struct { + baseLimiter + RateLimit int64 `json:"rate_limit" required:"true" placeholder:"10(kb)" zh_name:"最大速度"` + rate *rate.Rate +} + +func (rl *RateLimiter) GetName() string { + return "rate" +} + +func (rl *RateLimiter) GetZhName() string { + return "带宽限制" +} + +// Init the rate controller +func (rl *RateLimiter) Init() error { + if rl.RateLimit > 0 && rl.rate == nil { + rl.rate = rate.NewRate(rl.RateLimit) + rl.rate.Start() + } + return nil +} + +// DoLimit return limited Conn +func (rl *RateLimiter) DoLimit(c enet.Conn) (enet.Conn, error) { + return NewRateConn(c, rl.rate), nil +} + +// rateConn is used to limiter the rate fo connection +type rateConn struct { + enet.Conn + rate *rate.Rate +} + +// NewRateConn return limited connection by rate interface +func NewRateConn(rc enet.Conn, rate *rate.Rate) enet.Conn { + return &rateConn{ + Conn: rc, + rate: rate, + } +} + +// Read data and remove capacity from rate pool +func (s *rateConn) Read(b []byte) (n int, err error) { + n, err = s.Conn.Read(b) + if s.rate != nil && err == nil { + err = s.rate.Get(int64(n)) + } + return +} + +// Write data and remove capacity from rate pool +func (s *rateConn) Write(b []byte) (n int, err error) { + n, err = s.Conn.Write(b) + if s.rate != nil && err == nil { + err = s.rate.Get(int64(n)) + } + return +} diff --git a/core/limiter/rate_test.go b/core/limiter/rate_test.go new file mode 100644 index 0000000..0c103ca --- /dev/null +++ b/core/limiter/rate_test.go @@ -0,0 +1,46 @@ +package limiter + +import ( + "bytes" + "ehang.io/nps/lib/enet" + "github.com/stretchr/testify/assert" + "net" + "testing" + "time" +) + +func TestRateLimiter(t *testing.T) { + cl := RateLimiter{ + RateLimit: 100, + } + assert.NoError(t, cl.Init()) + ln, err := net.Listen("tcp", "127.0.0.1:0") + assert.NoError(t, err) + nowBytes := 0 + close := make(chan struct{}) + go func() { + buf := make([]byte, 10) + c, err := ln.Accept() + assert.NoError(t, err) + c, err = cl.DoLimit(enet.NewReaderConn(c)) + go func() { + <-time.After(time.Second * 2) + if nowBytes > 500 { + t.Fail() + } + close <- struct{}{} + }() + for { + n, err := c.Read(buf) + nowBytes += n + assert.NoError(t, err) + } + }() + c, err := net.Dial("tcp", ln.Addr().String()) + assert.NoError(t, err) + for i := 11; i > 0; i-- { + _, err := c.Write(bytes.Repeat([]byte{0}, 10000)) + assert.NoError(t, err) + } + <-close +} diff --git a/core/process/http_proxy.go b/core/process/http_proxy.go new file mode 100644 index 0000000..201ac8c --- /dev/null +++ b/core/process/http_proxy.go @@ -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 +} diff --git a/core/process/http_proxy_test.go b/core/process/http_proxy_test.go new file mode 100644 index 0000000..8d2b644 --- /dev/null +++ b/core/process/http_proxy_test.go @@ -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) +} diff --git a/core/process/http_serve.go b/core/process/http_serve.go new file mode 100644 index 0000000..89576ab --- /dev/null +++ b/core/process/http_serve.go @@ -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) +} diff --git a/core/process/http_serve_test.go b/core/process/http_serve_test.go new file mode 100644 index 0000000..7f1e0d9 --- /dev/null +++ b/core/process/http_serve_test.go @@ -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) + +} diff --git a/core/process/https_proxy.go b/core/process/https_proxy.go new file mode 100644 index 0000000..4cbaf53 --- /dev/null +++ b/core/process/https_proxy.go @@ -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))) +} diff --git a/core/process/https_proxy_test.go b/core/process/https_proxy_test.go new file mode 100644 index 0000000..dc44ada --- /dev/null +++ b/core/process/https_proxy_test.go @@ -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) +} diff --git a/core/process/https_redirect.go b/core/process/https_redirect.go new file mode 100644 index 0000000..0f1ad6d --- /dev/null +++ b/core/process/https_redirect.go @@ -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 +} diff --git a/core/process/https_redirect_test.go b/core/process/https_redirect_test.go new file mode 100644 index 0000000..fb67dcf --- /dev/null +++ b/core/process/https_redirect_test.go @@ -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) +} diff --git a/core/process/https_serve.go b/core/process/https_serve.go new file mode 100644 index 0000000..a13e38d --- /dev/null +++ b/core/process/https_serve.go @@ -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 +} diff --git a/core/process/https_serve_test.go b/core/process/https_serve_test.go new file mode 100644 index 0000000..a5bdd37 --- /dev/null +++ b/core/process/https_serve_test.go @@ -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) + +} diff --git a/core/process/pb_app.go b/core/process/pb_app.go new file mode 100644 index 0000000..fbf4692 --- /dev/null +++ b/core/process/pb_app.go @@ -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)) +} diff --git a/core/process/pb_app_test.go b/core/process/pb_app_test.go new file mode 100644 index 0000000..79fc816 --- /dev/null +++ b/core/process/pb_app_test.go @@ -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()) +} diff --git a/core/process/pb_ping.go b/core/process/pb_ping.go new file mode 100644 index 0000000..ab0b7f4 --- /dev/null +++ b/core/process/pb_ping.go @@ -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 +} diff --git a/core/process/pb_ping_test.go b/core/process/pb_ping_test.go new file mode 100644 index 0000000..b597a1f --- /dev/null +++ b/core/process/pb_ping_test.go @@ -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) +} diff --git a/core/process/process.go b/core/process/process.go new file mode 100644 index 0000000..05674f7 --- /dev/null +++ b/core/process/process.go @@ -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) +} diff --git a/core/process/serve.go b/core/process/serve.go new file mode 100644 index 0000000..bccbd93 --- /dev/null +++ b/core/process/serve.go @@ -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")}) +} diff --git a/core/process/serve_test.go b/core/process/serve_test.go new file mode 100644 index 0000000..18e6cc3 --- /dev/null +++ b/core/process/serve_test.go @@ -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) +} diff --git a/core/process/socks5.go b/core/process/socks5.go new file mode 100644 index 0000000..6168283 --- /dev/null +++ b/core/process/socks5.go @@ -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) +} diff --git a/core/process/socks5_test.go b/core/process/socks5_test.go new file mode 100644 index 0000000..8f5fd9e --- /dev/null +++ b/core/process/socks5_test.go @@ -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) +} diff --git a/core/process/transparent_linux.go b/core/process/transparent_linux.go new file mode 100644 index 0000000..2c0b992 --- /dev/null +++ b/core/process/transparent_linux.go @@ -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 +} diff --git a/core/process/transparent_others.go b/core/process/transparent_others.go new file mode 100644 index 0000000..1d1b153 --- /dev/null +++ b/core/process/transparent_others.go @@ -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 +} diff --git a/core/rule/list.go b/core/rule/list.go new file mode 100644 index 0000000..5b1be3e --- /dev/null +++ b/core/rule/list.go @@ -0,0 +1,138 @@ +package rule + +import ( + "ehang.io/nps/core/action" + "ehang.io/nps/core/handler" + "ehang.io/nps/core/limiter" + "ehang.io/nps/core/process" + "ehang.io/nps/core/server" + "github.com/fatih/structtag" + "reflect" + "strconv" +) + +var orderMap map[string]int +var nowOrder = 2<<8 - 1 + +type children map[string]*List + +var chains children +var limiters children + +func init() { + orderMap = make(map[string]int, 0) + chains = make(map[string]*List, 0) + limiters = make(map[string]*List, 0) + chains.Append(&server.TcpServer{}).Append(&handler.HttpHandler{}).Append(&process.HttpServeProcess{}).AppendMany(&action.NpcAction{}, &action.LocalAction{}, &action.AdminAction{}) + chains.Append(&server.TcpServer{}).Append(&handler.HttpsHandler{}).Append(&process.HttpsServeProcess{HttpServeProcess: process.HttpServeProcess{}}).AppendMany(&action.NpcAction{}, &action.LocalAction{}, &action.AdminAction{}) + chains.Append(&server.TcpServer{}).Append(&handler.HttpsHandler{}).Append(&process.HttpsRedirectProcess{}).AppendMany(&action.NpcAction{}, &action.LocalAction{}) + chains.Append(&server.TcpServer{}).Append(&handler.HttpHandler{}).Append(&process.HttpProxyProcess{}).AppendMany(&action.NpcAction{}, &action.LocalAction{}) + chains.Append(&server.TcpServer{}).Append(&handler.HttpsHandler{}).Append(&process.HttpsProxyProcess{}).AppendMany(&action.NpcAction{}, &action.LocalAction{}) + chains.Append(&server.TcpServer{}).Append(&handler.Socks5Handler{}).Append(&process.Socks5Process{}).AppendMany(&action.LocalAction{}, &action.NpcAction{}) + chains.Append(&server.TcpServer{}).Append(&handler.TransparentHandler{}).Append(&process.TransparentProcess{}).AppendMany(&action.NpcAction{}) + chains.Append(&server.TcpServer{}).Append(&handler.RdpHandler{}).Append(&process.DefaultProcess{}).AppendMany(&action.NpcAction{}, &action.LocalAction{}) + chains.Append(&server.TcpServer{}).Append(&handler.RedisHandler{}).Append(&process.DefaultProcess{}).AppendMany(&action.NpcAction{}, &action.LocalAction{}) + + chains.Append(&server.UdpServer{}).Append(&handler.DnsHandler{}).Append(&process.DefaultProcess{}).AppendMany(&action.NpcAction{}, &action.LocalAction{}) + // TODO p2p + chains.Append(&server.UdpServer{}).Append(&handler.P2PHandler{}).Append(&process.DefaultProcess{}).AppendMany(&action.NpcAction{}, &action.LocalAction{}) + chains.Append(&server.UdpServer{}).Append(&handler.QUICHandler{}).Append(&process.DefaultProcess{}).AppendMany(&action.BridgeAction{}) + chains.Append(&server.UdpServer{}).Append(&handler.Socks5UdpHandler{}).Append(&process.Socks5Process{}).AppendMany(&action.LocalAction{}, &action.NpcAction{}) + + chains.Append(&server.TcpServer{}).Append(&handler.DefaultHandler{}).Append(&process.DefaultProcess{}).AppendMany(&action.BridgeAction{}, &action.AdminAction{}, &action.NpcAction{}, &action.LocalAction{}) + + limiters.AppendMany(&limiter.RateLimiter{}, &limiter.ConnNumLimiter{}, &limiter.FlowLimiter{}, &limiter.IpConnNumLimiter{}) +} + +func GetLimiters() children { + return limiters +} + +func GetChains() children { + return chains +} + +type NameInterface interface { + GetName() string + GetZhName() string +} + +type List struct { + ZhName string `json:"zh_name"` + Self interface{} `json:"-"` + Field []field `json:"field"` + Children children `json:"children"` +} + +func (c children) AppendMany(child ...NameInterface) { + for _, cd := range child { + c.Append(cd) + } +} + +func (c children) Append(child NameInterface) children { + if v, ok := c[child.GetName()]; ok { + return v.Children + } + if _, ok := orderMap[child.GetName()]; !ok { + orderMap[child.GetName()] = nowOrder + nowOrder-- + } + cd := &List{Self: child, Field: getFieldName(child), Children: make(map[string]*List, 0), ZhName: child.GetZhName()} + c[child.GetName()] = cd + return cd.Children +} + +type field struct { + FiledType string `json:"field_type"` + FieldName string `json:"field_name"` + FieldZhName string `json:"field_zh_name"` + FieldRequired bool `json:"field_required"` + FieldExample string `json:"field_example"` +} + +func getFieldName(structName interface{}, child ...bool) []field { + result := make([]field, 0) + t := reflect.TypeOf(structName) + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() != reflect.Struct { + return result + } + fieldNum := t.NumField() + for i := 0; i < fieldNum; i++ { + if len(child) == 0 && t.Field(i).Type.Kind() == reflect.Struct { + value := reflect.ValueOf(structName) + if value.Kind() == reflect.Ptr { + value = value.Elem() + } + if value.Field(i).CanInterface() { + result = append(result, getFieldName(value.Field(i).Interface(), true)...) + } + } + tags, err := structtag.Parse(string(t.Field(i).Tag)) + if err == nil { + tag, err := tags.Get("json") + if err == nil { + f := field{} + f.FiledType = t.Field(i).Type.Kind().String() + f.FieldName = tag.Name + tag, err = tags.Get("required") + if err == nil { + f.FieldRequired, _ = strconv.ParseBool(tag.Name) + } + tag, err = tags.Get("placeholder") + if err == nil { + f.FieldExample = tag.Name + } + tag, err = tags.Get("zh_name") + if err == nil { + f.FieldZhName = tag.Name + } + result = append(result, f) + } + } + } + return result +} diff --git a/core/rule/list_test.go b/core/rule/list_test.go new file mode 100644 index 0000000..bad5875 --- /dev/null +++ b/core/rule/list_test.go @@ -0,0 +1,13 @@ +package rule + +import ( + "ehang.io/nps/core/process" + "testing" +) + +func TestGetFields(t *testing.T) { + h := process.HttpsServeProcess{HttpServeProcess: process.HttpServeProcess{}} + if len(getFieldName(h)) < 3 { + t.Fail() + } +} diff --git a/core/rule/rule.go b/core/rule/rule.go new file mode 100644 index 0000000..dd8c187 --- /dev/null +++ b/core/rule/rule.go @@ -0,0 +1,71 @@ +package rule + +import ( + "ehang.io/nps/core/action" + "ehang.io/nps/core/handler" + "ehang.io/nps/core/limiter" + "ehang.io/nps/core/process" + "ehang.io/nps/core/server" + "ehang.io/nps/lib/enet" + "github.com/pkg/errors" +) + +type Rule struct { + Server server.Server `json:"server"` + Handler handler.Handler `json:"handler"` + Process process.Process `json:"process"` + Action action.Action `json:"action"` + Limiters []limiter.Limiter `json:"limiters"` +} + +var servers map[string]server.Server + +func init() { + servers = make(map[string]server.Server, 0) +} + +func (r *Rule) GetHandler() handler.Handler { + return r.Handler +} + +func (r *Rule) Init() error { + s := r.Server + var ok bool + if s, ok = servers[r.Server.GetName()+":"+r.Server.GetServerAddr()]; !ok { + s = r.Server + err := s.Init() + servers[r.Server.GetName()+":"+r.Server.GetServerAddr()] = s + if err != nil { + return err + } + go s.Serve() + } + s.RegisterHandle(r) + r.Handler.AddRule(r) + if err := r.Action.Init(); err != nil { + return err + } + for _, l := range r.Limiters { + if err := l.Init(); err != nil { + return err + } + } + return r.Process.Init(r.Action) +} + +func (r *Rule) RunConn(c enet.Conn) (bool, error) { + var err error + for _, lm := range r.Limiters { + if c, err = lm.DoLimit(c); err != nil { + return true, errors.Wrap(err, "rule run") + } + } + if err = c.Reset(0); err != nil { + return false, err + } + return r.Process.ProcessConn(c) +} + +func (r *Rule) RunPacketConn(pc enet.PacketConn) (bool, error) { + return r.Process.ProcessPacketConn(pc) +} diff --git a/core/rule/rule_json.go b/core/rule/rule_json.go new file mode 100644 index 0000000..b6d9dca --- /dev/null +++ b/core/rule/rule_json.go @@ -0,0 +1,92 @@ +package rule + +import ( + "ehang.io/nps/core/action" + "ehang.io/nps/core/handler" + "ehang.io/nps/core/limiter" + "ehang.io/nps/core/process" + "ehang.io/nps/core/server" + "encoding/json" + "github.com/pkg/errors" + "reflect" +) + +type JsonData struct { + ObjType string `json:"obj_type"` + ObjData string `json:"obj_data"` +} + +type JsonRule struct { + Name string `json:"name"` + Uuid string `json:"uuid"` + Status int `json:"status"` + Extend int `json:"extend"` + Server JsonData `json:"server"` + Handler JsonData `json:"handler"` + Process JsonData `json:"process"` + Action JsonData `json:"action"` + Limiters []JsonData `json:"limiters"` + Remark string `json:"remark"` +} + +var NotFoundError = errors.New("not found") + +func (jd *JsonRule) ToRule() (*Rule, error) { + r := &Rule{Limiters: make([]limiter.Limiter, 0)} + s, ok := chains[jd.Server.ObjType] + if !ok { + return nil, NotFoundError + } + r.Server = clone(s.Self).(server.Server) + err := json.Unmarshal([]byte(jd.Server.ObjData), r.Server) + if err != nil { + return nil, err + } + h, ok := s.Children[jd.Handler.ObjType] + if !ok { + return nil, NotFoundError + } + r.Handler = clone(h.Self).(handler.Handler) + err = json.Unmarshal([]byte(jd.Handler.ObjData), r.Handler) + if err != nil { + return nil, err + } + p, ok := h.Children[jd.Process.ObjType] + if !ok { + return nil, NotFoundError + } + r.Process = clone(p.Self).(process.Process) + err = json.Unmarshal([]byte(jd.Process.ObjData), r.Process) + if err != nil { + return nil, err + } + a, ok := p.Children[jd.Action.ObjType] + if !ok { + return nil, NotFoundError + } + r.Action = clone(a.Self).(action.Action) + err = json.Unmarshal([]byte(jd.Action.ObjData), r.Action) + if err != nil { + return nil, err + } + for _, v := range jd.Limiters { + l, ok := limiters[v.ObjType] + if !ok { + return nil, NotFoundError + } + lm := clone(l.Self).(limiter.Limiter) + err = json.Unmarshal([]byte(v.ObjData), lm) + if err != nil { + return nil, err + } + r.Limiters = append(r.Limiters, lm) + } + return r, nil +} + +func clone(i interface{}) interface{} { + v := reflect.ValueOf(i).Elem() + vNew := reflect.New(v.Type()) + vNew.Elem().Set(v) + return vNew.Interface() +} diff --git a/core/rule/rule_json_test.go b/core/rule/rule_json_test.go new file mode 100644 index 0000000..27f3fbb --- /dev/null +++ b/core/rule/rule_json_test.go @@ -0,0 +1,58 @@ +package rule + +import ( + "ehang.io/nps/core/action" + "ehang.io/nps/core/handler" + "ehang.io/nps/core/process" + "ehang.io/nps/core/server" + "encoding/json" + "github.com/stretchr/testify/assert" + "reflect" + "testing" +) + +func TestClone(t *testing.T) { + type person struct { + Name string + Age int + } + a := &person{ + Name: "ALice", + Age: 20, + } + b := clone(a).(*person) + assert.Equal(t, a.Name, b.Name) + assert.Equal(t, a.Age, b.Age) + a.Name = "Bob" + a.Age = 21 + assert.NotEqual(t, a.Name, b.Name) + assert.NotEqual(t, a.Age, b.Age) + assert.NotEqual(t, reflect.ValueOf(a).Pointer(), reflect.ValueOf(b).Pointer()) +} + +func getJson(t *testing.T, i interface{}) string { + b, err := json.Marshal(i) + assert.NoError(t, err) + assert.NotEmpty(t, string(b)) + return string(b) +} + +func TestJsonRule(t *testing.T) { + s := &server.TcpServer{ ServerAddr: "127.0.0.1:0"} + h := &handler.HttpHandler{} + p := &process.HttpServeProcess{} + a := &action.LocalAction{} + js := JsonRule{ + Uuid: "", + Server: JsonData{s.GetName(), getJson(t, s)}, + Handler: JsonData{h.GetName(), getJson(t, h)}, + Process: JsonData{p.GetName(), getJson(t, p)}, + Action: JsonData{a.GetName(), getJson(t, a)}, + Limiters: make([]JsonData, 0), + } + rl, err := js.ToRule() + assert.NoError(t, err) + err = rl.Init() + assert.NoError(t, err) + assert.Equal(t, rl.Server.(*server.TcpServer).ServerAddr, "127.0.0.1:0") +} diff --git a/core/rule/rule_test.go b/core/rule/rule_test.go new file mode 100644 index 0000000..c356aba --- /dev/null +++ b/core/rule/rule_test.go @@ -0,0 +1,45 @@ +package rule + +import ( + "ehang.io/nps/core/action" + "ehang.io/nps/core/handler" + "ehang.io/nps/core/limiter" + "ehang.io/nps/core/process" + "ehang.io/nps/core/server" + "github.com/stretchr/testify/assert" + "net" + "testing" +) + +func TestRule(t *testing.T) { + ln, err := net.Listen("tcp", "127.0.0.1:0") + assert.NoError(t, err) + r := &Rule{ + Server: &server.TcpServer{ServerAddr: "127.0.0.1:0"}, + Handler: &handler.DefaultHandler{}, + Process: &process.DefaultProcess{}, + Action: &action.LocalAction{TargetAddr: []string{ln.Addr().String()}}, + Limiters: make([]limiter.Limiter, 0), + } + err = r.Init() + assert.NoError(t, err) + data := []byte("test") + go func() { + conn, err := ln.Accept() + assert.NoError(t, err) + b := make([]byte, 1024) + n, err := conn.Read(b) + assert.NoError(t, err) + assert.Equal(t, data, b[:n]) + _, err = conn.Write(b[:n]) + assert.NoError(t, err) + }() + conn, err := net.Dial(r.Server.GetName(), r.Server.GetServerAddr()) + assert.NoError(t, err) + _, err = conn.Write(data) + assert.NoError(t, err) + b := make([]byte, 1024) + n, err := conn.Read(b) + assert.NoError(t, err) + assert.Equal(t, b[:n], data) +} diff --git a/core/rule/sort.go b/core/rule/sort.go new file mode 100644 index 0000000..f882906 --- /dev/null +++ b/core/rule/sort.go @@ -0,0 +1,30 @@ +package rule + +import "ehang.io/nps/core/process" + +type Sort []*Rule + +func (s Sort) Len() int { return len(s) } +func (s Sort) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +// Less rule sort by +func (s Sort) Less(i, j int) bool { + iHandlerSort := orderMap[s[i].Handler.GetName()] + iProcessSort := orderMap[s[i].Process.GetName()] + jHandlerSort := orderMap[s[j].Handler.GetName()] + jProcessSort := orderMap[s[j].Process.GetName()] + iSort := iHandlerSort<<16 | iProcessSort<<8 + jSort := jHandlerSort<<16 | jProcessSort<<8 + if vi, ok := s[i].Process.(*process.HttpServeProcess); ok { + if vj, ok := s[j].Process.(*process.HttpServeProcess); ok { + iSort = iSort | (len(vj.RouteUrl) & (2 ^ 8 - 1)) + jSort = jSort | (len(vi.RouteUrl) & (2 ^ 8 - 1)) + } + } + if vi, ok := s[i].Process.(*process.HttpsServeProcess); ok { + if vj, ok := s[j].Process.(*process.HttpsServeProcess); ok { + iSort = iSort | (len(vj.RouteUrl) & (2 ^ 8 - 1)) + jSort = jSort | (len(vi.RouteUrl) & (2 ^ 8 - 1)) + } + } + return iSort > jSort +} diff --git a/core/rule/sort_test.go b/core/rule/sort_test.go new file mode 100644 index 0000000..0894210 --- /dev/null +++ b/core/rule/sort_test.go @@ -0,0 +1,27 @@ +package rule + +import ( + "ehang.io/nps/core/handler" + "ehang.io/nps/core/process" + "sort" + "testing" +) + +func TestSort_Len(t *testing.T) { + r1 := &Rule{Handler: &handler.DefaultHandler{}, Process: &process.TransparentProcess{}} + r2 := &Rule{Handler: &handler.DefaultHandler{}, Process: &process.DefaultProcess{}} + r3 := &Rule{Handler: &handler.DefaultHandler{}, Process: &process.HttpServeProcess{RouteUrl: "/test/aaa"}} + r4 := &Rule{Handler: &handler.DefaultHandler{}, Process: &process.Socks5Process{}} + r5 := &Rule{Handler: &handler.DefaultHandler{}, Process: &process.HttpServeProcess{RouteUrl: "/test"}} + r6 := &Rule{Handler: &handler.HttpsHandler{}, Process: &process.HttpsProxyProcess{}} + s := make(Sort, 0) + s = append(s, r1, r2, r3, r4, r5, r6) + sort.Sort(s) + expected := make(Sort, 0) + expected = append(expected, r6, r5, r3, r4, r1, r2) + for k, v := range expected { + if v != s[k] { + t.Fail() + } + } +} diff --git a/core/server/server.go b/core/server/server.go new file mode 100644 index 0000000..c4b4183 --- /dev/null +++ b/core/server/server.go @@ -0,0 +1,32 @@ +package server + +import "ehang.io/nps/core/handler" + +type rule interface { + handler.RuleRun + GetHandler() handler.Handler +} + +type Server interface { + Init() error + Serve() + GetServerAddr() string + GetName() string + GetZhName() string + RegisterHandle(rl rule) +} + +type BaseServer struct { + handlers map[string]handler.Handler +} + +func (bs *BaseServer) RegisterHandle(rl rule) { + var h handler.Handler + var ok bool + if h, ok = bs.handlers[rl.GetHandler().GetName()]; !ok { + h = rl.GetHandler() + bs.handlers[h.GetName()] = h + } + h.AddRule(rl) + return +} \ No newline at end of file diff --git a/core/server/tcp.go b/core/server/tcp.go new file mode 100644 index 0000000..f654812 --- /dev/null +++ b/core/server/tcp.go @@ -0,0 +1,97 @@ +package server + +import ( + "ehang.io/nps/core/handler" + "ehang.io/nps/lib/enet" + "ehang.io/nps/lib/logger" + "ehang.io/nps/lib/pool" + "github.com/panjf2000/ants/v2" + "go.uber.org/zap" + "io" + "net" +) + +var bp = pool.NewBufferPool(1500) + +type TcpServer struct { + BaseServer + ServerAddr string `json:"server_addr" required:"true" placeholder:"0.0.0.0:8080 or :8080" zh_name:"监听地址"` + listener net.Listener + gp *ants.PoolWithFunc +} + +func (cm *TcpServer) GetServerAddr() string { + if cm.listener == nil { + return cm.ServerAddr + } + return cm.listener.Addr().String() +} + +func (cm *TcpServer) Init() error { + var err error + cm.handlers = make(map[string]handler.Handler, 0) + if err = cm.listen(); err != nil { + return err + } + cm.gp, err = ants.NewPoolWithFunc(1000000, func(i interface{}) { + rc := enet.NewReaderConn(i.(net.Conn)) + buf := bp.Get() + defer bp.Put(buf) + + if _, err := io.ReadAtLeast(rc, buf, 3); err != nil { + logger.Warn("read handle type fom connection failed", zap.String("remote addr", rc.RemoteAddr().String())) + _ = rc.Close() + return + } + logger.Debug("read handle type", zap.Uint8("type 1", buf[0]), zap.Uint8("type 2", buf[1]), + zap.Uint8("type 3", buf[2]), zap.String("remote addr", rc.RemoteAddr().String())) + + for _, h := range cm.handlers { + err = rc.Reset(0) + if err != nil { + logger.Warn("reset connection error", zap.Error(err), zap.String("remote addr", rc.RemoteAddr().String())) + _ = rc.Close() + return + } + ok, err := h.HandleConn(buf, rc) + if err != nil { + logger.Warn("handle connection error", zap.Error(err), zap.String("remote addr", rc.RemoteAddr().String())) + return + } + if ok { + logger.Debug("handle connection success", zap.String("remote addr", rc.RemoteAddr().String())) + return + } + } + }) + return nil +} + +func (cm *TcpServer) GetName() string { + return "tcp" +} + +func (cm *TcpServer) GetZhName() string { + return "tcp服务" +} + +// create a listener accept user and npc +func (cm *TcpServer) listen() error { + var err error + cm.listener, err = net.Listen("tcp", cm.ServerAddr) + if err != nil { + return err + } + return nil +} + +func (cm *TcpServer) Serve() { + for { + c, err := cm.listener.Accept() + if err != nil { + logger.Error("accept enet error", zap.Error(err)) + break + } + _ = cm.gp.Invoke(c) + } +} diff --git a/core/server/udp.go b/core/server/udp.go new file mode 100644 index 0000000..28c0762 --- /dev/null +++ b/core/server/udp.go @@ -0,0 +1,80 @@ +package server + +import ( + "ehang.io/nps/core/handler" + "ehang.io/nps/lib/logger" + "github.com/panjf2000/ants/v2" + "go.uber.org/zap" + "net" +) + +type UdpServer struct { + ServerAddr string `json:"server_addr" required:"true" placeholder:"0.0.0.0:8080 or :8080" zh_name:"监听地址"` + gp *ants.PoolWithFunc + packetConn net.PacketConn + handlers map[string]handler.Handler +} + +type udpPacket struct { + n int + buf []byte + addr net.Addr +} + +func (us *UdpServer) Init() error { + us.handlers = make(map[string]handler.Handler, 0) + if err := us.listen(); err != nil { + return err + } + var err error + us.gp, err = ants.NewPoolWithFunc(1000000, func(i interface{}) { + p := i.(*udpPacket) + defer bp.Put(p.buf) + + logger.Debug("accept a now packet", zap.String("remote addr", p.addr.String())) + + }) + return err +} + +func (us *UdpServer) GetServerAddr() string { + if us.packetConn == nil { + return us.ServerAddr + } + return us.packetConn.LocalAddr().String() +} + +func (us *UdpServer) GetName() string { + return "udp" +} + +func (us *UdpServer) GetZhName() string { + return "udp服务" +} + +func (us *UdpServer) listen() error { + addr, err := net.ResolveUDPAddr("udp", us.ServerAddr) + if err != nil { + return err + } + us.packetConn, err = net.ListenUDP("udp", addr) + if err != nil { + return err + } + return nil +} + +func (us *UdpServer) Serve() { + for { + buf := bp.Get() + n, addr, err := us.packetConn.ReadFrom(buf) + if err != nil { + logger.Error("accept packet failed", zap.Error(err)) + break + } + err = us.gp.Invoke(udpPacket{n: n, buf: buf, addr: addr}) + if err != nil { + logger.Error("Invoke error", zap.Error(err)) + } + } +} diff --git a/db/db.go b/db/db.go new file mode 100644 index 0000000..75a116e --- /dev/null +++ b/db/db.go @@ -0,0 +1,216 @@ +package db + +import ( + "ehang.io/nps/lib/logger" + "github.com/pkg/errors" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + glog "gorm.io/gorm/logger" + "log" + "reflect" + "time" +) + +type Db interface { + Init() error + SetConfig(key string, val string) error + GetConfig(key string) (string, error) + Insert(table string, uuid string, data string) error + Delete(table string, uuid string) error + Update(table string, uuid string, data string) error + Count(table string, filterValue string) (int64, error) + QueryOne(table string, uuid string) (string, error) + QueryAll(table string, filterValue string) ([]string, error) + QueryPage(table string, limit int, offset int, filterValue string) ([]string, error) +} + +type dbLogger struct{} + +func (_ dbLogger) Write(p []byte) (n int, err error) { + logger.Warn(string(p)) + return len(p), nil +} + +var _ Db = (*SqliteDb)(nil) + +type SqliteDb struct { + Path string + db *gorm.DB +} + +func NewSqliteDb(path string) *SqliteDb { + return &SqliteDb{Path: path} +} + +func (sd *SqliteDb) SetConfig(key string, val string) error { + c := &Config{Key: key} + err := sd.db.First(c, "key = ?", key).Error + if err != nil { + return sd.db.Create(&Config{Key: key, Val: val}).Error + } + return sd.db.Model(c).Update("val", val).Error +} + +func (sd *SqliteDb) GetConfig(key string) (string, error) { + c := &Config{} + err := sd.db.First(c, "key = ?", key).Error + return c.Val, err +} + +func (sd *SqliteDb) Count(table string, filterValue string) (int64, error) { + i, err := sd.GetTable(table, "", "") + if err != nil { + return 0, err + } + var count int64 + err = sd.db.Where("data LIKE ?", "%"+filterValue+"%").Model(i).Count(&count).Error + return count, err +} + +func (sd *SqliteDb) QueryOne(table string, uuid string) (string, error) { + i, err := sd.GetTable(table, "", "") + if err != nil { + return "", err + } + err = sd.db.First(i, "uuid = ?", uuid).Error + if err != nil { + return "", err + } + + return getData(i), err +} + +func getData(i interface{}) string { + immutable := reflect.ValueOf(i) + return immutable.Elem().FieldByName("Data").String() +} + +func (sd *SqliteDb) QueryAll(table string, filterValue string) (data []string, err error) { + list := make([]string, 0) + switch table { + case "rule": + var r []Rule + d := sd.db.Order("id desc") + if filterValue != "" { + d = d.Where("data LIKE ?", "%"+filterValue+"%") + } + err = d.Find(&r).Error + for _, v := range r { + list = append(list, v.Data) + } + case "cert": + var c []Cert + d := sd.db.Order("id desc") + if filterValue != "" { + d = d.Where("data LIKE ?", "%"+filterValue+"%") + } + err = d.Find(&c).Error + for _, v := range c { + list = append(list, v.Data) + } + default: + err = errors.New("error table") + } + return list, err +} + +func (sd *SqliteDb) QueryPage(table string, limit int, offset int, filterValue string) (data []string, err error) { + list := make([]string, 0) + switch table { + case "rule": + var r []Rule + d := sd.db.Limit(limit).Offset(offset) + if filterValue != "" { + d = d.Where("data LIKE ?", "%"+filterValue+"%") + } + err = d.Order("id desc").Find(&r).Error + for _, v := range r { + list = append(list, v.Data) + } + case "cert": + var c []Cert + d := sd.db.Limit(limit).Offset(offset) + if filterValue != "" { + d = d.Where("data LIKE ?", "%"+filterValue+"%") + } + err = d.Order("id desc").Find(&c).Error + for _, v := range c { + list = append(list, v.Data) + } + default: + err = errors.New("error table") + } + return list, err +} + +func (sd *SqliteDb) Insert(table string, uuid string, data string) error { + i, err := sd.GetTable(table, uuid, data) + if err != nil { + return err + } + return sd.db.Create(i).Error +} + +func (sd *SqliteDb) Delete(table string, uuid string) error { + i, err := sd.GetTable(table, uuid, "") + if err != nil { + return err + } + return sd.db.Where("uuid", uuid).Delete(i).Error +} + +func (sd *SqliteDb) Update(table string, uuid string, data string) error { + i, err := sd.GetTable(table, uuid, "") + if err != nil { + return err + } + return sd.db.Model(i).Where("uuid", uuid).Update("data", data).Error +} + +func (sd *SqliteDb) Init() error { + var err error + newLogger := glog.New( + log.New(dbLogger{}, "\r\n", log.LstdFlags), + glog.Config{ + SlowThreshold: time.Second, + LogLevel: glog.Silent, + IgnoreRecordNotFoundError: true, + Colorful: false, + }, + ) + sd.db, err = gorm.Open(sqlite.Open(sd.Path), &gorm.Config{Logger: newLogger}) + if err != nil { + return err + } + return sd.db.AutoMigrate(&Cert{}, &Rule{}, &Config{}) +} + +func (sd *SqliteDb) GetTable(table string, uuid string, data string) (i interface{}, err error) { + switch table { + case "rule": + i = &Rule{Uuid: uuid, Data: data} + case "cert": + i = &Cert{Uuid: uuid, Data: data} + default: + err = errors.New("error table") + } + return +} + +type Cert struct { + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + Uuid string `gorm:"column:uuid" json:"uuid"` + Data string `gorm:"column:data" json:"data"` +} + +type Rule struct { + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + Uuid string `gorm:"column:uuid" json:"uuid"` + Data string `gorm:"column:data" json:"data"` +} + +type Config struct { + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + Key string `gorm:"column:key" json:"key"` + Val string `gorm:"column:val" json:"val"` +} diff --git a/db/db_test.go b/db/db_test.go new file mode 100644 index 0000000..ab688c0 --- /dev/null +++ b/db/db_test.go @@ -0,0 +1,73 @@ +package db + +import ( + uuid "github.com/satori/go.uuid" + "github.com/stretchr/testify/assert" + "os" + "path/filepath" + "strconv" + "testing" +) + +func TestSqlite(t *testing.T) { + err := os.Remove(filepath.Join(os.TempDir(), "test.db")) + assert.NoError(t, err) + d := NewSqliteDb(filepath.Join(os.TempDir(), "test.db")) + err = d.Init() + assert.NoError(t, err) + for _, tableName := range []string{"rule", "cert"} { + var firstUuid, lastUuid string + for i := 0; i < 1000; i++ { + uid := uuid.NewV4().String() + if i == 0 { + firstUuid = uid + } + lastUuid = uid + err = d.Insert(tableName, uid, "test"+strconv.Itoa(i)) + assert.NoError(t, err) + } + n, err := d.Count(tableName,"") + assert.NoError(t, err) + assert.Equal(t, int(n), 1000) + list, err := d.QueryAll(tableName, "") + assert.NoError(t, err) + assert.Equal(t, len(list), 1000) + one, err := d.QueryOne(tableName, firstUuid) + assert.NoError(t, err) + assert.Equal(t, one, "test0") + list, err = d.QueryPage(tableName, 10, 10, "") + assert.NoError(t, err) + assert.Equal(t, len(list), 10) + assert.Equal(t, list[0], "test989") + err = d.Delete(tableName, lastUuid) + assert.NoError(t, err) + n, err = d.Count(tableName,"") + assert.NoError(t, err) + assert.Equal(t, n, int64(999)) + one, err = d.QueryOne(tableName, firstUuid) + assert.NoError(t, err) + err = d.Update(tableName, firstUuid, "test_new") + assert.NoError(t, err) + one, err = d.QueryOne(tableName, firstUuid) + assert.NoError(t, err) + assert.Equal(t, one, "test_new") + } + err = d.SetConfig("test_key1", "test_val1") + assert.NoError(t, err) + v, err := d.GetConfig("test_key1") + assert.NoError(t, err) + assert.Equal(t, v, "test_val1") + v, err = d.GetConfig("test_key2") + assert.Error(t, err) + assert.Equal(t, v, "") + err = d.SetConfig("test_key2", "test_val2") + assert.NoError(t, err) + v, err = d.GetConfig("test_key2") + assert.NoError(t, err) + assert.Equal(t, v, "test_val2") + err = d.SetConfig("test_key1", "test_val2") + assert.NoError(t, err) + v, err = d.GetConfig("test_key1") + assert.NoError(t, err) + assert.Equal(t, v, "test_val2") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..cf22e31 --- /dev/null +++ b/go.mod @@ -0,0 +1,43 @@ +module ehang.io/nps + +go 1.16 + +require ( + github.com/appleboy/gin-jwt v2.5.0+incompatible // indirect + github.com/appleboy/gin-jwt/v2 v2.7.0 // indirect + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect + github.com/fatih/structtag v1.2.0 // indirect + github.com/gin-contrib/cache v1.1.0 // indirect + github.com/gin-contrib/cors v1.3.1 // indirect + github.com/gin-gonic/gin v1.7.6 // indirect + github.com/gliderlabs/ssh v0.3.3 // indirect + github.com/go-redis/redis/v8 v8.11.4 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 + github.com/icodeface/grdp v0.0.0-20200414055757-e0008b0b5cb2 // indirect + github.com/knadh/koanf v1.3.3 // indirect + github.com/lucas-clemente/quic-go v0.24.0 + github.com/marten-seemann/qtls v0.10.0 // indirect + github.com/miekg/dns v1.1.43 // indirect + github.com/panjf2000/ants/v2 v2.4.6 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 // indirect + github.com/satori/go.uuid v1.2.0 // indirect + github.com/shirou/gopsutil v3.21.11+incompatible // indirect + github.com/stretchr/testify v1.7.0 // indirect + github.com/tidwall/gjson v1.12.1 // indirect + github.com/yusufpapurcu/wmi v1.2.2 // indirect + go.opentelemetry.io/otel v0.20.0 // indirect + go.uber.org/zap v1.19.1 + golang.org/x/crypto v0.0.0-20211202192323-5770296d904e // indirect + golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c // indirect + google.golang.org/protobuf v1.27.1 // indirect + gopkg.in/dgrijalva/jwt-go.v3 v3.2.0 // indirect + gopkg.in/go-playground/assert.v1 v1.2.1 // indirect + gopkg.in/go-playground/validator.v8 v8.18.2 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.0.0 + gorm.io/driver/sqlite v1.2.6 // indirect + gorm.io/gorm v1.22.3 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a237cfb --- /dev/null +++ b/go.sum @@ -0,0 +1,546 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/appleboy/gin-jwt v2.5.0+incompatible h1:oLQTP1fiGDoDKoC2UDqXD9iqCP44ABIZMMenfH/xCqw= +github.com/appleboy/gin-jwt v2.5.0+incompatible/go.mod h1:pG7tv32IEe5wEh1NSQzcyD02ZZAqZWp07RdGiIhgaRQ= +github.com/appleboy/gin-jwt/v2 v2.7.0 h1:MjbX0OVC1hmb+cYNSW7yrlG8KfIN/X0qn5kqhAsHinY= +github.com/appleboy/gin-jwt/v2 v2.7.0/go.mod h1:AP2pmslwuqdHcjua9m8APdgqX9Ksx0ZM34d5RGvUYBU= +github.com/appleboy/gofight/v2 v2.1.2/go.mod h1:frW+U1QZEdDgixycTj4CygQ48yLTUhplt43+Wczp3rw= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= +github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw= +github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ= +github.com/aws/aws-sdk-go-v2/service/appconfig v1.4.2/go.mod h1:FZ3HkCe+b10uFZZkFdvf98LHW21k49W8o8J366lqVKY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8= +github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk= +github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= +github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737 h1:rRISKWyXfVxvoa702s91Zl5oREZTrR3yv+tXrrX7G/g= +github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= +github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= +github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/cache v1.1.0 h1:lM8B4YtzdQQM6ThTlvtNPeBNfW1mNdh/CMFQfenH1dk= +github.com/gin-contrib/cache v1.1.0/go.mod h1:9ylpYjLq309/y5hTpyuDxfPG+V6QlSB56vrWe6OhoLQ= +github.com/gin-contrib/cors v1.3.1 h1:doAsuITavI4IOcd0Y19U4B+O0dNWihRyX//nn4sEmgA= +github.com/gin-contrib/cors v1.3.1/go.mod h1:jjEJ4268OPZUcU7k9Pm653S7lXUGcqMADzFA61xsmDk= +github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= +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.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= +github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= +github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= +github.com/gin-gonic/gin v1.7.6 h1:Ma2JlolDP9KCHuHTrW58EIIxVUQKxSxzuCKguCYyFas= +github.com/gin-gonic/gin v1.7.6/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/gliderlabs/ssh v0.3.3 h1:mBQ8NiOgDkINJrZtoizkC3nDNYgSaWtxyem6S2XHBtA= +github.com/gliderlabs/ssh v0.3.3/go.mod h1:ZSS+CUoKHDrqVakTfTWUlKSr9MtMFkC4UvtQKD7O914= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= +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.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= +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.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg= +github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.1.0 h1:XUgk2Ex5veyVFVeLm0xhusUTQybEbexJXrvPNOKkSY0= +github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= +github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= +github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= +github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q= +github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87 h1:xixZ2bWeofWV68J+x6AzmKuVM/JWCQwkWm6GW/MUR6I= +github.com/hashicorp/yamux v0.0.0-20211028200310-0bc27b27de87/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/icodeface/grdp v0.0.0-20200414055757-e0008b0b5cb2 h1:ygCqbylErDVlQ3ykPaa8lmfrbPQoiGcOlSgC+Ej8VgI= +github.com/icodeface/grdp v0.0.0-20200414055757-e0008b0b5cb2/go.mod h1:AENknrjjTG+yAL3EFNMDxSALP140yz4RXJOqIwGulig= +github.com/icodeface/tls v0.0.0-20190904082144-a3e1fe30543e h1:3V+yaobzgt0CfQTbMoTEwDY5qbvrVnRgr96JBZ00Vhw= +github.com/icodeface/tls v0.0.0-20190904082144-a3e1fe30543e/go.mod h1:VJNHW2GxCtQP/IQtXykBIPBV8maPJ/dHWirVTwm9GwY= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI= +github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/knadh/koanf v1.3.3 h1:eNtBOzQDzkzIIPRCJCx/Ha3DeD/ZFwCAp8JxyqoVAls= +github.com/knadh/koanf v1.3.3/go.mod h1:1cfH5223ZeZUOs8FU2UdTmaNfHpqgtjV0+NHjRO43gs= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +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/lucas-clemente/quic-go v0.24.0 h1:ToR7SIIEdrgOhgVTHvPgdVRJfgVy+N0wQAagH7L4d5g= +github.com/lucas-clemente/quic-go v0.24.0/go.mod h1:paZuzjXCE5mj6sikVLMvqXk8lJV2AsqtJ6bDhjEfxx0= +github.com/lunixbochs/struc v0.0.0-20190326164542-a9e4041416c2 h1:xvBq0/ARZLqmB57m6jds017I+KtXPcsKBHv6dUUac4A= +github.com/lunixbochs/struc v0.0.0-20190326164542-a9e4041416c2/go.mod h1:iOJu9pApjjmEmNq7PqlA5R9mDu/HMF5EM3llWKX/TyA= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= +github.com/marten-seemann/qtls v0.10.0 h1:ECsuYUKalRL240rRD4Ri33ISb7kAQ3qGDlrrl55b2pc= +github.com/marten-seemann/qtls v0.10.0/go.mod h1:UvMd1oaYDACI99/oZUYLzMCkBXQVT0aGm99sJhbT8hs= +github.com/marten-seemann/qtls-go1-15 v0.1.4/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I= +github.com/marten-seemann/qtls-go1-16 v0.1.4 h1:xbHbOGGhrenVtII6Co8akhLEdrawwB2iHl5yhJRpnco= +github.com/marten-seemann/qtls-go1-16 v0.1.4/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk= +github.com/marten-seemann/qtls-go1-17 v0.1.0 h1:P9ggrs5xtwiqXv/FHNwntmuLMNq3KaSIG93AtAZ48xk= +github.com/marten-seemann/qtls-go1-17 v0.1.0/go.mod h1:fz4HIxByo+LlWcreM4CZOYNuz3taBQ8rN2X6FqvaWo8= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA= +github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/memcachier/mc v2.0.1+incompatible h1:s8EDz0xrJLP8goitwZOoq1vA/sm0fPS4X3KAF0nyhWQ= +github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= +github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.13.0 h1:7lLHu94wT9Ij0o6EWWclhu0aOh32VxhkwEJvzuWPeak= +github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= +github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/panjf2000/ants/v2 v2.4.6 h1:drmj9mcygn2gawZ155dRbo+NfXEfAssjZNU1qoIb4gQ= +github.com/panjf2000/ants/v2 v2.4.6/go.mod h1:f6F0NZVFsGCp5A7QW/Zj/m92atWwOkY0OIhFxRNFr4A= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= +github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62 h1:pyecQtsPmlkCsMkYhT5iZ+sUXuwee+OvfuJjinEA3ko= +github.com/robfig/go-cache v0.0.0-20130306151617-9fc39e0dbf62/go.mod h1:65XQgovT59RWatovFwnwocoUxiI/eENTnOY5GK3STuY= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +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/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tidwall/gjson v1.9.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.12.1 h1:ikuZsLdhr8Ws0IdROXUS1Gi4v9Z4pGqpX/CvJkxvfpo= +github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +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 v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +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= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= +github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= +go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= +go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= +go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4= +go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= +go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190909091759-094676da4a83/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211202192323-5770296d904e h1:MUP6MR3rJ7Gk9LEia0LP2ytiH6MuCfs7qYz+47jGdD8= +golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c h1:WtYZ93XtWSO5KlOMgPZu7hXY9WhMZpprvlm5VwvAl8c= +golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/dgrijalva/jwt-go.v3 v3.2.0 h1:N46iQqOtHry7Hxzb9PGrP68oovQmj7EhudNoKHvbOvI= +gopkg.in/dgrijalva/jwt-go.v3 v3.2.0/go.mod h1:hdNXC2Z9yC029rvsQ/on2ZNQ44Z2XToVhpXXbR+J05A= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= +gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.2.6 h1:SStaH/b+280M7C8vXeZLz/zo9cLQmIGwwj3cSj7p6l4= +gorm.io/driver/sqlite v1.2.6/go.mod h1:gyoX0vHiiwi0g49tv+x2E7l8ksauLK0U/gShcdUsjWY= +gorm.io/gorm v1.22.3 h1:/JS6z+GStEQvJNW3t1FTwJwG/gZ+A7crFdRqtvG5ehA= +gorm.io/gorm v1.22.3/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/lib/cert/cert.go b/lib/cert/cert.go new file mode 100644 index 0000000..8a6c52f --- /dev/null +++ b/lib/cert/cert.go @@ -0,0 +1,34 @@ +package cert + +import ( + "crypto/tls" + "crypto/x509" + "encoding/pem" + "github.com/pkg/errors" +) + +// GetCertSnFromConfig return SerialNumber by tls.Config +func GetCertSnFromConfig(config *tls.Config) (string, error) { + if len(config.Certificates) == 0 || len(config.Certificates[0].Certificate) == 0 { + return "", errors.New("certificates is empty") + } + return GetCertSnFromBlock(config.Certificates[0].Certificate[0]) +} + +// GetCertSnFromEncode return SerialNumber by encoded cert +func GetCertSnFromEncode(b []byte) (string, error) { + block, _ := pem.Decode(b) + if block == nil { + return "", errors.New("block is not a cert encoded") + } + return GetCertSnFromBlock(block.Bytes) +} + +// GetCertSnFromBlock return SerialNumber by decode block +func GetCertSnFromBlock(block []byte) (string, error) { + cert, err := x509.ParseCertificate(block) + if err != nil { + return "", errors.Wrap(err, "ParseCertificate") + } + return cert.SerialNumber.String(), nil +} diff --git a/lib/cert/cert_test.go b/lib/cert/cert_test.go new file mode 100644 index 0000000..4843e9f --- /dev/null +++ b/lib/cert/cert_test.go @@ -0,0 +1,42 @@ +package cert + +import ( + "crypto/tls" + "crypto/x509/pkix" + "github.com/stretchr/testify/assert" + "os" + "path/filepath" + "testing" +) + +func TestGetCertSerialNumber(t *testing.T) { + g := 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)) + assert.NoError(t, err) + + cliCrt, err := tls.LoadX509KeyPair(filepath.Join(os.TempDir(), "cert.pem"), filepath.Join(os.TempDir(), "key.pem")) + assert.NoError(t, err) + + config := &tls.Config{ + Certificates: []tls.Certificate{cliCrt}, + } + sn1, err := GetCertSnFromConfig(config) + assert.NoError(t, err) + assert.NotEmpty(t, sn1) + + sn2, err := GetCertSnFromEncode(cert) + assert.NoError(t, err) + assert.NotEmpty(t, sn2) + + assert.Equal(t, sn1, sn2) +} diff --git a/lib/cert/client_hello.go b/lib/cert/client_hello.go new file mode 100644 index 0000000..f8b20d4 --- /dev/null +++ b/lib/cert/client_hello.go @@ -0,0 +1,253 @@ +package cert + +import ( + "strings" +) + +type CurveID uint16 +type SignatureScheme uint16 + +const ( + statusTypeOCSP uint8 = 1 + extensionServerName uint16 = 0 + extensionStatusRequest uint16 = 5 + extensionSupportedCurves uint16 = 10 + extensionSupportedPoints uint16 = 11 + extensionSignatureAlgorithms uint16 = 13 + extensionALPN uint16 = 16 + extensionSCT uint16 = 18 // https://tools.ietf.org/html/rfc6962#section-6 + extensionSessionTicket uint16 = 35 + extensionNextProtoNeg uint16 = 13172 // not IANA assigned + extensionRenegotiationInfo uint16 = 0xff01 + scsvRenegotiation uint16 = 0x00ff +) + +type ClientHelloMsg struct { + raw []byte + vers uint16 + random []byte + sessionId []byte + cipherSuites []uint16 + compressionMethods []uint8 + nextProtoNeg bool + serverName string + ocspStapling bool + scts bool + supportedCurves []CurveID + supportedPoints []uint8 + ticketSupported bool + sessionTicket []uint8 + supportedSignatureAlgorithms []SignatureScheme + secureRenegotiation []byte + secureRenegotiationSupported bool + alpnProtocols []string +} + +func (m *ClientHelloMsg) GetServerName() string { + return m.serverName +} + +func (m *ClientHelloMsg) Unmarshal(data []byte) bool { + if len(data) < 42 { + return false + } + m.raw = data + m.vers = uint16(data[4])<<8 | uint16(data[5]) + m.random = data[6:38] + sessionIdLen := int(data[38]) + if sessionIdLen > 32 || len(data) < 39+sessionIdLen { + return false + } + m.sessionId = data[39 : 39+sessionIdLen] + data = data[39+sessionIdLen:] + if len(data) < 2 { + return false + } + // cipherSuiteLen is the number of bytes of cipher suite numbers. Since + // they are uint16s, the number must be even. + cipherSuiteLen := int(data[0])<<8 | int(data[1]) + if cipherSuiteLen%2 == 1 || len(data) < 2+cipherSuiteLen { + return false + } + numCipherSuites := cipherSuiteLen / 2 + m.cipherSuites = make([]uint16, numCipherSuites) + for i := 0; i < numCipherSuites; i++ { + m.cipherSuites[i] = uint16(data[2+2*i])<<8 | uint16(data[3+2*i]) + if m.cipherSuites[i] == scsvRenegotiation { + m.secureRenegotiationSupported = true + } + } + data = data[2+cipherSuiteLen:] + if len(data) < 1 { + return false + } + compressionMethodsLen := int(data[0]) + if len(data) < 1+compressionMethodsLen { + return false + } + m.compressionMethods = data[1 : 1+compressionMethodsLen] + data = data[1+compressionMethodsLen:] + + m.nextProtoNeg = false + m.serverName = "" + m.ocspStapling = false + m.ticketSupported = false + m.sessionTicket = nil + m.supportedSignatureAlgorithms = nil + m.alpnProtocols = nil + m.scts = false + + if len(data) == 0 { + // ClientHello is optionally followed by extension data + return true + } + if len(data) < 2 { + return false + } + + extensionsLength := int(data[0])<<8 | int(data[1]) + data = data[2:] + if extensionsLength != len(data) { + return false + } + + for len(data) != 0 { + if len(data) < 4 { + return false + } + extension := uint16(data[0])<<8 | uint16(data[1]) + length := int(data[2])<<8 | int(data[3]) + data = data[4:] + if len(data) < length { + return false + } + + switch extension { + case extensionServerName: + d := data[:length] + if len(d) < 2 { + return false + } + namesLen := int(d[0])<<8 | int(d[1]) + d = d[2:] + if len(d) != namesLen { + return false + } + for len(d) > 0 { + if len(d) < 3 { + return false + } + nameType := d[0] + nameLen := int(d[1])<<8 | int(d[2]) + d = d[3:] + if len(d) < nameLen { + return false + } + if nameType == 0 { + m.serverName = string(d[:nameLen]) + // An SNI value may not include a + // trailing dot. See + // https://tools.ietf.org/html/rfc6066#section-3. + if strings.HasSuffix(m.serverName, ".") { + return false + } + break + } + d = d[nameLen:] + } + case extensionNextProtoNeg: + if length > 0 { + return false + } + m.nextProtoNeg = true + case extensionStatusRequest: + m.ocspStapling = length > 0 && data[0] == statusTypeOCSP + case extensionSupportedCurves: + // https://tools.ietf.org/html/rfc4492#section-5.5.1 + if length < 2 { + return false + } + l := int(data[0])<<8 | int(data[1]) + if l%2 == 1 || length != l+2 { + return false + } + numCurves := l / 2 + m.supportedCurves = make([]CurveID, numCurves) + d := data[2:] + for i := 0; i < numCurves; i++ { + m.supportedCurves[i] = CurveID(d[0])<<8 | CurveID(d[1]) + d = d[2:] + } + case extensionSupportedPoints: + // https://tools.ietf.org/html/rfc4492#section-5.5.2 + if length < 1 { + return false + } + l := int(data[0]) + if length != l+1 { + return false + } + m.supportedPoints = make([]uint8, l) + copy(m.supportedPoints, data[1:]) + case extensionSessionTicket: + // https://tools.ietf.org/html/rfc5077#section-3.2 + m.ticketSupported = true + m.sessionTicket = data[:length] + case extensionSignatureAlgorithms: + // https://tools.ietf.org/html/rfc5246#section-7.4.1.4.1 + if length < 2 || length&1 != 0 { + return false + } + l := int(data[0])<<8 | int(data[1]) + if l != length-2 { + return false + } + n := l / 2 + d := data[2:] + m.supportedSignatureAlgorithms = make([]SignatureScheme, n) + for i := range m.supportedSignatureAlgorithms { + m.supportedSignatureAlgorithms[i] = SignatureScheme(d[0])<<8 | SignatureScheme(d[1]) + d = d[2:] + } + case extensionRenegotiationInfo: + if length == 0 { + return false + } + d := data[:length] + l := int(d[0]) + d = d[1:] + if l != len(d) { + return false + } + + m.secureRenegotiation = d + m.secureRenegotiationSupported = true + case extensionALPN: + if length < 2 { + return false + } + l := int(data[0])<<8 | int(data[1]) + if l != length-2 { + return false + } + d := data[2:length] + for len(d) != 0 { + stringLen := int(d[0]) + d = d[1:] + if stringLen == 0 || stringLen > len(d) { + return false + } + m.alpnProtocols = append(m.alpnProtocols, string(d[:stringLen])) + d = d[stringLen:] + } + case extensionSCT: + m.scts = true + if length != 0 { + return false + } + } + data = data[length:] + } + + return true +} diff --git a/lib/cert/generate.go b/lib/cert/generate.go new file mode 100644 index 0000000..aa133b1 --- /dev/null +++ b/lib/cert/generate.go @@ -0,0 +1,108 @@ +package cert + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" + "math/big" + "time" +) + +var _ Generator = (*X509Generator)(nil) + +type Generator interface { + CreateRootCa() ([]byte, []byte, error) + CreateCert(dnsName string) ([]byte, []byte, error) + InitRootCa(rootCa []byte, rootKey []byte) error +} + +type X509Generator struct { + rootCert *x509.Certificate + rootRsaPrivate *rsa.PrivateKey + subject pkix.Name +} + +func NewX509Generator(subject pkix.Name) *X509Generator { + return &X509Generator{ + subject: subject, + } +} + +func (cg *X509Generator) InitRootCa(rootCa []byte, rootKey []byte) error { + var err error + caBlock, _ := pem.Decode(rootCa) + cg.rootCert, err = x509.ParseCertificate(caBlock.Bytes) + if err != nil { + return err + } + + keyBlock, _ := pem.Decode(rootKey) + cg.rootRsaPrivate, err = x509.ParsePKCS1PrivateKey(keyBlock.Bytes) + if err != nil { + return err + } + return nil +} + +func (cg *X509Generator) CreateCert(dnsName string) ([]byte, []byte, error) { + return cg.create(false, dnsName) +} + +func (cg *X509Generator) CreateRootCa() ([]byte, []byte, error) { + return cg.create(true, "") +} + +func (cg *X509Generator) create(isRootCa bool, dnsName string) ([]byte, []byte, error) { + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + template := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: cg.subject, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(3, 0, 0), + BasicConstraintsValid: true, + IsCA: false, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageDataEncipherment, + DNSNames: []string{dnsName}, + } + + if isRootCa { + template.IsCA = true + template.KeyUsage |= x509.KeyUsageCertSign + } + + priKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, err + } + var ca []byte + if !isRootCa { + if cg.rootCert == nil || cg.rootRsaPrivate == nil { + return nil, nil, errors.New("root ca is not exist") + } + ca, err = x509.CreateCertificate(rand.Reader, template, cg.rootCert, &priKey.PublicKey, cg.rootRsaPrivate) + } else { + ca, err = x509.CreateCertificate(rand.Reader, template, template, &priKey.PublicKey, priKey) + } + if err != nil { + return nil, nil, err + } + + caPem := &pem.Block{ + Type: "CERTIFICATE", + Bytes: ca, + } + ca = pem.EncodeToMemory(caPem) + + buf := x509.MarshalPKCS1PrivateKey(priKey) + keyPem := &pem.Block{ + Type: "PRIVATE KEY", + Bytes: buf, + } + key := pem.EncodeToMemory(keyPem) + return ca, key, nil +} diff --git a/lib/cert/generate_test.go b/lib/cert/generate_test.go new file mode 100644 index 0000000..185f5d6 --- /dev/null +++ b/lib/cert/generate_test.go @@ -0,0 +1,62 @@ +package cert + +import ( + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "testing" +) + +func TestCreateCert(t *testing.T) { + dnsName := "ehang.io" + g := NewX509Generator(pkix.Name{ + Country: []string{"CN"}, + Organization: []string{"ehang.io"}, + OrganizationalUnit: []string{"nps"}, + Province: []string{"Beijing"}, + CommonName: "nps", + Locality: []string{"Beijing"}, + }) + // generate root ca + rootCa, rootKey, err := g.CreateRootCa() + if err != nil { + t.Fatal(err) + } + err = g.InitRootCa(rootCa, rootKey) + if err != nil { + t.Fatal(err) + } + + // generate npc cert + clientCa, _, err := g.CreateCert(dnsName) + if err != nil { + t.Fatal(err) + } + + // verify npc cert by root cert + roots := x509.NewCertPool() + ok := roots.AppendCertsFromPEM(rootCa) + if !ok { + panic("failed to parse root certificate") + } + + block, _ := pem.Decode(clientCa) + if block == nil { + t.Fatal("failed to parse certificate PEM") + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + t.Fatal("failed to parse certificate: " + err.Error()) + } + + opts := x509.VerifyOptions{ + Roots: roots, + DNSName: dnsName, + Intermediates: x509.NewCertPool(), + } + + if _, err := cert.Verify(opts); err != nil { + t.Fatal("failed to verify certificate: " + err.Error()) + } + +} diff --git a/lib/common/addr.go b/lib/common/addr.go new file mode 100644 index 0000000..29689fe --- /dev/null +++ b/lib/common/addr.go @@ -0,0 +1,141 @@ +package common + +import ( + "errors" + "io" + "net" + "strconv" +) + +var AddrError = errors.New("addr error") + +// SOCKS address types as defined in RFC 1928 section 5. +const ( + AtypIPv4 = 1 + AtypDomainName = 3 + AtypIPv6 = 4 +) + +// MaxAddrLen is the maximum size of SOCKS address in bytes. +const MaxAddrLen = 1 + 1 + 255 + 2 + +// Addr represents a SOCKS address as defined in RFC 1928 section 5. +type Addr []byte + +// String serializes SOCKS address a to string form. +func (a Addr) String() string { + var host, port string + + switch a[0] { // address type + case AtypDomainName: + host = string(a[2 : 2+int(a[1])]) + port = strconv.Itoa((int(a[2+int(a[1])]) << 8) | int(a[2+int(a[1])+1])) + case AtypIPv4: + host = net.IP(a[1 : 1+net.IPv4len]).String() + port = strconv.Itoa((int(a[1+net.IPv4len]) << 8) | int(a[1+net.IPv4len+1])) + case AtypIPv6: + host = net.IP(a[1 : 1+net.IPv6len]).String() + port = strconv.Itoa((int(a[1+net.IPv6len]) << 8) | int(a[1+net.IPv6len+1])) + } + + return net.JoinHostPort(host, port) +} + +func readAddr(r io.Reader, b []byte) (Addr, error) { + if len(b) < MaxAddrLen { + return nil, io.ErrShortBuffer + } + _, err := io.ReadFull(r, b[:1]) // read 1st byte for address type + if err != nil { + return nil, err + } + + switch b[0] { + case AtypDomainName: + _, err = io.ReadFull(r, b[1:2]) // read 2nd byte for domain length + if err != nil { + return nil, err + } + _, err = io.ReadFull(r, b[2:2+int(b[1])+2]) + return b[:1+1+int(b[1])+2], err + case AtypIPv4: + _, err = io.ReadFull(r, b[1:1+net.IPv4len+2]) + return b[:1+net.IPv4len+2], err + case AtypIPv6: + _, err = io.ReadFull(r, b[1:1+net.IPv6len+2]) + return b[:1+net.IPv6len+2], err + } + + return nil, errors.New("addr type not supported") +} + +// ReadAddr reads just enough bytes from r to get a valid Addr. +func ReadAddr(r io.Reader) (Addr, error) { + return readAddr(r, make([]byte, MaxAddrLen)) +} + +// SplitAddr slices a SOCKS address from beginning of b. Returns nil if failed. +func SplitAddr(b []byte) (Addr, error) { + addrLen := 1 + if len(b) < addrLen { + return nil, AddrError + } + + switch b[0] { + case AtypDomainName: + if len(b) < 2 { + return nil, AddrError + } + addrLen = 1 + 1 + int(b[1]) + 2 + case AtypIPv4: + addrLen = 1 + net.IPv4len + 2 + case AtypIPv6: + addrLen = 1 + net.IPv6len + 2 + default: + return nil, AddrError + + } + + if len(b) < addrLen { + return nil, AddrError + } + + return b[:addrLen], nil +} + +// ParseAddr parses the address in string s. Returns nil if failed. +func ParseAddr(s string) (Addr, error) { + var addr Addr + host, port, err := net.SplitHostPort(s) + if err != nil { + return nil, AddrError + } + if ip := net.ParseIP(host); ip != nil { + if ip4 := ip.To4(); ip4 != nil { + addr = make([]byte, 1+net.IPv4len+2) + addr[0] = AtypIPv4 + copy(addr[1:], ip4) + } else { + addr = make([]byte, 1+net.IPv6len+2) + addr[0] = AtypIPv6 + copy(addr[1:], ip) + } + } else { + if len(host) > 255 { + return nil, AddrError + } + addr = make([]byte, 1+1+len(host)+2) + addr[0] = AtypDomainName + addr[1] = byte(len(host)) + copy(addr[2:], host) + } + + portnum, err := strconv.ParseUint(port, 10, 16) + if err != nil { + return nil, AddrError + } + + addr[len(addr)-2], addr[len(addr)-1] = byte(portnum>>8), byte(portnum) + + return addr, nil +} diff --git a/lib/common/utils.go b/lib/common/utils.go new file mode 100644 index 0000000..de74bc4 --- /dev/null +++ b/lib/common/utils.go @@ -0,0 +1,75 @@ +package common + +import ( + "encoding/binary" + "github.com/pkg/errors" + "io" + "strings" +) + +// CopyBuffer is an implement of io.Copy with buffer pool +func CopyBuffer(dst io.Writer, src io.Reader, buf []byte) (written int64, err error) { + for { + nr, er := src.Read(buf) + if nr > 0 { + nw, ew := dst.Write(buf[0:nr]) + if nw < 0 || nr < nw { + nw = 0 + if ew == nil { + ew = errors.New("invalid write result") + } + } + written += int64(nw) + if ew != nil { + err = ew + break + } + if nr != nw { + err = errors.New("short write") + break + } + } + if er != nil { + if er != io.EOF { + err = er + } + break + } + } + return written, err +} + +// HostContains tests whether the string host contained ruleHost +func HostContains(ruleHost string, host string) bool { + return strings.HasSuffix(host, strings.Replace(ruleHost, "*", "", -1)) +} + +// WriteLenBytes is used to write length and bytes to writer +func WriteLenBytes(w io.Writer, b []byte) (int, error) { + err := binary.Write(w, binary.LittleEndian, uint32(len(b))) + if err != nil { + return 0, errors.Wrap(err, "write len") + } + n, err := w.Write(b) + if err != nil { + return 0, errors.Wrap(err, "write bytes") + } + return n, nil +} + +// ReadLenBytes is used to read bytes from reader +func ReadLenBytes(r io.Reader, b []byte) (int, error) { + var l int32 + err := binary.Read(r, binary.LittleEndian, &l) + if err != nil { + return 0, errors.Wrap(err, "read len") + } + if int(l) > len(b) { + return 0, errors.Errorf("data is too long(%d)", l) + } + n, err := io.ReadAtLeast(r, b, int(l)) + if err != nil { + return n, errors.Wrap(err, "read data error") + } + return n, nil +} diff --git a/lib/enet/conn.go b/lib/enet/conn.go new file mode 100644 index 0000000..f0f4bcc --- /dev/null +++ b/lib/enet/conn.go @@ -0,0 +1,119 @@ +package enet + +import ( + "ehang.io/nps/lib/pool" + "errors" + "net" + "sync" + "syscall" +) + +type Conn interface { + net.Conn + Reset(int) error + Clear() + Readable() bool + AllBytes() ([]byte, error) + SyscallConn() (syscall.RawConn, error) +} + +var _ Conn = (*ReaderConn)(nil) + +var bp = pool.NewBufferPool(MaxReadSize) + +const MaxReadSize = 32 * 1024 + +// ReaderConn is an implement of reusable data connection +type ReaderConn struct { + buf []byte + nowIndex int + hasRead int + hasClear bool + net.Conn + sync.RWMutex +} + +// NewReaderConn returns a new ReaderConn +func NewReaderConn(conn net.Conn) *ReaderConn { + return &ReaderConn{Conn: conn, buf: bp.Get()} +} + +// SyscallConn returns a raw network connection +func (rc *ReaderConn) SyscallConn() (syscall.RawConn, error) { + return rc.Conn.(syscall.Conn).SyscallConn() +} + +// Read is an implement of Net.Conn Read function +func (rc *ReaderConn) Read(b []byte) (n int, err error) { + rc.Lock() + defer rc.Unlock() + if rc.hasClear || (rc.nowIndex == rc.hasRead && rc.hasRead == MaxReadSize) { + if !rc.hasClear { + rc.Clear() + } + return rc.Conn.Read(b) + } + if rc.hasRead > rc.nowIndex { + n = copy(b, rc.buf[rc.nowIndex:rc.hasRead]) + rc.nowIndex += n + return + } + if rc.hasRead == MaxReadSize { + n = copy(b, rc.buf[rc.nowIndex:rc.hasRead]) + rc.nowIndex += n + return + } + err = rc.readOnce() + if err != nil { + return + } + n = copy(b, rc.buf[rc.nowIndex:rc.hasRead]) + rc.nowIndex += n + return +} + +// readOnce +func (rc *ReaderConn) readOnce() error { + // int(math.Min(float64(MaxReadSize-rc.hasRead), float64(len(b)-(rc.hasRead-rc.nowIndex)))) + // read as much as possible to judge whether there is still readable + n, err := rc.Conn.Read(rc.buf[rc.nowIndex : rc.hasRead+MaxReadSize-rc.hasRead]) + rc.hasRead += n + return err +} + +// Readable return whether there is data in the buffer +func (rc *ReaderConn) Readable() bool { + return (rc.hasRead - rc.nowIndex) > 0 +} + +// AllBytes return all data in the buffer +func (rc *ReaderConn) AllBytes() ([]byte, error) { + rc.Lock() + defer rc.Unlock() + if rc.hasRead == 0 { + if err := rc.readOnce(); err != nil { + return nil, err + } + } + if !rc.Readable() { + return nil, errors.New("can not read '") + } + b := rc.buf[rc.nowIndex:rc.hasRead] + rc.nowIndex = rc.hasRead + return b, nil +} + +// Reset will reset data index +func (rc *ReaderConn) Reset(n int) error { + if !rc.hasClear { + rc.nowIndex = n + return nil + } + return errors.New("the enet can not reset anymore") +} + +// Clear will put buf to pool and can not reuse anymore +func (rc *ReaderConn) Clear() { + rc.hasClear = true + bp.Put(rc.buf) +} diff --git a/lib/enet/conn_test.go b/lib/enet/conn_test.go new file mode 100644 index 0000000..6b837c4 --- /dev/null +++ b/lib/enet/conn_test.go @@ -0,0 +1,81 @@ +package enet + +import ( + "math/rand" + "net" + "testing" + "time" +) + +func TestReaderConn_Read(t *testing.T) { + ln, err := net.Listen("tcp", "127.0.0.1:61254") + if err != nil { + t.Fatal(err) + } + b := make([]byte, 33*1024) + go func() { + conn, err := net.Dial("tcp", "127.0.0.1:61254") + if err != nil { + t.Fatal(err) + } + rand.Seed(time.Now().UnixNano()) + for i := 0; i < 33*1024; i++ { + b[i] = byte(rand.Intn(128)) + } + conn.Write(b) + }() + conn, err := ln.Accept() + if err != nil { + t.Fatal(err) + } + rConn := NewReaderConn(conn) + buf := make([]byte, 1024) + nn := 0 + times := 0 + for { + n, err := rConn.Read(buf) + if err != nil { + t.Fatal(err) + } + for i := 0; i < 1024; i++ { + if b[times*1024+i] != buf[i] { + t.Fatal("data error") + } + } + times++ + nn += n + if nn > 30*1024 { + break + } + if times > 100 { + t.Fatal("read error") + } + } + + rConn.Reset(0) + nn = 0 + times = 0 + for { + n, err := rConn.Read(buf) + if err != nil { + t.Fatal(err) + } + for i := 0; i < 1024; i++ { + if b[times*1024+i] != buf[i] { + t.Fatal("data error") + } + } + nn += n + times++ + if nn > 32*1024 { + break + } + if times > 100 { + t.Fatal("read error") + } + } + if !rConn.hasClear || rConn.hasRead != rConn.nowIndex || rConn.nowIndex != MaxReadSize { + t.Fatal("read error") + } + +} diff --git a/lib/enet/listener.go b/lib/enet/listener.go new file mode 100644 index 0000000..6c8ca87 --- /dev/null +++ b/lib/enet/listener.go @@ -0,0 +1,62 @@ +package enet + +import ( + "errors" + "net" + "sync/atomic" +) + +var _ net.Listener = (*Listener)(nil) + +// Listener is an implementation of net.Listener +type Listener struct { + ch chan net.Conn + closeCh chan struct{} + closed int32 + nowNum int32 + addr net.Addr +} + +// NewListener returns an initialized Listener +func NewListener() *Listener { + return &Listener{ch: make(chan net.Conn, 10), closeCh: make(chan struct{})} +} + +// SendConn is used to add connection to the listener +func (bl *Listener) SendConn(c net.Conn) error { + if atomic.LoadInt32(&bl.closed) == 1 { + return errors.New("the listener is already closed") + } + atomic.AddInt32(&bl.nowNum, 1) + select { + case bl.ch <- c: + return nil + case <-bl.closeCh: + } + if atomic.AddInt32(&bl.nowNum, -1) == 0 && atomic.LoadInt32(&bl.closed) == 1 { + close(bl.ch) + } + return errors.New("the listener is already closed") +} + +// Accept is used to get connection from the listener +func (bl *Listener) Accept() (net.Conn, error) { + c := <-bl.ch + if c == nil { + return nil, errors.New("the listener is already closed") + } + return c, nil +} + +// Close is used to close the listener, it will discard all existing connections +func (bl *Listener) Close() error { + if atomic.CompareAndSwapInt32(&bl.closed, 0, 1) { + close(bl.closeCh) + } + return nil +} + +// Addr returns the listener's address' +func (bl *Listener) Addr() net.Addr { + return bl.addr +} diff --git a/lib/enet/paket.go b/lib/enet/paket.go new file mode 100644 index 0000000..85f05c4 --- /dev/null +++ b/lib/enet/paket.go @@ -0,0 +1,161 @@ +package enet + +import ( + "bytes" + "ehang.io/nps/lib/common" + "ehang.io/nps/lib/pool" + "github.com/pkg/errors" + "net" + "sync/atomic" + "time" +) + +var ( + _ net.PacketConn = (*TcpPacketConn)(nil) + _ PacketConn = (*ReaderPacketConn)(nil) +) + +type PacketConn interface { + net.PacketConn + SendPacket([]byte, net.Addr) error + FirstPacket() ([]byte, net.Addr, error) +} + +var udpBp = pool.NewBufferPool(1500) + +// TcpPacketConn is an implement of net.PacketConn by net.Conn +type TcpPacketConn struct { + udpBp []byte + net.Conn +} + +// NewTcpPacketConn return a *TcpPacketConn +func NewTcpPacketConn(conn net.Conn) *TcpPacketConn { + return &TcpPacketConn{Conn: conn} +} + +// ReadFrom is a implement of net.PacketConn ReadFrom +func (tp *TcpPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + b := udpBp.Get() + defer udpBp.Put(b) + n, err = common.ReadLenBytes(tp.Conn, b) + if err != nil { + return + } + rAddr, err := common.ReadAddr(bytes.NewReader(b[:n])) + if err != nil { + return + } + n = copy(p, b[len(rAddr):n]) + addr, err = net.ResolveUDPAddr("udp", rAddr.String()) + return +} + +// WriteTo is a implement of net.PacketConn WriteTo +func (tp *TcpPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + var pAddr common.Addr + pAddr, err = common.ParseAddr(addr.String()) + if err != nil { + return + } + return common.WriteLenBytes(tp.Conn, append(pAddr, p...)) +} + +// ReaderPacketConn is an implementation of net.PacketConn +type ReaderPacketConn struct { + ch chan *packet + closeCh chan struct{} + closed int32 + nowNum int32 + addr net.Addr + writePacketConn net.PacketConn + readTimer *time.Timer + firstPacket []byte +} + +type packet struct { + b []byte + addr net.Addr +} + +// NewReaderPacketConn returns an initialized PacketConn +func NewReaderPacketConn(writePacketConn net.PacketConn, firstPacket []byte, addr net.Addr) *ReaderPacketConn { + return &ReaderPacketConn{ + ch: make(chan *packet, 10), + closeCh: make(chan struct{}), + addr: addr, + writePacketConn: writePacketConn, + readTimer: time.NewTimer(time.Hour * 24 * 3650), + firstPacket: firstPacket, + } +} + +func (pc *ReaderPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + var pt *packet + select { + case pt = <-pc.ch: + case <-pc.readTimer.C: + } + if pt == nil { + return 0, nil, errors.New("the PacketConn is already closed") + } + copy(p, pt.b) + return len(pt.b), pt.addr, nil +} + +func (pc *ReaderPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + return pc.writePacketConn.WriteTo(p, addr) +} + +// LocalAddr returns the listener's address +func (pc *ReaderPacketConn) LocalAddr() net.Addr { + return pc.addr +} + +func (pc *ReaderPacketConn) SetDeadline(t time.Time) error { + pc.readTimer.Reset(t.Sub(time.Now())) + return pc.writePacketConn.SetWriteDeadline(t) +} + +func (pc *ReaderPacketConn) SetReadDeadline(t time.Time) error { + pc.readTimer.Reset(t.Sub(time.Now())) + return nil +} + +func (pc *ReaderPacketConn) SetWriteDeadline(t time.Time) error { + return pc.writePacketConn.SetWriteDeadline(t) +} + +func (pc *ReaderPacketConn) FirstPacket() ([]byte, net.Addr, error) { + if pc.firstPacket == nil || pc.addr == nil { + return nil, nil, errors.New("not found first packet") + } + return pc.firstPacket, pc.addr, nil +} + +// SendPacket is used to add connection to the listener +func (pc *ReaderPacketConn) SendPacket(b []byte, addr net.Addr) error { + if atomic.LoadInt32(&pc.closed) == 1 { + return errors.New("the listener is already closed") + } + atomic.AddInt32(&pc.nowNum, 1) + select { + case pc.ch <- &packet{b: b, addr: addr}: + return nil + case <-pc.closeCh: + case <-pc.readTimer.C: + _ = pc.Close() + } + if atomic.AddInt32(&pc.nowNum, -1) == 0 && atomic.LoadInt32(&pc.closed) == 1 { + close(pc.ch) + } + return errors.New("the packetConn is already closed") +} + +// Close is used to close the listener, it will discard all existing connections +func (pc *ReaderPacketConn) Close() error { + if atomic.CompareAndSwapInt32(&pc.closed, 0, 1) { + close(pc.closeCh) + } + return nil +} diff --git a/lib/enet/paket_test.go b/lib/enet/paket_test.go new file mode 100644 index 0000000..52c0c37 --- /dev/null +++ b/lib/enet/paket_test.go @@ -0,0 +1,78 @@ +package enet + +import ( + "bytes" + "github.com/stretchr/testify/assert" + "net" + "testing" +) + +func TestTcpPacketConn(t *testing.T) { + bs := bytes.Repeat([]byte{1}, 100) + targetAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:53") + assert.NoError(t, err) + + finish := make(chan struct{}, 0) + + ln, err := net.Listen("tcp", "127.0.0.1:0") + assert.NoError(t, err) + go func() { + conn, err := ln.Accept() + assert.NoError(t, err) + b := make([]byte, 1024) + n, addr, err := NewTcpPacketConn(conn).ReadFrom(b) + assert.NoError(t, err) + + assert.Equal(t, targetAddr, addr) + assert.Equal(t, n, 100) + finish <- struct{}{} + }() + + conn, err := net.Dial("tcp", ln.Addr().String()) + assert.NoError(t, err) + + _, err = NewTcpPacketConn(conn).WriteTo(bs, targetAddr) + assert.NoError(t, err) + + <-finish +} + +func TestPacketConn(t *testing.T) { + finish := make(chan struct{}, 0) + + sPacketConn, err := net.ListenPacket("udp", "127.0.0.1:0") + assert.NoError(t, err) + + cPacketConn, err := net.ListenPacket("udp", "127.0.0.1:0") + assert.NoError(t, err) + + bPacketConn := NewReaderPacketConn(sPacketConn, nil, sPacketConn.LocalAddr()) + + sendAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:53") + assert.NoError(t, err) + + go func() { + b := make([]byte, 1024) + n, addr, err := bPacketConn.ReadFrom(b) + assert.NoError(t, err) + assert.Equal(t, sendAddr, addr) + assert.Equal(t, n, 4) + + _, err = bPacketConn.WriteTo(bytes.Repeat(b[:n], 10), cPacketConn.LocalAddr()) + assert.NoError(t, err) + + finish <- struct{}{} + }() + + err = bPacketConn.SendPacket([]byte{0, 0, 0, 0}, sendAddr) + assert.NoError(t, err) + + b := make([]byte, 1024) + n, addr, err := cPacketConn.ReadFrom(b) + assert.NoError(t, err) + assert.Equal(t, n, 40) + assert.Equal(t, addr, sPacketConn.LocalAddr()) + + <-finish + +} diff --git a/lib/enet/s5_packet.go b/lib/enet/s5_packet.go new file mode 100644 index 0000000..21e562e --- /dev/null +++ b/lib/enet/s5_packet.go @@ -0,0 +1,55 @@ +package enet + +import ( + "ehang.io/nps/lib/common" + "ehang.io/nps/lib/pool" + "github.com/pkg/errors" + "net" +) + +var packetBp = pool.NewBufferPool(1500) + +type S5PacketConn struct { + net.PacketConn + remoteAddr net.Addr +} + +func NewS5PacketConn(pc net.PacketConn, remoteAddr net.Addr) *S5PacketConn { + return &S5PacketConn{PacketConn: pc, remoteAddr: remoteAddr} +} + +func (s *S5PacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + b := packetBp.Get() + defer packetBp.Put(b) + n, addr, err = s.PacketConn.ReadFrom(b) + if err != nil { + return + } + var targetAddr common.Addr + targetAddr, err = common.SplitAddr(b[3:]) + if err != nil { + return + } + n = copy(p, b[3+len(targetAddr):n]) + addr, err = net.ResolveUDPAddr("udp", targetAddr.String()) + return +} + +func (s *S5PacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + n = len(p) + b := packetBp.Get() + defer packetBp.Put(b) + var sAddr common.Addr + sAddr, err = common.ParseAddr(addr.String()) + if err != nil { + return + } + copy(b[3:], sAddr) + if (3 + len(sAddr) + len(p)) > len(b) { + err = errors.Errorf("data too long(%d)", len(p)) + return + } + copy(b[3+len(sAddr):], p) + _, err = s.PacketConn.WriteTo(b[:3+len(sAddr)+len(p)], s.remoteAddr) + return +} diff --git a/lib/enet/s5_packet_test.go b/lib/enet/s5_packet_test.go new file mode 100644 index 0000000..0adb769 --- /dev/null +++ b/lib/enet/s5_packet_test.go @@ -0,0 +1,50 @@ +package enet + +import ( + "ehang.io/nps/lib/common" + "github.com/stretchr/testify/assert" + "net" + "testing" +) + +func TestNewS5PacketConn(t *testing.T) { + 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) + appAddr, err := net.ResolveUDPAddr("udp", "8.8.8.8:53") + assert.NoError(t, err) + data := []byte("test") + go func() { + p := make([]byte, 1500) + n, addr, err := serverPc.ReadFrom(p) + assert.NoError(t, err) + pc := NewReaderPacketConn(serverPc, p[:n], addr) + err = pc.SendPacket(p[:n], addr) + assert.NoError(t, err) + + _, addr, err = pc.FirstPacket() + assert.NoError(t, err) + s5Pc := NewS5PacketConn(pc, addr) + n, addr, err = s5Pc.ReadFrom(p) + assert.NoError(t, err) + assert.Equal(t, data, p[:n]) + assert.Equal(t, addr.String(), "8.8.8.8:53") + _, err = s5Pc.WriteTo(data, appAddr) + assert.NoError(t, err) + }() + b := []byte{0, 0, 0} + pAddr, err := common.ParseAddr(appAddr.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(), appAddr.String()) + assert.Equal(t, p[3+len(respAddr):n], data) +} diff --git a/lib/lb/algo.go b/lib/lb/algo.go new file mode 100644 index 0000000..a53de41 --- /dev/null +++ b/lib/lb/algo.go @@ -0,0 +1,96 @@ +package lb + +import ( + "errors" + "sync" +) + +func GetLbAlgo(algo string) Algo { + // switch + return NewRoundRobin() +} + +type Algo interface { + Next() (interface{}, error) + Append(i interface{}) error + Remove(i interface{}) error + Empty() bool +} + +// rotation +type roundRobin struct { + head *server + now *server + sync.RWMutex +} + +type server struct { + self interface{} + next *server +} + +func NewRoundRobin() *roundRobin { + return &roundRobin{} +} + +func (r *roundRobin) Append(i interface{}) error { + r.Lock() + defer r.Unlock() + if r.head == nil { + r.head = &server{self: i} + return nil + } + r.now = r.head + for { + if r.now.next == nil { + r.now.next = &server{self: i} + break + } + r.now = r.now.next + } + return nil +} + +func (r *roundRobin) Remove(i interface{}) error { + r.Lock() + defer r.Unlock() + o := r.head + var last *server + for { + if o == nil { + return errors.New("not round") + } + if o.self == i { + if last == nil { + r.head = o.next + } else { + last.next = o.next + } + r.now = r.head + return nil + } + last = o + o = o.next + } +} + +func (r *roundRobin) Next() (interface{}, error) { + r.Lock() + defer r.Unlock() + if r.head == nil { + return nil, errors.New("not found component") + } + if r.now == nil { + r.now = r.head + } + i := r.now + r.now = r.now.next + return i.self, nil +} + +func (r *roundRobin) Empty() bool { + if r.head != nil { + return false + } + return true +} diff --git a/lib/lb/lb.go b/lib/lb/lb.go new file mode 100644 index 0000000..bf68b53 --- /dev/null +++ b/lib/lb/lb.go @@ -0,0 +1,59 @@ +package lb + +import ( + "errors" + "sync" +) + +func NewLoadBalancer() *LoadBalancer { + return &LoadBalancer{ + instances: make(map[string]Algo, 0), + } +} + +type LoadBalancer struct { + instances map[string]Algo + Algo string + sync.RWMutex +} + +func (lb *LoadBalancer) SetClient(id string, instance interface{}) error { + lb.Lock() + defer lb.Unlock() + var l Algo + var ok bool + if l, ok = lb.instances[id]; !ok { + l = GetLbAlgo(lb.Algo) + lb.instances[id] = l + } + return l.Append(instance) +} + +func (lb *LoadBalancer) RemoveClient(id string, instance interface{}) error { + lb.Lock() + defer lb.Unlock() + var l Algo + var ok bool + if l, ok = lb.instances[id]; !ok { + return errors.New("not found Client") + } + err := l.Remove(instance) + if l.Empty() { + delete(lb.instances, id) + } + return err +} + +func (lb *LoadBalancer) GetClient(id string) (interface{}, error) { + lb.Lock() + l, ok := lb.instances[id] + lb.Unlock() + if !ok { + return nil, errors.New("client can not found") + } + i, err := l.Next() + if err != nil { + return nil, err + } + return i, nil +} diff --git a/lib/lb/lb_test.go b/lib/lb/lb_test.go new file mode 100644 index 0000000..ad9a427 --- /dev/null +++ b/lib/lb/lb_test.go @@ -0,0 +1,49 @@ +package lb + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +type test struct { + id int +} + +func TestLb(t *testing.T) { + lb := NewLoadBalancer() + + for i := 1; i <= 100; i++ { + err := lb.SetClient("test", &test{id: i}) + assert.NoError(t, err) + } + + m := make(map[int]bool) + + for i := 1; i <= 100; i++ { + tt, err := lb.GetClient("test") + assert.NoError(t, err) + if _, ok := m[tt.(*test).id]; ok { + t.Fail() + } + m[tt.(*test).id] = true + } + + for i := 1; i <= 50; i++ { + tt, err := lb.GetClient("test") + assert.NoError(t, err) + err = lb.RemoveClient("test", tt) + assert.NoError(t, err) + } + + m = make(map[int]bool) + for i := 1; i <= 50; i++ { + tt, err := lb.GetClient("test") + assert.NoError(t, err) + if _, ok := m[tt.(*test).id]; ok { + t.Fail() + } + m[tt.(*test).id] = true + } + + +} diff --git a/lib/logger/logger.go b/lib/logger/logger.go new file mode 100644 index 0000000..395cf29 --- /dev/null +++ b/lib/logger/logger.go @@ -0,0 +1,93 @@ +package logger + +import ( + "os" + "path/filepath" + "time" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "gopkg.in/natefinch/lumberjack.v2" +) + +var log *zap.Logger +var atomicLevel zap.AtomicLevel + +func Debug(msg string, fields ...zap.Field) { + log.Debug(msg, fields...) +} + +func Info(msg string, fields ...zap.Field) { + log.Info(msg, fields...) +} + +func Warn(msg string, fields ...zap.Field) { + log.Warn(msg, fields...) +} + +func Error(msg string, fields ...zap.Field) { + log.Error(msg, fields...) +} + +func DPanic(msg string, fields ...zap.Field) { + log.DPanic(msg, fields...) +} + +func Panic(msg string, fields ...zap.Field) { + log.Panic(msg, fields...) +} + +func Fatal(msg string, fields ...zap.Field) { + log.Fatal(msg, fields...) +} + +func Sync() { + log.Sync() +} + +func SetLogLevel(level int) { + atomicLevel.SetLevel(zapcore.Level(level)) +} + +func init() { + hook := lumberjack.Logger{ + Filename: filepath.Join(os.TempDir(), "nps.log"), + MaxSize: 128, + MaxBackups: 30, + MaxAge: 7, + Compress: true, + } + + encoderConfig := zap.NewDevelopmentEncoderConfig() + encoderConfig.TimeKey = "log_time" + encoderConfig.LevelKey = "level" + encoderConfig.NameKey = "logger" + encoderConfig.CallerKey = "caller" + encoderConfig.MessageKey = "msg" + encoderConfig.StacktraceKey = "StacktraceKey" + encoderConfig.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString(t.Format("2006-01-02 15:04:05")) + } + + consoleConfig := encoderConfig + consoleConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder + + atomicLevel = zap.NewAtomicLevel() + // init info level + atomicLevel.SetLevel(zapcore.Level(-1)) + + core := zapcore.NewTee( + zapcore.NewCore(zapcore.NewJSONEncoder(encoderConfig), zapcore.NewMultiWriteSyncer(zapcore.AddSync(&hook)), atomicLevel), + zapcore.NewCore(zapcore.NewConsoleEncoder(consoleConfig), zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout)), atomicLevel), + ) + + caller := zap.AddCaller() + development := zap.Development() + + log = zap.New(core, caller, development, zap.AddCallerSkip(1), zap.AddStacktrace(zap.ErrorLevel)) + defer log.Sync() + undo := zap.ReplaceGlobals(log) + defer undo() + logLevelSignal() + +} diff --git a/lib/logger/logger_others.go b/lib/logger/logger_others.go new file mode 100644 index 0000000..3c736fd --- /dev/null +++ b/lib/logger/logger_others.go @@ -0,0 +1,38 @@ +// +build !windows + +package logger + +import ( + "fmt" + "go.uber.org/zap/zapcore" + "os" + "os/signal" + "syscall" +) + +func logLevelSignal() { + c := make(chan os.Signal) + signal.Notify(c, syscall.SIGUSR1, syscall.SIGUSR2) + fmt.Println("notify receive signal") + + go func() { + for s := range c { + fmt.Println("receive signal ", s.String()) + switch s { + case syscall.SIGUSR1: + cur := atomicLevel.Level() + if (cur - 1) >= zapcore.DebugLevel { + atomicLevel.SetLevel(zapcore.Level(cur - 1)) + } + case syscall.SIGUSR2: + cur := atomicLevel.Level() + if (cur + 1) <= zapcore.FatalLevel { + atomicLevel.SetLevel(zapcore.Level(cur + 1)) + } + default: + } + + fmt.Println("debug level change to ", atomicLevel.String()) + } + }() +} diff --git a/lib/logger/logger_windows.go b/lib/logger/logger_windows.go new file mode 100644 index 0000000..426175b --- /dev/null +++ b/lib/logger/logger_windows.go @@ -0,0 +1,7 @@ +// +build windows + +package logger + +func logLevelSignal() { + +} \ No newline at end of file diff --git a/lib/pb/bridge.pb.go b/lib/pb/bridge.pb.go new file mode 100644 index 0000000..1ac480a --- /dev/null +++ b/lib/pb/bridge.pb.go @@ -0,0 +1,736 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.19.1 +// source: bridge.proto + +package pb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ConnType int32 + +const ( + ConnType_tcp ConnType = 0 + ConnType_udp ConnType = 1 + ConnType_unix ConnType = 2 +) + +// Enum value maps for ConnType. +var ( + ConnType_name = map[int32]string{ + 0: "tcp", + 1: "udp", + 2: "unix", + } + ConnType_value = map[string]int32{ + "tcp": 0, + "udp": 1, + "unix": 2, + } +) + +func (x ConnType) Enum() *ConnType { + p := new(ConnType) + *p = x + return p +} + +func (x ConnType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ConnType) Descriptor() protoreflect.EnumDescriptor { + return file_bridge_proto_enumTypes[0].Descriptor() +} + +func (ConnType) Type() protoreflect.EnumType { + return &file_bridge_proto_enumTypes[0] +} + +func (x ConnType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ConnType.Descriptor instead. +func (ConnType) EnumDescriptor() ([]byte, []int) { + return file_bridge_proto_rawDescGZIP(), []int{0} +} + +type ConnRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=Id,proto3" json:"Id,omitempty"` + // Types that are assignable to ConnType: + // *ConnRequest_AppInfo + // *ConnRequest_NpcInfo + // *ConnRequest_SecretInfo + ConnType isConnRequest_ConnType `protobuf_oneof:"connType"` +} + +func (x *ConnRequest) Reset() { + *x = ConnRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_bridge_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ConnRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ConnRequest) ProtoMessage() {} + +func (x *ConnRequest) ProtoReflect() protoreflect.Message { + mi := &file_bridge_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ConnRequest.ProtoReflect.Descriptor instead. +func (*ConnRequest) Descriptor() ([]byte, []int) { + return file_bridge_proto_rawDescGZIP(), []int{0} +} + +func (x *ConnRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (m *ConnRequest) GetConnType() isConnRequest_ConnType { + if m != nil { + return m.ConnType + } + return nil +} + +func (x *ConnRequest) GetAppInfo() *AppInfo { + if x, ok := x.GetConnType().(*ConnRequest_AppInfo); ok { + return x.AppInfo + } + return nil +} + +func (x *ConnRequest) GetNpcInfo() *NpcInfo { + if x, ok := x.GetConnType().(*ConnRequest_NpcInfo); ok { + return x.NpcInfo + } + return nil +} + +func (x *ConnRequest) GetSecretInfo() *SecretInfo { + if x, ok := x.GetConnType().(*ConnRequest_SecretInfo); ok { + return x.SecretInfo + } + return nil +} + +type isConnRequest_ConnType interface { + isConnRequest_ConnType() +} + +type ConnRequest_AppInfo struct { + AppInfo *AppInfo `protobuf:"bytes,2,opt,name=app_info,json=appInfo,proto3,oneof"` +} + +type ConnRequest_NpcInfo struct { + NpcInfo *NpcInfo `protobuf:"bytes,3,opt,name=npc_info,json=npcInfo,proto3,oneof"` +} + +type ConnRequest_SecretInfo struct { + SecretInfo *SecretInfo `protobuf:"bytes,4,opt,name=secret_info,json=secretInfo,proto3,oneof"` +} + +func (*ConnRequest_AppInfo) isConnRequest_ConnType() {} + +func (*ConnRequest_NpcInfo) isConnRequest_ConnType() {} + +func (*ConnRequest_SecretInfo) isConnRequest_ConnType() {} + +type ClientRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Types that are assignable to ConnType: + // *ClientRequest_AppInfo + // *ClientRequest_Ping + ConnType isClientRequest_ConnType `protobuf_oneof:"connType"` +} + +func (x *ClientRequest) Reset() { + *x = ClientRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_bridge_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ClientRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientRequest) ProtoMessage() {} + +func (x *ClientRequest) ProtoReflect() protoreflect.Message { + mi := &file_bridge_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientRequest.ProtoReflect.Descriptor instead. +func (*ClientRequest) Descriptor() ([]byte, []int) { + return file_bridge_proto_rawDescGZIP(), []int{1} +} + +func (m *ClientRequest) GetConnType() isClientRequest_ConnType { + if m != nil { + return m.ConnType + } + return nil +} + +func (x *ClientRequest) GetAppInfo() *AppInfo { + if x, ok := x.GetConnType().(*ClientRequest_AppInfo); ok { + return x.AppInfo + } + return nil +} + +func (x *ClientRequest) GetPing() *Ping { + if x, ok := x.GetConnType().(*ClientRequest_Ping); ok { + return x.Ping + } + return nil +} + +type isClientRequest_ConnType interface { + isClientRequest_ConnType() +} + +type ClientRequest_AppInfo struct { + AppInfo *AppInfo `protobuf:"bytes,1,opt,name=app_info,json=appInfo,proto3,oneof"` +} + +type ClientRequest_Ping struct { + Ping *Ping `protobuf:"bytes,2,opt,name=ping,proto3,oneof"` +} + +func (*ClientRequest_AppInfo) isClientRequest_ConnType() {} + +func (*ClientRequest_Ping) isClientRequest_ConnType() {} + +type AppInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + AppAddr string `protobuf:"bytes,1,opt,name=app_addr,json=appAddr,proto3" json:"app_addr,omitempty"` + NpcId string `protobuf:"bytes,2,opt,name=npc_id,json=npcId,proto3" json:"npc_id,omitempty"` + ConnType ConnType `protobuf:"varint,3,opt,name=conn_type,json=connType,proto3,enum=ConnType" json:"conn_type,omitempty"` +} + +func (x *AppInfo) Reset() { + *x = AppInfo{} + if protoimpl.UnsafeEnabled { + mi := &file_bridge_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AppInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AppInfo) ProtoMessage() {} + +func (x *AppInfo) ProtoReflect() protoreflect.Message { + mi := &file_bridge_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AppInfo.ProtoReflect.Descriptor instead. +func (*AppInfo) Descriptor() ([]byte, []int) { + return file_bridge_proto_rawDescGZIP(), []int{2} +} + +func (x *AppInfo) GetAppAddr() string { + if x != nil { + return x.AppAddr + } + return "" +} + +func (x *AppInfo) GetNpcId() string { + if x != nil { + return x.NpcId + } + return "" +} + +func (x *AppInfo) GetConnType() ConnType { + if x != nil { + return x.ConnType + } + return ConnType_tcp +} + +type SecretInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + IsUdpConn bool `protobuf:"varint,1,opt,name=is_udp_conn,json=isUdpConn,proto3" json:"is_udp_conn,omitempty"` + AppAddr string `protobuf:"bytes,2,opt,name=app_addr,json=appAddr,proto3" json:"app_addr,omitempty"` +} + +func (x *SecretInfo) Reset() { + *x = SecretInfo{} + if protoimpl.UnsafeEnabled { + mi := &file_bridge_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SecretInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SecretInfo) ProtoMessage() {} + +func (x *SecretInfo) ProtoReflect() protoreflect.Message { + mi := &file_bridge_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SecretInfo.ProtoReflect.Descriptor instead. +func (*SecretInfo) Descriptor() ([]byte, []int) { + return file_bridge_proto_rawDescGZIP(), []int{3} +} + +func (x *SecretInfo) GetIsUdpConn() bool { + if x != nil { + return x.IsUdpConn + } + return false +} + +func (x *SecretInfo) GetAppAddr() string { + if x != nil { + return x.AppAddr + } + return "" +} + +type NpcInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + TunnelId string `protobuf:"bytes,2,opt,name=tunnel_id,json=tunnelId,proto3" json:"tunnel_id,omitempty"` + IsControlTunnel bool `protobuf:"varint,3,opt,name=is_control_tunnel,json=isControlTunnel,proto3" json:"is_control_tunnel,omitempty"` +} + +func (x *NpcInfo) Reset() { + *x = NpcInfo{} + if protoimpl.UnsafeEnabled { + mi := &file_bridge_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *NpcInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NpcInfo) ProtoMessage() {} + +func (x *NpcInfo) ProtoReflect() protoreflect.Message { + mi := &file_bridge_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NpcInfo.ProtoReflect.Descriptor instead. +func (*NpcInfo) Descriptor() ([]byte, []int) { + return file_bridge_proto_rawDescGZIP(), []int{4} +} + +func (x *NpcInfo) GetTunnelId() string { + if x != nil { + return x.TunnelId + } + return "" +} + +func (x *NpcInfo) GetIsControlTunnel() bool { + if x != nil { + return x.IsControlTunnel + } + return false +} + +type NpcResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` +} + +func (x *NpcResponse) Reset() { + *x = NpcResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_bridge_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *NpcResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NpcResponse) ProtoMessage() {} + +func (x *NpcResponse) ProtoReflect() protoreflect.Message { + mi := &file_bridge_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NpcResponse.ProtoReflect.Descriptor instead. +func (*NpcResponse) Descriptor() ([]byte, []int) { + return file_bridge_proto_rawDescGZIP(), []int{5} +} + +func (x *NpcResponse) GetSuccess() bool { + if x != nil { + return x.Success + } + return false +} + +func (x *NpcResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +type Ping struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Now string `protobuf:"bytes,1,opt,name=now,proto3" json:"now,omitempty"` +} + +func (x *Ping) Reset() { + *x = Ping{} + if protoimpl.UnsafeEnabled { + mi := &file_bridge_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Ping) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Ping) ProtoMessage() {} + +func (x *Ping) ProtoReflect() protoreflect.Message { + mi := &file_bridge_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Ping.ProtoReflect.Descriptor instead. +func (*Ping) Descriptor() ([]byte, []int) { + return file_bridge_proto_rawDescGZIP(), []int{6} +} + +func (x *Ping) GetNow() string { + if x != nil { + return x.Now + } + return "" +} + +var File_bridge_proto protoreflect.FileDescriptor + +var file_bridge_proto_rawDesc = []byte{ + 0x0a, 0x0c, 0x62, 0x72, 0x69, 0x64, 0x67, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa7, + 0x01, 0x0a, 0x0b, 0x43, 0x6f, 0x6e, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, + 0x0a, 0x02, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x49, 0x64, 0x12, 0x25, + 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x08, 0x2e, 0x41, 0x70, 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x48, 0x00, 0x52, 0x07, 0x61, 0x70, + 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x25, 0x0a, 0x08, 0x6e, 0x70, 0x63, 0x5f, 0x69, 0x6e, 0x66, + 0x6f, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x4e, 0x70, 0x63, 0x49, 0x6e, 0x66, + 0x6f, 0x48, 0x00, 0x52, 0x07, 0x6e, 0x70, 0x63, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x2e, 0x0a, 0x0b, + 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x0b, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x48, 0x00, + 0x52, 0x0a, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x42, 0x0a, 0x0a, 0x08, + 0x63, 0x6f, 0x6e, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x22, 0x5f, 0x0a, 0x0d, 0x43, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x25, 0x0a, 0x08, 0x61, 0x70, 0x70, + 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x08, 0x2e, 0x41, 0x70, + 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x48, 0x00, 0x52, 0x07, 0x61, 0x70, 0x70, 0x49, 0x6e, 0x66, 0x6f, + 0x12, 0x1b, 0x0a, 0x04, 0x70, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x05, + 0x2e, 0x70, 0x69, 0x6e, 0x67, 0x48, 0x00, 0x52, 0x04, 0x70, 0x69, 0x6e, 0x67, 0x42, 0x0a, 0x0a, + 0x08, 0x63, 0x6f, 0x6e, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x22, 0x64, 0x0a, 0x07, 0x41, 0x70, 0x70, + 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x19, 0x0a, 0x08, 0x61, 0x70, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x70, 0x70, 0x41, 0x64, 0x64, 0x72, 0x12, + 0x15, 0x0a, 0x06, 0x6e, 0x70, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x6e, 0x70, 0x63, 0x49, 0x64, 0x12, 0x27, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x6e, 0x5f, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0a, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, + 0x5f, 0x74, 0x79, 0x70, 0x65, 0x52, 0x08, 0x63, 0x6f, 0x6e, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x22, + 0x47, 0x0a, 0x0a, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1e, 0x0a, + 0x0b, 0x69, 0x73, 0x5f, 0x75, 0x64, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x55, 0x64, 0x70, 0x43, 0x6f, 0x6e, 0x6e, 0x12, 0x19, 0x0a, + 0x08, 0x61, 0x70, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x61, 0x70, 0x70, 0x41, 0x64, 0x64, 0x72, 0x22, 0x52, 0x0a, 0x07, 0x4e, 0x70, 0x63, 0x49, + 0x6e, 0x66, 0x6f, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x64, + 0x12, 0x2a, 0x0a, 0x11, 0x69, 0x73, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x5f, 0x74, + 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x69, 0x73, 0x43, + 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x54, 0x75, 0x6e, 0x6e, 0x65, 0x6c, 0x22, 0x41, 0x0a, 0x0b, + 0x4e, 0x70, 0x63, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, + 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, + 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, + 0x18, 0x0a, 0x04, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x10, 0x0a, 0x03, 0x6e, 0x6f, 0x77, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6e, 0x6f, 0x77, 0x2a, 0x27, 0x0a, 0x09, 0x63, 0x6f, 0x6e, + 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x12, 0x07, 0x0a, 0x03, 0x74, 0x63, 0x70, 0x10, 0x00, 0x12, + 0x07, 0x0a, 0x03, 0x75, 0x64, 0x70, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x75, 0x6e, 0x69, 0x78, + 0x10, 0x02, 0x42, 0x15, 0x5a, 0x13, 0x65, 0x68, 0x61, 0x6e, 0x67, 0x2e, 0x69, 0x6f, 0x2f, 0x6e, + 0x70, 0x73, 0x2f, 0x6c, 0x69, 0x62, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_bridge_proto_rawDescOnce sync.Once + file_bridge_proto_rawDescData = file_bridge_proto_rawDesc +) + +func file_bridge_proto_rawDescGZIP() []byte { + file_bridge_proto_rawDescOnce.Do(func() { + file_bridge_proto_rawDescData = protoimpl.X.CompressGZIP(file_bridge_proto_rawDescData) + }) + return file_bridge_proto_rawDescData +} + +var file_bridge_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_bridge_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_bridge_proto_goTypes = []interface{}{ + (ConnType)(0), // 0: conn_type + (*ConnRequest)(nil), // 1: ConnRequest + (*ClientRequest)(nil), // 2: ClientRequest + (*AppInfo)(nil), // 3: AppInfo + (*SecretInfo)(nil), // 4: SecretInfo + (*NpcInfo)(nil), // 5: NpcInfo + (*NpcResponse)(nil), // 6: NpcResponse + (*Ping)(nil), // 7: ping +} +var file_bridge_proto_depIdxs = []int32{ + 3, // 0: ConnRequest.app_info:type_name -> AppInfo + 5, // 1: ConnRequest.npc_info:type_name -> NpcInfo + 4, // 2: ConnRequest.secret_info:type_name -> SecretInfo + 3, // 3: ClientRequest.app_info:type_name -> AppInfo + 7, // 4: ClientRequest.ping:type_name -> ping + 0, // 5: AppInfo.conn_type:type_name -> conn_type + 6, // [6:6] is the sub-list for method output_type + 6, // [6:6] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_bridge_proto_init() } +func file_bridge_proto_init() { + if File_bridge_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_bridge_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ConnRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_bridge_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ClientRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_bridge_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AppInfo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_bridge_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SecretInfo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_bridge_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*NpcInfo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_bridge_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*NpcResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_bridge_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Ping); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_bridge_proto_msgTypes[0].OneofWrappers = []interface{}{ + (*ConnRequest_AppInfo)(nil), + (*ConnRequest_NpcInfo)(nil), + (*ConnRequest_SecretInfo)(nil), + } + file_bridge_proto_msgTypes[1].OneofWrappers = []interface{}{ + (*ClientRequest_AppInfo)(nil), + (*ClientRequest_Ping)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_bridge_proto_rawDesc, + NumEnums: 1, + NumMessages: 7, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_bridge_proto_goTypes, + DependencyIndexes: file_bridge_proto_depIdxs, + EnumInfos: file_bridge_proto_enumTypes, + MessageInfos: file_bridge_proto_msgTypes, + }.Build() + File_bridge_proto = out.File + file_bridge_proto_rawDesc = nil + file_bridge_proto_goTypes = nil + file_bridge_proto_depIdxs = nil +} diff --git a/lib/pb/bridge.proto b/lib/pb/bridge.proto new file mode 100644 index 0000000..3dd815d --- /dev/null +++ b/lib/pb/bridge.proto @@ -0,0 +1,51 @@ +syntax = "proto3"; + +option go_package = "ehang.io/nps/lib/pb"; + + +message ConnRequest { + string Id = 1; + oneof connType { + AppInfo app_info = 2; + NpcInfo npc_info = 3; + SecretInfo secret_info = 4; + } +} + +message ClientRequest { + oneof connType { + AppInfo app_info = 1; + ping ping = 2; + } +} + +enum conn_type{ + tcp = 0; + udp = 1 ; + unix = 2; +} + +message AppInfo { + string app_addr = 1; + string npc_id = 2; + conn_type conn_type = 3; +} + +message SecretInfo { + bool is_udp_conn = 1; + string app_addr = 2; +} + +message NpcInfo { + string tunnel_id = 2; + bool is_control_tunnel = 3; +} + +message NpcResponse { + bool success = 1; + string message = 2; +} + +message ping{ + string now = 1; +} \ No newline at end of file diff --git a/lib/pb/pb.go b/lib/pb/pb.go new file mode 100644 index 0000000..c5ae1f0 --- /dev/null +++ b/lib/pb/pb.go @@ -0,0 +1,29 @@ +package pb + +import ( + "ehang.io/nps/lib/common" + "github.com/golang/protobuf/proto" + "github.com/pkg/errors" + "io" +) + +// WriteMessage is used to write a message to writer +func WriteMessage(w io.Writer, message proto.Message) (int, error) { + b, err := proto.Marshal(message) + if err != nil { + return 0, errors.Wrap(err, "proto Marshal") + } + n, err := common.WriteLenBytes(w, b) + return n, err +} + +// ReadMessage is used to read a message from reader +func ReadMessage(r io.Reader, message proto.Message) (int, error) { + message.Reset() + b := make([]byte, 4096) + n, err := common.ReadLenBytes(r, b) + if err != nil { + return 0, errors.Wrap(err, "read proto message") + } + return n, proto.Unmarshal(b[:n], message) +} diff --git a/lib/pb/pb_test.go b/lib/pb/pb_test.go new file mode 100644 index 0000000..2c7fcf9 --- /dev/null +++ b/lib/pb/pb_test.go @@ -0,0 +1,25 @@ +package pb + +import ( + "bytes" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestMarshal(t *testing.T) { + app := &AppInfo{ + ConnType: ConnType_udp, + AppAddr: "127.0.0.1:8080", + } + var buf []byte + b := bytes.NewBuffer(buf) + + _, err := WriteMessage(b, app) + assert.NoError(t, err) + + appRecv := &AppInfo{} + _, err = ReadMessage(b, appRecv) + assert.NoError(t, err) + assert.Equal(t, app.AppAddr, appRecv.AppAddr) + assert.Equal(t, app.ConnType, appRecv.ConnType) +} diff --git a/lib/pool/goroutine.go b/lib/pool/goroutine.go new file mode 100644 index 0000000..8e79db4 --- /dev/null +++ b/lib/pool/goroutine.go @@ -0,0 +1,76 @@ +package pool + +import ( + "ehang.io/nps/lib/common" + "github.com/panjf2000/ants/v2" + "io" + "net" + "sync" +) + +var connBp = NewBufferPool(MaxReadSize) +var packetBp = NewBufferPool(1500) + +const MaxReadSize = 32 * 1024 + +var CopyConnGoroutinePool *ants.PoolWithFunc +var CopyPacketGoroutinePool *ants.PoolWithFunc + +type CopyConnGpParams struct { + Writer io.Writer + Reader io.Reader + Wg *sync.WaitGroup +} + +type CopyPacketGpParams struct { + RPacket net.PacketConn + WPacket net.PacketConn + Wg *sync.WaitGroup +} + +func init() { + var err error + CopyConnGoroutinePool, err = ants.NewPoolWithFunc(1000000, func(i interface{}) { + gpp, ok := i.(*CopyConnGpParams) + if !ok { + return + } + buf := connBp.Get() + _, _ = common.CopyBuffer(gpp.Writer, gpp.Reader, buf) + connBp.Put(buf) + gpp.Wg.Done() + if v, ok := gpp.Reader.(*net.TCPConn); ok { + _ = v.CloseWrite() + } + if v, ok := gpp.Writer.(*net.TCPConn); ok { + _ = v.CloseRead() + } + }) + if err != nil { + panic(err) + } + CopyPacketGoroutinePool, err = ants.NewPoolWithFunc(1000000, func(i interface{}) { + cpp, ok := i.(*CopyPacketGpParams) + if !ok { + return + } + buf := connBp.Get() + for { + n, addr, err := cpp.RPacket.ReadFrom(buf) + if err != nil { + break + } + _, err = cpp.WPacket.WriteTo(buf[:n], addr) + if err != nil { + break + } + } + connBp.Put(buf) + _ = cpp.RPacket.Close() + _ = cpp.WPacket.Close() + cpp.Wg.Done() + }) + if err != nil { + panic(err) + } +} diff --git a/lib/pool/pool.go b/lib/pool/pool.go new file mode 100644 index 0000000..5939fc8 --- /dev/null +++ b/lib/pool/pool.go @@ -0,0 +1,32 @@ +package pool + +import "sync" + +type BufferPool struct { + pool sync.Pool + poolSize int +} + +func NewBufferPool(poolSize int) *BufferPool { + bp := &BufferPool{} + bp.pool = sync.Pool{ + New: func() interface{} { + return make([]byte, poolSize, poolSize) + }, + } + bp.poolSize = poolSize + return bp +} + +func (bp *BufferPool) Get() []byte { + buf := bp.pool.Get().([]byte) + return buf[:bp.poolSize] // just like make a new slice, but data may not be 0 +} + +func (bp *BufferPool) Put(x []byte) { + if len(x) == bp.poolSize { + bp.pool.Put(x) + } else { + x = nil // buf is not full, not allowed, New method returns a full buf + } +} diff --git a/lib/rate/rate.go b/lib/rate/rate.go new file mode 100644 index 0000000..76d54ec --- /dev/null +++ b/lib/rate/rate.go @@ -0,0 +1,111 @@ +package rate + +import ( + "errors" + "sync" + "sync/atomic" + "time" +) + +// Rate is an implementation of the token bucket added regularly +type Rate struct { + bucketSize int64 + bucketSurplusSize int64 + bucketAddSize int64 + stopChan chan bool + nowRate int64 + cond *sync.Cond + hasStop bool + hasStart bool +} + +// NewRate return token bucket with specified rate +func NewRate(addSize int64) *Rate { + r := &Rate{ + bucketSize: addSize * 2, + bucketSurplusSize: 0, + bucketAddSize: addSize, + stopChan: make(chan bool), + cond: sync.NewCond(new(sync.Mutex)), + } + return r +} + +// Start is used to add token regularly +func (r *Rate) Start() { + if !r.hasStart { + r.hasStart = true + go r.session() + } +} + +func (r *Rate) add(size int64) { + if res := r.bucketSize - r.bucketSurplusSize; res < r.bucketAddSize { + atomic.AddInt64(&r.bucketSurplusSize, res) + return + } + atomic.AddInt64(&r.bucketSurplusSize, size) +} + +// Write is called when add token to bucket +func (r *Rate) Write(size int64) { + r.add(size) +} + +// Stop is called when not use the rate bucket +func (r *Rate) Stop() { + if r.hasStart { + r.stopChan <- true + r.hasStop = true + r.cond.Broadcast() + } +} + +// Get is called when get token from bucket +func (r *Rate) Get(size int64) error { + if r.hasStop { + return errors.New("the rate has closed") + } + if r.bucketSurplusSize >= size { + atomic.AddInt64(&r.bucketSurplusSize, -size) + return nil + } + for { + r.cond.L.Lock() + r.cond.Wait() + if r.bucketSurplusSize >= size { + r.cond.L.Unlock() + atomic.AddInt64(&r.bucketSurplusSize, -size) + return nil + } + if r.hasStop { + return errors.New("the rate has closed") + } + r.cond.L.Unlock() + } +} + +// GetNowRate returns the current rate +// Just a rough number +func (r *Rate) GetNowRate() int64 { + return r.nowRate +} + +func (r *Rate) session() { + ticker := time.NewTicker(time.Second * 1) + for { + select { + case <-ticker.C: + if rs := r.bucketAddSize - r.bucketSurplusSize; rs > 0 { + r.nowRate = rs + } else { + r.nowRate = r.bucketSize - r.bucketSurplusSize + } + r.add(r.bucketAddSize) + r.cond.Broadcast() + case <-r.stopChan: + ticker.Stop() + return + } + } +} diff --git a/lib/rate/rate_test.go b/lib/rate/rate_test.go new file mode 100644 index 0000000..dd9e188 --- /dev/null +++ b/lib/rate/rate_test.go @@ -0,0 +1,52 @@ +package rate + +import ( + "math" + "testing" + "time" +) + +func TestRate_GetWrite(t *testing.T) { + r := NewRate(1024) + r.Write(2048) + n := 0 + go func() { + for { + r.Get(1024) + n += 1024 + } + }() + select { + case <-time.After(time.Second): + r.Stop() + if n != 2048 { + t.Fatal("get token error", n) + } + } +} + +func TestRate_StartGetRate(t *testing.T) { + r := NewRate(1024) + r.Start() + n := 0 + go func() { + for { + err := r.Get(1024) + if err != nil { + return + } + n += 1024 + } + }() + select { + case <-time.After(time.Second * 5): + r.Stop() + time.Sleep(time.Second * 2) + if n < 4*1024 || n > 1024*5 { + t.Fatal("get error", n) + } + if math.Abs(float64(r.GetNowRate()-1024)) > 100 { + t.Fatal("rate error", n) + } + } +} diff --git a/transport/quic.go b/transport/quic.go new file mode 100644 index 0000000..b7ef07f --- /dev/null +++ b/transport/quic.go @@ -0,0 +1,71 @@ +package transport + +import ( + "context" + quic "github.com/lucas-clemente/quic-go" + "net" +) + +type QUIC struct { + session quic.Session +} + +func NewQUIC(serverSession quic.Session) *QUIC { + return &QUIC{ + session: serverSession, + } +} + +func (qu *QUIC) Server() error { + return nil +} + +func (qu *QUIC) Accept() (net.Conn, error) { + s, err := qu.session.AcceptStream(context.Background()) + if err != nil { + return nil, err + } + return NewQUICConn(s, qu.session.RemoteAddr(), qu.session.LocalAddr()), nil +} + +func (qu *QUIC) Addr() net.Addr { + return qu.session.LocalAddr() +} + +func (qu *QUIC) RemoteAddr() net.Addr { + return qu.session.RemoteAddr() +} + +func (qu *QUIC) Client() error { + return nil +} + +func (qu *QUIC) Open() (net.Conn, error) { + s, err := qu.session.OpenStream() + if err != nil { + return nil, err + } + return NewQUICConn(s, qu.session.RemoteAddr(), qu.session.LocalAddr()), nil +} + +func (qu *QUIC) Close() error { + return qu.session.CloseWithError(1, "by npc") +} + +type QUICConn struct { + quic.Stream + localAddr net.Addr + remoteAddr net.Addr +} + +func NewQUICConn(stream quic.Stream, rd net.Addr, ld net.Addr) *QUICConn { + return &QUICConn{Stream: stream, localAddr: ld, remoteAddr: rd} +} + +func (qc *QUICConn) LocalAddr() net.Addr { + return qc.localAddr +} + +func (qc *QUICConn) RemoteAddr() net.Addr { + return qc.remoteAddr +} diff --git a/transport/transport.go b/transport/transport.go new file mode 100644 index 0000000..45f6a0a --- /dev/null +++ b/transport/transport.go @@ -0,0 +1,17 @@ +package transport + +import ( + "net" +) + +type TunnelType int + +type Conn interface { + Server() error + Accept() (net.Conn, error) + Addr() net.Addr + RemoteAddr() net.Addr + Client() error + Open() (net.Conn, error) + Close() error +} diff --git a/transport/yamux.go b/transport/yamux.go new file mode 100644 index 0000000..c10e609 --- /dev/null +++ b/transport/yamux.go @@ -0,0 +1,51 @@ +package transport + +import ( + "github.com/hashicorp/yamux" + "net" +) + +type YaMux struct { + conn net.Conn + config *yamux.Config + session *yamux.Session +} + +func NewYaMux(conn net.Conn, config *yamux.Config) *YaMux { + return &YaMux{ + conn: conn, + config: config, + } +} + +func (ym *YaMux) Server() error { + var err error + ym.session, err = yamux.Server(ym.conn, ym.config) + return err +} + +func (ym *YaMux) Accept() (net.Conn, error) { + return ym.session.Accept() +} + +func (ym *YaMux) Addr() net.Addr { + return ym.conn.LocalAddr() +} + +func (ym *YaMux) RemoteAddr() net.Addr { + return ym.conn.RemoteAddr() +} + +func (ym *YaMux) Client() error { + var err error + ym.session, err = yamux.Client(ym.conn, ym.config) + return err +} + +func (ym *YaMux) Open() (net.Conn, error) { + return ym.session.Open() +} + +func (ym *YaMux) Close() error { + return ym.conn.Close() +}