Files
sing-box-extended/transport/openvpn/config.go

176 lines
4.1 KiB
Go

package openvpn
import (
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"strings"
)
const (
ProtoUDP = "udp"
ProtoTCP = "tcp"
CipherAES128GCM = "AES-128-GCM"
CipherAES192GCM = "AES-192-GCM"
CipherAES256GCM = "AES-256-GCM"
CipherAES128CBC = "AES-128-CBC"
CipherAES192CBC = "AES-192-CBC"
CipherAES256CBC = "AES-256-CBC"
CipherCHACHA20POLY = "CHACHA20-POLY1305"
AuthSHA1 = "SHA1"
AuthSHA256 = "SHA256"
AuthSHA384 = "SHA384"
AuthSHA512 = "SHA512"
)
type ClientConfig struct {
Proto string
Cipher string
Auth string
Username string
Password string
KeyDirection int
TLSCrypt []byte
TLSCryptV2 bool
TLSCryptKey []byte
TLSCryptV2WKc []byte
TLSAuthKey []byte
}
func (c *ClientConfig) Prepare() error {
if c == nil {
return errors.New("nil openvpn client config")
}
c.Proto = normalizeProto(c.Proto)
c.Cipher = strings.ToUpper(strings.TrimSpace(c.Cipher))
if c.Auth == "" {
c.Auth = AuthSHA1
}
c.Auth = strings.ToUpper(strings.TrimSpace(c.Auth))
if c.Proto != ProtoUDP && c.Proto != ProtoTCP {
return fmt.Errorf("unsupported openvpn proto %q: only udp and tcp are supported", c.Proto)
}
if c.Cipher != "" && !isValidCipher(c.Cipher) {
return fmt.Errorf("unsupported openvpn cipher %q", c.Cipher)
}
if !isValidAuth(c.Auth) {
return fmt.Errorf("unsupported openvpn auth %q", c.Auth)
}
if c.TLSCryptV2 {
kc, wkc, err := decodeTLSCryptV2Key(c.TLSCrypt)
if err != nil {
return fmt.Errorf("parse tls-crypt-v2 key: %w", err)
}
c.TLSCryptKey = kc
c.TLSCryptV2WKc = wkc
return nil
}
if len(strings.TrimSpace(string(c.TLSCrypt))) == 0 {
return nil
}
key, err := decodeStaticKey(c.TLSCrypt)
if err != nil {
return fmt.Errorf("parse tls key: %w", err)
}
if c.KeyDirection >= 0 {
c.TLSAuthKey = key
} else {
c.TLSCryptKey = key
}
return nil
}
func normalizeProto(proto string) string {
switch strings.ToLower(strings.TrimSpace(proto)) {
case "", "udp", "udp4":
return ProtoUDP
case "tcp", "tcp-client", "tcp4", "tcp4-client":
return ProtoTCP
default:
return strings.ToLower(strings.TrimSpace(proto))
}
}
func isValidCipher(cipher string) bool {
switch cipher {
case CipherAES128GCM, CipherAES192GCM, CipherAES256GCM,
CipherAES128CBC, CipherAES192CBC, CipherAES256CBC,
CipherCHACHA20POLY:
return true
}
return false
}
func isValidAuth(auth string) bool {
switch auth {
case AuthSHA1, AuthSHA256, AuthSHA384, AuthSHA512:
return true
}
return false
}
func decodeStaticKey(block []byte) ([]byte, error) {
var hexLines []string
for _, raw := range strings.Split(string(block), "\n") {
line := strings.TrimSpace(raw)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if strings.HasPrefix(line, "-----BEGIN OpenVPN Static key") || strings.HasPrefix(line, "-----END OpenVPN Static key") {
continue
}
hexLines = append(hexLines, line)
}
encoded := strings.Join(hexLines, "")
key, err := hex.DecodeString(encoded)
if err != nil {
return nil, err
}
if len(key) != 256 {
return nil, fmt.Errorf("invalid static key length %d, expected 256 bytes", len(key))
}
return key, nil
}
func decodeTLSCryptV2Key(block []byte) (kc []byte, wkc []byte, err error) {
data, err := decodePEM(block, "OpenVPN tls-crypt-v2 client key")
if err != nil {
return nil, nil, err
}
if len(data) < 256 {
return nil, nil, fmt.Errorf("tls-crypt-v2 key too short: %d bytes", len(data))
}
return data[:256], data[256:], nil
}
func decodePEM(block []byte, expectedHeader string) ([]byte, error) {
lines := strings.Split(string(block), "\n")
var b64 strings.Builder
inBlock := false
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.Contains(line, "BEGIN") && strings.Contains(line, expectedHeader) {
inBlock = true
continue
}
if strings.Contains(line, "END") && strings.Contains(line, expectedHeader) {
break
}
if inBlock {
b64.WriteString(line)
}
}
if b64.Len() == 0 {
return nil, fmt.Errorf("no %s block found", expectedHeader)
}
return base64Decode(b64.String())
}
func base64Decode(s string) ([]byte, error) {
return base64.StdEncoding.DecodeString(s)
}