softether-go/pkg/protocol/pack.go
Git Sagar 829ca73b1b initial commit: standalone SoftEther VPN client in Go
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>
2026-06-06 16:13:51 +05:30

366 lines
9.8 KiB
Go

// Package protocol implements the SoftEther VPN wire protocol.
//
// SoftEther uses a custom binary serialization format called "Pack" for all
// control-plane messages (hello, auth, welcome). A Pack contains Elements,
// each with a name, type, and one or more Values.
//
// Wire format (all big-endian):
//
// Pack: uint32(numElements) + Element...
// Element: BufStr(name) + uint32(type) + uint32(numValues) + Value...
// BufStr: uint32(len) + bytes (null-terminated)
// Value: type-dependent (see below)
//
// Reference: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Mayaqua/Pack.c
package protocol
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"math/rand"
"strings"
)
// Value types matching SoftEther's VALUE_* constants.
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Mayaqua/Pack.h#L20-L28
const (
ValueInt uint32 = 0 // 32-bit unsigned integer
ValueData uint32 = 1 // Arbitrary binary data (prefixed with uint32 length)
ValueStr uint32 = 2 // ANSI string (prefixed with uint32 length)
ValueUniStr uint32 = 3 // Unicode string as UTF-8 (prefixed with uint32 length, null-terminated)
ValueInt64 uint32 = 4 // 64-bit unsigned integer
)
// Value holds a single typed value within an Element.
type Value struct {
IntValue uint32
Int64Value uint64
Data []byte
Str string
}
// Element is a named, typed collection of values within a Pack.
type Element struct {
Name string
Type uint32
Values []Value
}
// Pack is a collection of named Elements, used for all SoftEther control messages.
type Pack struct {
Elements []*Element
}
// --- Reading ---
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Mayaqua/Pack.c#L831 (PackRead)
// ReadPack deserializes a Pack from a binary reader.
func ReadPack(r io.Reader) (*Pack, error) {
var num uint32
if err := binary.Read(r, binary.BigEndian, &num); err != nil {
return nil, err
}
if num > 262144 {
return nil, fmt.Errorf("too many elements: %d", num)
}
p := &Pack{}
for i := uint32(0); i < num; i++ {
e, err := readElement(r)
if err != nil {
return nil, fmt.Errorf("element %d: %w", i, err)
}
p.Elements = append(p.Elements, e)
}
return p, nil
}
func readElement(r io.Reader) (*Element, error) {
name, err := readBufStr(r)
if err != nil {
return nil, err
}
var typ, numValues uint32
if err := binary.Read(r, binary.BigEndian, &typ); err != nil {
return nil, err
}
if err := binary.Read(r, binary.BigEndian, &numValues); err != nil {
return nil, err
}
e := &Element{Name: name, Type: typ}
for i := uint32(0); i < numValues; i++ {
v, err := readValue(r, typ)
if err != nil {
return nil, err
}
e.Values = append(e.Values, v)
}
return e, nil
}
func readValue(r io.Reader, typ uint32) (v Value, err error) {
switch typ {
case ValueInt:
err = binary.Read(r, binary.BigEndian, &v.IntValue)
case ValueInt64:
err = binary.Read(r, binary.BigEndian, &v.Int64Value)
case ValueData:
var size uint32
if err = binary.Read(r, binary.BigEndian, &size); err != nil {
return
}
v.Data = make([]byte, size)
_, err = io.ReadFull(r, v.Data)
case ValueStr:
var size uint32
if err = binary.Read(r, binary.BigEndian, &size); err != nil {
return
}
buf := make([]byte, size)
if _, err = io.ReadFull(r, buf); err != nil {
return
}
v.Str = string(buf)
case ValueUniStr:
var size uint32
if err = binary.Read(r, binary.BigEndian, &size); err != nil {
return
}
buf := make([]byte, size)
if _, err = io.ReadFull(r, buf); err != nil {
return
}
// SoftEther sends UTF-8 for UniStr on the wire
v.Str = strings.TrimRight(string(buf), "\x00")
default:
err = fmt.Errorf("unknown value type: %d", typ)
}
return
}
// readBufStr reads a string written by SoftEther's WriteBufStr.
// Wire format: uint32(strlen+1) + strlen bytes (no null terminator on wire).
// The size field includes the null terminator in its count, but the null
// is NOT actually written to the wire.
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Mayaqua/Memory.c#L2903
func readBufStr(r io.Reader) (string, error) {
var size uint32
if err := binary.Read(r, binary.BigEndian, &size); err != nil {
return "", err
}
if size == 0 {
return "", nil
}
// size includes the null terminator count, but only (size-1) bytes are on the wire
buf := make([]byte, size-1)
if _, err := io.ReadFull(r, buf); err != nil {
return "", err
}
return string(buf), nil
}
// --- Writing ---
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Mayaqua/Pack.c#L873 (PackWrite)
// Bytes serializes the Pack to binary format.
func (p *Pack) Bytes() ([]byte, error) {
var buf bytes.Buffer
if err := binary.Write(&buf, binary.BigEndian, uint32(len(p.Elements))); err != nil {
return nil, err
}
for _, e := range p.Elements {
if err := writeElement(&buf, e); err != nil {
return nil, err
}
}
return buf.Bytes(), nil
}
func writeElement(w io.Writer, e *Element) error {
if err := writeBufStr(w, e.Name); err != nil {
return err
}
if err := binary.Write(w, binary.BigEndian, e.Type); err != nil {
return err
}
if err := binary.Write(w, binary.BigEndian, uint32(len(e.Values))); err != nil {
return err
}
for _, v := range e.Values {
if err := writeValue(w, &v, e.Type); err != nil {
return err
}
}
return nil
}
func writeValue(w io.Writer, v *Value, typ uint32) error {
switch typ {
case ValueInt:
return binary.Write(w, binary.BigEndian, v.IntValue)
case ValueInt64:
return binary.Write(w, binary.BigEndian, v.Int64Value)
case ValueData:
if err := binary.Write(w, binary.BigEndian, uint32(len(v.Data))); err != nil {
return err
}
_, err := w.Write(v.Data)
return err
case ValueStr:
b := []byte(v.Str)
if err := binary.Write(w, binary.BigEndian, uint32(len(b))); err != nil {
return err
}
_, err := w.Write(b)
return err
case ValueUniStr:
b := append([]byte(v.Str), 0) // UTF-8 with null terminator
if err := binary.Write(w, binary.BigEndian, uint32(len(b))); err != nil {
return err
}
_, err := w.Write(b)
return err
default:
return fmt.Errorf("unknown value type: %d", typ)
}
}
// writeBufStr writes a string in SoftEther's WriteBufStr format.
// Wire format: uint32(strlen+1) + strlen bytes (no null terminator on wire).
func writeBufStr(w io.Writer, s string) error {
b := []byte(s)
if err := binary.Write(w, binary.BigEndian, uint32(len(b)+1)); err != nil {
return err
}
_, err := w.Write(b)
return err
}
// --- Accessors ---
// GetElement finds an element by name (case-insensitive).
func (p *Pack) GetElement(name string) *Element {
upper := strings.ToUpper(name)
for _, e := range p.Elements {
if strings.ToUpper(e.Name) == upper {
return e
}
}
return nil
}
// GetInt returns the first uint32 value of the named element.
func (p *Pack) GetInt(name string) uint32 {
if e := p.GetElement(name); e != nil && e.Type == ValueInt && len(e.Values) > 0 {
return e.Values[0].IntValue
}
return 0
}
// GetInt64 returns the first uint64 value of the named element.
func (p *Pack) GetInt64(name string) uint64 {
if e := p.GetElement(name); e != nil && e.Type == ValueInt64 && len(e.Values) > 0 {
return e.Values[0].Int64Value
}
return 0
}
// GetStr returns the first string value of the named element (STR or UNISTR).
func (p *Pack) GetStr(name string) string {
if e := p.GetElement(name); e != nil && len(e.Values) > 0 {
if e.Type == ValueStr || e.Type == ValueUniStr {
return e.Values[0].Str
}
}
return ""
}
// GetData returns the first data value of the named element.
func (p *Pack) GetData(name string) []byte {
if e := p.GetElement(name); e != nil && e.Type == ValueData && len(e.Values) > 0 {
return e.Values[0].Data
}
return nil
}
// GetBool returns the first integer value as a boolean (non-zero = true).
func (p *Pack) GetBool(name string) bool {
return p.GetInt(name) != 0
}
// GetError returns the "error" field, which holds SoftEther error codes.
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Cedar/Cedar.h#L590
func (p *Pack) GetError() uint32 {
return p.GetInt("error")
}
// --- Mutators ---
// AddInt adds a uint32 element.
func (p *Pack) AddInt(name string, val uint32) {
p.Elements = append(p.Elements, &Element{
Name: name,
Type: ValueInt,
Values: []Value{{IntValue: val}},
})
}
// AddInt64 adds a uint64 element.
func (p *Pack) AddInt64(name string, val uint64) {
p.Elements = append(p.Elements, &Element{
Name: name,
Type: ValueInt64,
Values: []Value{{Int64Value: val}},
})
}
// AddStr adds a string element.
func (p *Pack) AddStr(name string, val string) {
p.Elements = append(p.Elements, &Element{
Name: name,
Type: ValueStr,
Values: []Value{{Str: val}},
})
}
// AddData adds a binary data element.
func (p *Pack) AddData(name string, val []byte) {
p.Elements = append(p.Elements, &Element{
Name: name,
Type: ValueData,
Values: []Value{{Data: val}},
})
}
// AddBool adds a boolean element (stored as uint32: 0 or 1).
func (p *Pack) AddBool(name string, val bool) {
v := uint32(0)
if val {
v = 1
}
p.AddInt(name, v)
}
// AddDummyValue adds the "pencore" random padding element.
// SoftEther servers expect this padding in Pack messages sent via HTTP.
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Mayaqua/HTTP.c#L1165
func (p *Pack) AddDummyValue() {
size := rand.Intn(1000)
buf := make([]byte, size)
rand.Read(buf)
p.AddData("pencore", buf)
}
// AddIP4 adds an IPv4 address with the required companion @ipv6_* elements.
// SoftEther always expects these three companion elements for any IP field.
// See: https://github.com/SoftEtherVPN/SoftEtherVPN/blob/v5.02.5187/src/Mayaqua/Pack.c#L575 (PackAddIp32)
func (p *Pack) AddIP4(name string, ip uint32) {
p.AddBool(name+"@ipv6_bool", false)
p.AddData(name+"@ipv6_array", make([]byte, 16))
p.AddInt(name+"@ipv6_scope_id", 0)
p.AddInt(name, ip)
}