diff --git a/internal/client/tunnel/client.go b/internal/client/tunnel/client.go index f68f694..7bb5698 100644 --- a/internal/client/tunnel/client.go +++ b/internal/client/tunnel/client.go @@ -108,20 +108,24 @@ func (c *Client) handleSession() { // handleStream 处理流 func (c *Client) handleStream(stream net.Conn) { - defer stream.Close() - msg, err := protocol.ReadMessage(stream) if err != nil { + stream.Close() return } switch msg.Type { case protocol.MsgTypeProxyConfig: + defer stream.Close() c.handleProxyConfig(msg) case protocol.MsgTypeNewProxy: + defer stream.Close() c.handleNewProxy(stream, msg) case protocol.MsgTypeHeartbeat: + defer stream.Close() c.handleHeartbeat(stream) + case protocol.MsgTypeProxyConnect: + c.handleProxyConnect(stream, msg) } } @@ -175,3 +179,37 @@ func (c *Client) handleHeartbeat(stream net.Conn) { msg := &protocol.Message{Type: protocol.MsgTypeHeartbeatAck} protocol.WriteMessage(stream, msg) } + +// handleProxyConnect 处理代理连接请求 (SOCKS5/HTTP) +func (c *Client) handleProxyConnect(stream net.Conn, msg *protocol.Message) { + defer stream.Close() + + var req protocol.ProxyConnectRequest + if err := msg.ParsePayload(&req); err != nil { + c.sendProxyResult(stream, false, "invalid request") + return + } + + // 连接目标地址 + targetConn, err := net.DialTimeout("tcp", req.Target, 10*time.Second) + if err != nil { + c.sendProxyResult(stream, false, err.Error()) + return + } + defer targetConn.Close() + + // 发送成功响应 + if err := c.sendProxyResult(stream, true, ""); err != nil { + return + } + + // 双向转发数据 + relay.Relay(stream, targetConn) +} + +// sendProxyResult 发送代理连接结果 +func (c *Client) sendProxyResult(stream net.Conn, success bool, message string) error { + result := protocol.ProxyConnectResult{Success: success, Message: message} + msg, _ := protocol.NewMessage(protocol.MsgTypeProxyResult, result) + return protocol.WriteMessage(stream, msg) +} diff --git a/internal/server/tunnel/server.go b/internal/server/tunnel/server.go index a43a5cb..3173558 100644 --- a/internal/server/tunnel/server.go +++ b/internal/server/tunnel/server.go @@ -9,6 +9,7 @@ import ( "github.com/gotunnel/internal/server/db" "github.com/gotunnel/pkg/protocol" + "github.com/gotunnel/pkg/proxy" "github.com/gotunnel/pkg/relay" "github.com/gotunnel/pkg/utils" "github.com/hashicorp/yamux" @@ -208,10 +209,21 @@ func (s *Server) startProxyListeners(cs *ClientSession) { cs.Listeners[rule.RemotePort] = ln cs.mu.Unlock() - log.Printf("[Server] Proxy %s: :%d -> %s:%d", - rule.Name, rule.RemotePort, rule.LocalIP, rule.LocalPort) + ruleType := rule.Type + if ruleType == "" { + ruleType = "tcp" + } - go s.acceptProxyConns(cs, ln, rule) + switch ruleType { + case "socks5", "http": + log.Printf("[Server] %s proxy %s on :%d", + ruleType, rule.Name, rule.RemotePort) + go s.acceptProxyServerConns(cs, ln, rule) + default: + log.Printf("[Server] TCP proxy %s: :%d -> %s:%d", + rule.Name, rule.RemotePort, rule.LocalIP, rule.LocalPort) + go s.acceptProxyConns(cs, ln, rule) + } } } @@ -226,6 +238,20 @@ func (s *Server) acceptProxyConns(cs *ClientSession, ln net.Listener, rule proto } } +// acceptProxyServerConns 接受 SOCKS5/HTTP 代理连接 +func (s *Server) acceptProxyServerConns(cs *ClientSession, ln net.Listener, rule protocol.ProxyRule) { + dialer := proxy.NewTunnelDialer(cs.Session) + proxyServer := proxy.NewServer(rule.Type, dialer) + + for { + conn, err := ln.Accept() + if err != nil { + return + } + go proxyServer.HandleConn(conn) + } +} + // handleProxyConn 处理代理连接 func (s *Server) handleProxyConn(cs *ClientSession, conn net.Conn, rule protocol.ProxyRule) { defer conn.Close() diff --git a/pkg/crypto/tls.go b/pkg/crypto/tls.go new file mode 100644 index 0000000..e5dcf76 --- /dev/null +++ b/pkg/crypto/tls.go @@ -0,0 +1,71 @@ +package crypto + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "net" + "os" + "time" +) + +// GenerateSelfSignedCert 生成自签名证书 +func GenerateSelfSignedCert(certFile, keyFile string) error { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return err + } + + serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + if err != nil { + return err + } + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"GoTunnel"}, + CommonName: "GoTunnel Server", + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), // 10年有效期 + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + DNSNames: []string{"localhost"}, + } + + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return err + } + + // 写入证书文件 + certOut, err := os.Create(certFile) + if err != nil { + return err + } + defer certOut.Close() + pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: certDER}) + + // 写入私钥文件 + keyOut, err := os.Create(keyFile) + if err != nil { + return err + } + defer keyOut.Close() + + privBytes, err := x509.MarshalECPrivateKey(priv) + if err != nil { + return err + } + pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: privBytes}) + + return nil +} diff --git a/pkg/protocol/message.go b/pkg/protocol/message.go index 6865db3..8054210 100644 --- a/pkg/protocol/message.go +++ b/pkg/protocol/message.go @@ -17,6 +17,8 @@ const ( MsgTypeNewProxy uint8 = 6 // 新建代理连接请求 MsgTypeProxyReady uint8 = 7 // 代理就绪 MsgTypeError uint8 = 8 // 错误消息 + MsgTypeProxyConnect uint8 = 9 // 代理连接请求 (SOCKS5/HTTP) + MsgTypeProxyResult uint8 = 10 // 代理连接结果 ) // Message 基础消息结构 @@ -40,9 +42,10 @@ type AuthResponse struct { // ProxyRule 代理规则 type ProxyRule struct { Name string `json:"name" yaml:"name"` - LocalIP string `json:"local_ip" yaml:"local_ip"` - LocalPort int `json:"local_port" yaml:"local_port"` - RemotePort int `json:"remote_port" yaml:"remote_port"` + Type string `json:"type" yaml:"type"` // tcp, socks5, http + LocalIP string `json:"local_ip" yaml:"local_ip"` // tcp 模式使用 + LocalPort int `json:"local_port" yaml:"local_port"` // tcp 模式使用 + RemotePort int `json:"remote_port" yaml:"remote_port"` // 服务端监听端口 } // ProxyConfig 代理配置下发 @@ -61,6 +64,17 @@ type ErrorMessage struct { Message string `json:"message"` } +// ProxyConnectRequest 代理连接请求 +type ProxyConnectRequest struct { + Target string `json:"target"` // 目标地址 host:port +} + +// ProxyConnectResult 代理连接结果 +type ProxyConnectResult struct { + Success bool `json:"success"` + Message string `json:"message,omitempty"` +} + // WriteMessage 写入消息到 writer func WriteMessage(w io.Writer, msg *Message) error { header := make([]byte, 5) diff --git a/pkg/proxy/dialer.go b/pkg/proxy/dialer.go new file mode 100644 index 0000000..9792e0e --- /dev/null +++ b/pkg/proxy/dialer.go @@ -0,0 +1,65 @@ +package proxy + +import ( + "errors" + "net" + + "github.com/gotunnel/pkg/protocol" + "github.com/hashicorp/yamux" +) + +// TunnelDialer 通过隧道连接的拨号器 +type TunnelDialer struct { + session *yamux.Session +} + +// NewTunnelDialer 创建隧道拨号器 +func NewTunnelDialer(session *yamux.Session) *TunnelDialer { + return &TunnelDialer{session: session} +} + +// Dial 通过隧道建立连接 +func (d *TunnelDialer) Dial(network, address string) (net.Conn, error) { + stream, err := d.session.Open() + if err != nil { + return nil, err + } + + // 发送代理连接请求 + req := protocol.ProxyConnectRequest{Target: address} + msg, err := protocol.NewMessage(protocol.MsgTypeProxyConnect, req) + if err != nil { + stream.Close() + return nil, err + } + + if err := protocol.WriteMessage(stream, msg); err != nil { + stream.Close() + return nil, err + } + + // 读取连接结果 + respMsg, err := protocol.ReadMessage(stream) + if err != nil { + stream.Close() + return nil, err + } + + if respMsg.Type != protocol.MsgTypeProxyResult { + stream.Close() + return nil, errors.New("unexpected response type") + } + + var result protocol.ProxyConnectResult + if err := respMsg.ParsePayload(&result); err != nil { + stream.Close() + return nil, err + } + + if !result.Success { + stream.Close() + return nil, errors.New(result.Message) + } + + return stream, nil +} diff --git a/pkg/proxy/http.go b/pkg/proxy/http.go new file mode 100644 index 0000000..b5e550b --- /dev/null +++ b/pkg/proxy/http.go @@ -0,0 +1,88 @@ +package proxy + +import ( + "bufio" + "io" + "net" + "net/http" + "strings" +) + +// HTTPServer HTTP 代理服务 +type HTTPServer struct { + dialer Dialer +} + +// NewHTTPServer 创建 HTTP 代理服务 +func NewHTTPServer(dialer Dialer) *HTTPServer { + return &HTTPServer{dialer: dialer} +} + +// HandleConn 处理 HTTP 代理连接 +func (h *HTTPServer) HandleConn(conn net.Conn) error { + defer conn.Close() + + reader := bufio.NewReader(conn) + req, err := http.ReadRequest(reader) + if err != nil { + return err + } + + if req.Method == http.MethodConnect { + return h.handleConnect(conn, req) + } + return h.handleHTTP(conn, req, reader) +} + +// handleConnect 处理 CONNECT 方法 (HTTPS) +func (h *HTTPServer) handleConnect(conn net.Conn, req *http.Request) error { + target := req.Host + if !strings.Contains(target, ":") { + target = target + ":443" + } + + remote, err := h.dialer.Dial("tcp", target) + if err != nil { + conn.Write([]byte("HTTP/1.1 502 Bad Gateway\r\n\r\n")) + return err + } + defer remote.Close() + + conn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n")) + + go io.Copy(remote, conn) + io.Copy(conn, remote) + return nil +} + +// handleHTTP 处理普通 HTTP 请求 +func (h *HTTPServer) handleHTTP(conn net.Conn, req *http.Request, reader *bufio.Reader) error { + target := req.Host + if !strings.Contains(target, ":") { + target = target + ":80" + } + + remote, err := h.dialer.Dial("tcp", target) + if err != nil { + conn.Write([]byte("HTTP/1.1 502 Bad Gateway\r\n\r\n")) + return err + } + defer remote.Close() + + // 修改请求路径为相对路径 + req.URL.Scheme = "" + req.URL.Host = "" + req.RequestURI = req.URL.Path + if req.URL.RawQuery != "" { + req.RequestURI += "?" + req.URL.RawQuery + } + + // 发送请求到目标 + if err := req.Write(remote); err != nil { + return err + } + + // 转发响应 + _, err = io.Copy(conn, remote) + return err +} diff --git a/pkg/proxy/server.go b/pkg/proxy/server.go new file mode 100644 index 0000000..b6b71b9 --- /dev/null +++ b/pkg/proxy/server.go @@ -0,0 +1,62 @@ +package proxy + +import ( + "log" + "net" +) + +// Server 代理服务器 +type Server struct { + socks5 *SOCKS5Server + http *HTTPServer + listener net.Listener + typ string +} + +// NewServer 创建代理服务器 +func NewServer(typ string, dialer Dialer) *Server { + return &Server{ + socks5: NewSOCKS5Server(dialer), + http: NewHTTPServer(dialer), + typ: typ, + } +} + +// Run 启动代理服务 +func (s *Server) Run(addr string) error { + ln, err := net.Listen("tcp", addr) + if err != nil { + return err + } + s.listener = ln + log.Printf("[Proxy] %s listening on %s", s.typ, addr) + + for { + conn, err := ln.Accept() + if err != nil { + return err + } + go s.HandleConn(conn) + } +} + +func (s *Server) HandleConn(conn net.Conn) { + var err error + switch s.typ { + case "socks5": + err = s.socks5.HandleConn(conn) + case "http": + err = s.http.HandleConn(conn) + } + if err != nil { + log.Printf("[Proxy] Error: %v", err) + } +} + +// Close 关闭服务 +func (s *Server) Close() error { + if s.listener != nil { + return s.listener.Close() + } + return nil +} diff --git a/pkg/proxy/socks5.go b/pkg/proxy/socks5.go new file mode 100644 index 0000000..202ad7b --- /dev/null +++ b/pkg/proxy/socks5.go @@ -0,0 +1,145 @@ +package proxy + +import ( + "encoding/binary" + "errors" + "fmt" + "io" + "net" +) + +const ( + socks5Version = 0x05 + noAuth = 0x00 + cmdConnect = 0x01 + atypIPv4 = 0x01 + atypDomain = 0x03 + atypIPv6 = 0x04 +) + +// SOCKS5Server SOCKS5 代理服务 +type SOCKS5Server struct { + dialer Dialer +} + +// Dialer 连接拨号器接口 +type Dialer interface { + Dial(network, address string) (net.Conn, error) +} + +// NewSOCKS5Server 创建 SOCKS5 服务 +func NewSOCKS5Server(dialer Dialer) *SOCKS5Server { + return &SOCKS5Server{dialer: dialer} +} + +// HandleConn 处理 SOCKS5 连接 +func (s *SOCKS5Server) HandleConn(conn net.Conn) error { + defer conn.Close() + + // 握手阶段 + if err := s.handshake(conn); err != nil { + return err + } + + // 获取请求 + target, err := s.readRequest(conn) + if err != nil { + return err + } + + // 连接目标 + remote, err := s.dialer.Dial("tcp", target) + if err != nil { + s.sendReply(conn, 0x05) // Connection refused + return err + } + defer remote.Close() + + // 发送成功响应 + if err := s.sendReply(conn, 0x00); err != nil { + return err + } + + // 双向转发 + go io.Copy(remote, conn) + io.Copy(conn, remote) + + return nil +} + +// handshake 处理握手 +func (s *SOCKS5Server) handshake(conn net.Conn) error { + buf := make([]byte, 2) + if _, err := io.ReadFull(conn, buf); err != nil { + return err + } + if buf[0] != socks5Version { + return errors.New("unsupported SOCKS version") + } + + nmethods := int(buf[1]) + methods := make([]byte, nmethods) + if _, err := io.ReadFull(conn, methods); err != nil { + return err + } + + // 响应:使用无认证 + _, err := conn.Write([]byte{socks5Version, noAuth}) + return err +} + +// readRequest 读取请求 +func (s *SOCKS5Server) readRequest(conn net.Conn) (string, error) { + buf := make([]byte, 4) + if _, err := io.ReadFull(conn, buf); err != nil { + return "", err + } + + if buf[0] != socks5Version || buf[1] != cmdConnect { + return "", errors.New("unsupported command") + } + + var host string + switch buf[3] { + case atypIPv4: + ip := make([]byte, 4) + if _, err := io.ReadFull(conn, ip); err != nil { + return "", err + } + host = net.IP(ip).String() + case atypDomain: + lenBuf := make([]byte, 1) + if _, err := io.ReadFull(conn, lenBuf); err != nil { + return "", err + } + domain := make([]byte, lenBuf[0]) + if _, err := io.ReadFull(conn, domain); err != nil { + return "", err + } + host = string(domain) + case atypIPv6: + ip := make([]byte, 16) + if _, err := io.ReadFull(conn, ip); err != nil { + return "", err + } + host = net.IP(ip).String() + default: + return "", errors.New("unsupported address type") + } + + portBuf := make([]byte, 2) + if _, err := io.ReadFull(conn, portBuf); err != nil { + return "", err + } + port := binary.BigEndian.Uint16(portBuf) + + return fmt.Sprintf("%s:%d", host, port), nil +} + +// sendReply 发送响应 +func (s *SOCKS5Server) sendReply(conn net.Conn, rep byte) error { + // VER REP RSV ATYP BND.ADDR BND.PORT + reply := []byte{socks5Version, rep, 0x00, atypIPv4, 0, 0, 0, 0, 0, 0} + _, err := conn.Write(reply) + return err +}