// 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) }