Built-in DHCP (raw Ethernet frames through tunnel), automatic reconnection, host route management, classless static routes (option 121/249), DNS config. Single static binary, Linux only. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
154 lines
4.9 KiB
Go
154 lines
4.9 KiB
Go
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))
|
|
}
|