package protocol import ( "bufio" "bytes" "crypto/tls" "fmt" "io" "net" "net/http" "strconv" ) // HTTP constants matching SoftEther's protocol layer. // See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Mayaqua/HTTP.h const ( httpVPNTarget = "/vpnsvc/vpn.cgi" // Pack exchange endpoint httpVPNTarget2 = "/vpnsvc/connect.cgi" // Signature upload endpoint httpContentType2 = "application/octet-stream" // Content-Type for Pack POST/response httpContentType3 = "image/jpeg" // Content-Type for signature POST ) // Conn wraps a TLS connection with buffered reading for HTTP protocol exchange. // The SoftEther protocol layers HTTP over TLS, using standard HTTP/1.1 POST // requests and responses to exchange binary Pack payloads. type Conn struct { raw net.Conn tls *tls.Conn reader *bufio.Reader } // DialTLS establishes a TLS connection to a SoftEther server. // SoftEther servers listen on port 443 (or 5555, etc.) and accept TLS connections // that look like normal HTTPS traffic. func DialTLS(host string, port int, insecureSkipVerify bool) (*Conn, error) { addr := net.JoinHostPort(host, strconv.Itoa(port)) raw, err := net.Dial("tcp", addr) if err != nil { return nil, fmt.Errorf("tcp dial: %w", err) } tlsConn := tls.Client(raw, &tls.Config{ ServerName: host, InsecureSkipVerify: insecureSkipVerify, }) if err := tlsConn.Handshake(); err != nil { raw.Close() return nil, fmt.Errorf("tls handshake: %w", err) } return &Conn{ raw: raw, tls: tlsConn, reader: bufio.NewReader(tlsConn), }, nil } // Close closes the underlying TCP connection. func (c *Conn) Close() error { return c.raw.Close() } // Read implements io.Reader, reading from the buffered TLS stream. func (c *Conn) Read(p []byte) (int, error) { return c.reader.Read(p) } // Write implements io.Writer, writing to the TLS stream. func (c *Conn) Write(p []byte) (int, error) { return c.tls.Write(p) } // RemoteAddr returns the remote IP address as a string. func (c *Conn) RemoteAddr() string { if addr, ok := c.raw.RemoteAddr().(*net.TCPAddr); ok { return addr.IP.String() } return c.raw.RemoteAddr().String() } // UploadSignature sends the VPN protocol signature to the server. // The server validates this to confirm the client speaks the SoftEther protocol. // The full watermark GIF (src/Cedar/WaterMark.c) can be sent, but the server // also accepts the short string "VPNCONNECT" as a valid signature. // See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Protocol.c#L7208 (ClientUploadSignature) func UploadSignature(conn *Conn) error { body := []byte("VPNCONNECT") // Build raw HTTP request matching SoftEther's ClientUploadSignature format. var buf bytes.Buffer fmt.Fprintf(&buf, "POST %s HTTP/1.1\r\n", httpVPNTarget2) fmt.Fprintf(&buf, "Host: %s\r\n", conn.RemoteAddr()) fmt.Fprintf(&buf, "Content-Type: %s\r\n", httpContentType3) fmt.Fprintf(&buf, "Connection: Keep-Alive\r\n") fmt.Fprintf(&buf, "Content-Length: %d\r\n", len(body)) fmt.Fprintf(&buf, "\r\n") buf.Write(body) if _, err := conn.Write(buf.Bytes()); err != nil { return fmt.Errorf("write signature: %w", err) } return nil } // SendPack sends a Pack as an HTTP POST request body. // The Pack is serialized to binary and sent as the body of a POST to /vpnsvc/vpn.cgi. // See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Mayaqua/HTTP.c#L1160 (HttpClientSend) func SendPack(conn *Conn, p *Pack) error { p.AddDummyValue() body, err := p.Bytes() if err != nil { return fmt.Errorf("pack serialize: %w", err) } // Build raw HTTP request matching SoftEther's HttpClientSend format exactly. // Go's http.Request.Write adds User-Agent and may reorder headers. var buf bytes.Buffer fmt.Fprintf(&buf, "POST %s HTTP/1.1\r\n", httpVPNTarget) fmt.Fprintf(&buf, "Host: %s\r\n", conn.RemoteAddr()) fmt.Fprintf(&buf, "Keep-Alive: timeout=15; max=19\r\n") fmt.Fprintf(&buf, "Connection: Keep-Alive\r\n") fmt.Fprintf(&buf, "Content-Type: %s\r\n", httpContentType2) fmt.Fprintf(&buf, "Content-Length: %d\r\n", len(body)) fmt.Fprintf(&buf, "\r\n") buf.Write(body) if _, err := conn.Write(buf.Bytes()); err != nil { return fmt.Errorf("write pack: %w", err) } return nil } // RecvPack reads an HTTP response and deserializes the body as a Pack. // See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Mayaqua/HTTP.c#L1202 (HttpClientRecv) func RecvPack(conn *Conn) (*Pack, error) { resp, err := http.ReadResponse(conn.reader, nil) if err != nil { return nil, fmt.Errorf("read http response: %w", err) } defer resp.Body.Close() if resp.StatusCode != 200 { return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode) } // Read entire body into buffer so we can parse the Pack from it body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read response body: %w", err) } return ReadPack(bytes.NewReader(body)) }