mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-08-01 11:34:16 +03:00
Add OpenVPN, TrustTunnel, Sudoku, inbound managers. Fixes
This commit is contained in:
227
transport/openvpn/cipher.go
Normal file
227
transport/openvpn/cipher.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package openvpn
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"hash"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
const (
|
||||
AESGCMTagSize = 16
|
||||
AESGCMIVSize = 12
|
||||
CBCIVSize = aes.BlockSize
|
||||
)
|
||||
|
||||
type DataCipher interface {
|
||||
Encrypt(header []byte, packetID uint32, payload []byte) ([]byte, error)
|
||||
Decrypt(packet []byte, headerSize int) ([]byte, error)
|
||||
}
|
||||
|
||||
type AEADDataCipher struct {
|
||||
send cipher.AEAD
|
||||
recv cipher.AEAD
|
||||
sendImplicitIV [AESGCMIVSize]byte
|
||||
recvImplicitIV [AESGCMIVSize]byte
|
||||
}
|
||||
|
||||
func NewAEADCipher(keys *KeyMaterial, cipherName string) (*AEADDataCipher, error) {
|
||||
var send, recv cipher.AEAD
|
||||
var err error
|
||||
if cipherName == CipherCHACHA20POLY {
|
||||
send, err = chacha20poly1305.New(keys.SendCipherKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
recv, err = chacha20poly1305.New(keys.RecvCipherKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
sendBlock, err := aes.NewCipher(keys.SendCipherKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
recvBlock, err := aes.NewCipher(keys.RecvCipherKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
send, err = cipher.NewGCMWithTagSize(sendBlock, AESGCMTagSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
recv, err = cipher.NewGCMWithTagSize(recvBlock, AESGCMTagSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(keys.SendHMACKey) < AESGCMIVSize-4 || len(keys.RecvHMACKey) < AESGCMIVSize-4 {
|
||||
return nil, errors.New("openvpn implicit IV keys are too short")
|
||||
}
|
||||
g := &AEADDataCipher{send: send, recv: recv}
|
||||
copy(g.sendImplicitIV[4:], keys.SendHMACKey[:AESGCMIVSize-4])
|
||||
copy(g.recvImplicitIV[4:], keys.RecvHMACKey[:AESGCMIVSize-4])
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func (g *AEADDataCipher) Encrypt(header []byte, packetID uint32, payload []byte) ([]byte, error) {
|
||||
var pidBytes [4]byte
|
||||
binary.BigEndian.PutUint32(pidBytes[:], packetID)
|
||||
nonce := g.nonce(packetID, g.sendImplicitIV)
|
||||
ad := append(header, pidBytes[:]...)
|
||||
sealed := g.send.Seal(nil, nonce[:], payload, ad)
|
||||
out := make([]byte, 0, len(header)+4+len(sealed))
|
||||
out = append(out, header...)
|
||||
out = append(out, pidBytes[:]...)
|
||||
out = append(out, sealed[len(sealed)-AESGCMTagSize:]...)
|
||||
out = append(out, sealed[:len(sealed)-AESGCMTagSize]...)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (g *AEADDataCipher) Decrypt(packet []byte, headerSize int) ([]byte, error) {
|
||||
if len(packet) < headerSize+4+AESGCMTagSize+1 {
|
||||
return nil, errors.New("openvpn gcm data packet too short")
|
||||
}
|
||||
header := packet[:headerSize]
|
||||
pidBytes := packet[headerSize : headerSize+4]
|
||||
tag := packet[headerSize+4 : headerSize+4+AESGCMTagSize]
|
||||
ciphertext := packet[headerSize+4+AESGCMTagSize:]
|
||||
combined := append(ciphertext, tag...)
|
||||
ad := append(header, pidBytes...)
|
||||
nonce := g.nonce(binary.BigEndian.Uint32(pidBytes), g.recvImplicitIV)
|
||||
return g.recv.Open(nil, nonce[:], combined, ad)
|
||||
}
|
||||
|
||||
func (g *AEADDataCipher) nonce(packetID uint32, implicit [AESGCMIVSize]byte) [AESGCMIVSize]byte {
|
||||
nonce := implicit
|
||||
binary.BigEndian.PutUint32(nonce[:4], binary.BigEndian.Uint32(nonce[:4])^packetID)
|
||||
return nonce
|
||||
}
|
||||
|
||||
type CBCDataCipher struct {
|
||||
sendBlock cipher.Block
|
||||
recvBlock cipher.Block
|
||||
sendHMAC []byte
|
||||
recvHMAC []byte
|
||||
newHash func() hash.Hash
|
||||
hmacSize int
|
||||
}
|
||||
|
||||
func NewCBCCipher(keys *KeyMaterial, auth string) (*CBCDataCipher, error) {
|
||||
sendBlock, err := aes.NewCipher(keys.SendCipherKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
recvBlock, err := aes.NewCipher(keys.RecvCipherKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var newHash func() hash.Hash
|
||||
var hmacSize int
|
||||
switch auth {
|
||||
case AuthSHA256:
|
||||
newHash = sha256.New
|
||||
hmacSize = sha256.Size
|
||||
case AuthSHA384:
|
||||
newHash = sha512.New384
|
||||
hmacSize = 48
|
||||
case AuthSHA512:
|
||||
newHash = sha512.New
|
||||
hmacSize = sha512.Size
|
||||
default:
|
||||
newHash = sha1.New
|
||||
hmacSize = sha1.Size
|
||||
}
|
||||
return &CBCDataCipher{
|
||||
sendBlock: sendBlock,
|
||||
recvBlock: recvBlock,
|
||||
sendHMAC: cloneBytes(keys.SendHMACKey[:hmacSize]),
|
||||
recvHMAC: cloneBytes(keys.RecvHMACKey[:hmacSize]),
|
||||
newHash: newHash,
|
||||
hmacSize: hmacSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *CBCDataCipher) Encrypt(header []byte, packetID uint32, payload []byte) ([]byte, error) {
|
||||
var pidBytes [4]byte
|
||||
binary.BigEndian.PutUint32(pidBytes[:], packetID)
|
||||
plain := append(pidBytes[:], payload...)
|
||||
padLen := aes.BlockSize - (len(plain) % aes.BlockSize)
|
||||
for i := 0; i < padLen; i++ {
|
||||
plain = append(plain, byte(padLen))
|
||||
}
|
||||
iv := make([]byte, CBCIVSize)
|
||||
if _, err := rand.Read(iv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ct := make([]byte, len(plain))
|
||||
cipher.NewCBCEncrypter(c.sendBlock, iv).CryptBlocks(ct, plain)
|
||||
mac := hmac.New(c.newHash, c.sendHMAC)
|
||||
mac.Write(iv)
|
||||
mac.Write(ct)
|
||||
tag := mac.Sum(nil)
|
||||
out := make([]byte, 0, len(header)+c.hmacSize+CBCIVSize+len(ct))
|
||||
out = append(out, header...)
|
||||
out = append(out, tag...)
|
||||
out = append(out, iv...)
|
||||
out = append(out, ct...)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *CBCDataCipher) Decrypt(packet []byte, headerSize int) ([]byte, error) {
|
||||
minSize := headerSize + c.hmacSize + CBCIVSize + aes.BlockSize
|
||||
if len(packet) < minSize {
|
||||
return nil, errors.New("openvpn cbc data packet too short")
|
||||
}
|
||||
tag := packet[headerSize : headerSize+c.hmacSize]
|
||||
iv := packet[headerSize+c.hmacSize : headerSize+c.hmacSize+CBCIVSize]
|
||||
ct := packet[headerSize+c.hmacSize+CBCIVSize:]
|
||||
if len(ct)%aes.BlockSize != 0 {
|
||||
return nil, errors.New("openvpn cbc ciphertext not block-aligned")
|
||||
}
|
||||
mac := hmac.New(c.newHash, c.recvHMAC)
|
||||
mac.Write(iv)
|
||||
mac.Write(ct)
|
||||
if !hmac.Equal(tag, mac.Sum(nil)) {
|
||||
return nil, errors.New("openvpn cbc hmac verification failed")
|
||||
}
|
||||
plain := make([]byte, len(ct))
|
||||
cipher.NewCBCDecrypter(c.recvBlock, iv).CryptBlocks(plain, ct)
|
||||
padLen := int(plain[len(plain)-1])
|
||||
if padLen < 1 || padLen > aes.BlockSize {
|
||||
return nil, errors.New("openvpn cbc invalid padding")
|
||||
}
|
||||
plain = plain[:len(plain)-padLen]
|
||||
if len(plain) < 4 {
|
||||
return nil, errors.New("openvpn cbc payload too short")
|
||||
}
|
||||
return plain[4:], nil
|
||||
}
|
||||
|
||||
func CipherKeyLength(cipher string) int {
|
||||
switch cipher {
|
||||
case CipherAES128GCM, CipherAES128CBC:
|
||||
return 16
|
||||
case CipherAES192GCM, CipherAES192CBC:
|
||||
return 24
|
||||
default:
|
||||
return 32
|
||||
}
|
||||
}
|
||||
|
||||
func IsAEAD(cipher string) bool {
|
||||
switch cipher {
|
||||
case CipherAES128GCM, CipherAES192GCM, CipherAES256GCM, CipherCHACHA20POLY:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
280
transport/openvpn/client.go
Normal file
280
transport/openvpn/client.go
Normal file
@@ -0,0 +1,280 @@
|
||||
package openvpn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common/tls"
|
||||
)
|
||||
|
||||
const defaultHandshakeTimeout = 30 * time.Second
|
||||
|
||||
type Client struct {
|
||||
config *ClientConfig
|
||||
tlsConfig tls.Config
|
||||
mux *PacketMux
|
||||
|
||||
control *ControlChannel
|
||||
tlsConn tls.Conn
|
||||
data *DataChannel
|
||||
push *PushReply
|
||||
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewClient(config *ClientConfig, io PacketIO, tlsConfig tls.Config) (*Client, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("nil openvpn client config")
|
||||
}
|
||||
if io == nil {
|
||||
return nil, errors.New("nil openvpn packet io")
|
||||
}
|
||||
if tlsConfig == nil {
|
||||
return nil, errors.New("nil openvpn tls config")
|
||||
}
|
||||
var crypt ControlCrypt
|
||||
var err error
|
||||
if config.TLSAuthKey != nil {
|
||||
crypt, err = NewTLSAuth(config.TLSAuthKey, config.KeyDirection, config.Auth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if config.TLSCryptKey != nil {
|
||||
crypt, err = NewTLSCrypt(config.TLSCryptKey, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
local, err := NewSessionID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
runCtx, cancel := context.WithCancel(context.Background())
|
||||
mux := NewPacketMux(io)
|
||||
go mux.Run(runCtx)
|
||||
return &Client{
|
||||
config: config,
|
||||
tlsConfig: tlsConfig,
|
||||
mux: mux,
|
||||
control: NewControlChannel(mux, crypt, local),
|
||||
cancel: cancel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) Handshake(ctx context.Context) (*PushReply, error) {
|
||||
if c == nil {
|
||||
return nil, errors.New("nil openvpn client")
|
||||
}
|
||||
if _, ok := ctx.Deadline(); !ok {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, defaultHandshakeTimeout)
|
||||
defer cancel()
|
||||
}
|
||||
if c.config.TLSCryptV2WKc != nil {
|
||||
if err := c.sendResetV3(ctx); err != nil {
|
||||
return nil, fmt.Errorf("send hard reset v3: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := c.control.SendReset(ctx); err != nil {
|
||||
return nil, fmt.Errorf("send hard reset: %w", err)
|
||||
}
|
||||
}
|
||||
if err := c.waitServerReset(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
controlConn := NewControlConn(c.control)
|
||||
tlsConn, err := c.tlsConfig.Client(controlConn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("openvpn tls client: %w", err)
|
||||
}
|
||||
c.tlsConn = tlsConn
|
||||
if err := c.tlsConn.HandshakeContext(ctx); err != nil {
|
||||
return nil, fmt.Errorf("openvpn tls handshake: %w", err)
|
||||
}
|
||||
|
||||
clientRecord, err := NewClientKeyMethod2Record(
|
||||
InstallScriptOptionsString(c.config.Proto, c.config.Cipher, c.config.Auth),
|
||||
InstallScriptPeerInfo(c.config.Cipher),
|
||||
strings.TrimSpace(c.config.Username),
|
||||
c.config.Password,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clientBytes, err := clientRecord.MarshalClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := c.tlsConn.Write(clientBytes); err != nil {
|
||||
return nil, fmt.Errorf("write key method 2 client record: %w", err)
|
||||
}
|
||||
serverRecord, err := c.readServerKeyMethod(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sources := clientRecord.Sources
|
||||
sources.Server = serverRecord.Sources.Server
|
||||
keys, err := DeriveClientKeyMaterial(sources, c.control.LocalSessionID(), c.control.RemoteSessionID(), 32)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("derive data channel keys: %w", err)
|
||||
}
|
||||
if _, err := c.tlsConn.Write([]byte(PushRequest + "\x00")); err != nil {
|
||||
return nil, fmt.Errorf("write push request: %w", err)
|
||||
}
|
||||
push, err := c.readPushReply(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.push = push
|
||||
dataCipher := c.config.Cipher
|
||||
if push.Cipher != "" {
|
||||
dataCipher = push.Cipher
|
||||
}
|
||||
if dataCipher == "" {
|
||||
return nil, errors.New("openvpn server did not negotiate a cipher and no cipher configured")
|
||||
}
|
||||
keyLen := CipherKeyLength(dataCipher)
|
||||
keys.SendCipherKey = keys.SendCipherKey[:keyLen]
|
||||
keys.RecvCipherKey = keys.RecvCipherKey[:keyLen]
|
||||
var cipher DataCipher
|
||||
if IsAEAD(dataCipher) {
|
||||
cipher, err = NewAEADCipher(keys, dataCipher)
|
||||
} else {
|
||||
cipher, err = NewCBCCipher(keys, c.config.Auth)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.data = NewDataChannel(cipher, push.PeerID, push.CompLZO)
|
||||
return push, nil
|
||||
}
|
||||
|
||||
func (c *Client) WriteIPPacket(ctx context.Context, packet []byte) error {
|
||||
if c.data == nil {
|
||||
return errors.New("openvpn data channel is not ready")
|
||||
}
|
||||
encrypted, err := c.data.Encrypt(packet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.mux.WritePacket(ctx, encrypted)
|
||||
}
|
||||
|
||||
func (c *Client) ReadIPPacket(ctx context.Context) ([]byte, error) {
|
||||
if c.data == nil {
|
||||
return nil, errors.New("openvpn data channel is not ready")
|
||||
}
|
||||
for {
|
||||
packet, err := c.mux.ReadDataPacket(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
plain, err := c.data.Decrypt(packet)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return plain, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
if c.cancel != nil {
|
||||
c.cancel()
|
||||
}
|
||||
if c.tlsConn != nil {
|
||||
_ = c.tlsConn.Close()
|
||||
}
|
||||
if c.mux != nil {
|
||||
return c.mux.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) waitServerReset(ctx context.Context) error {
|
||||
for {
|
||||
packet, err := c.control.Read(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read hard reset response: %w", err)
|
||||
}
|
||||
switch packet.Opcode {
|
||||
case PControlHardResetServerV2:
|
||||
return c.control.SendAck(ctx)
|
||||
case PControlHardResetServerV1:
|
||||
return fmt.Errorf("openvpn server replied with unsupported key method 1 reset")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) readServerKeyMethod(ctx context.Context) (*KeyMethod2Record, error) {
|
||||
var buf []byte
|
||||
tmp := make([]byte, 4096)
|
||||
for {
|
||||
n, err := c.tlsConn.Read(tmp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read key method 2 server record: %w", err)
|
||||
}
|
||||
buf = append(buf, tmp[:n]...)
|
||||
record, err := ParseServerKeyMethod2Record(buf)
|
||||
if err == nil {
|
||||
return record, nil
|
||||
}
|
||||
if !strings.Contains(err.Error(), "truncated") && !errors.Is(err, ioStringEOF) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) readPushReply(ctx context.Context) (*PushReply, error) {
|
||||
var buf []byte
|
||||
tmp := make([]byte, 4096)
|
||||
for {
|
||||
n, err := c.tlsConn.Read(tmp)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) && len(buf) > 0 {
|
||||
break
|
||||
}
|
||||
return nil, fmt.Errorf("read push reply: %w", err)
|
||||
}
|
||||
buf = append(buf, tmp[:n]...)
|
||||
if bytes.Contains(buf, []byte("\x00")) || strings.Contains(string(buf), "PUSH_REPLY") {
|
||||
msg := string(buf)
|
||||
if idx := strings.IndexByte(msg, 0); idx >= 0 {
|
||||
msg = msg[:idx]
|
||||
}
|
||||
if reply, err := ParsePushReply(msg); err == nil {
|
||||
return reply, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
|
||||
func (c *Client) sendResetV3(ctx context.Context) error {
|
||||
c.control.mu.Lock()
|
||||
messageID := c.control.sendMessage
|
||||
c.control.sendMessage++
|
||||
packet := &ControlPacket{
|
||||
Opcode: PControlHardResetClientV3,
|
||||
KeyID: c.control.keyID,
|
||||
LocalSession: c.control.local,
|
||||
MessageID: messageID,
|
||||
}
|
||||
c.control.pending[messageID] = packet
|
||||
c.control.mu.Unlock()
|
||||
encoded, err := c.control.encodeAndWrap(ctx, packet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
encoded = append(encoded, c.config.TLSCryptV2WKc...)
|
||||
return c.control.io.WritePacket(ctx, encoded)
|
||||
}
|
||||
|
||||
var _ net.Conn = (*ControlConn)(nil)
|
||||
175
transport/openvpn/config.go
Normal file
175
transport/openvpn/config.go
Normal file
@@ -0,0 +1,175 @@
|
||||
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)
|
||||
}
|
||||
475
transport/openvpn/control.go
Normal file
475
transport/openvpn/control.go
Normal file
@@ -0,0 +1,475 @@
|
||||
package openvpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type PacketIO interface {
|
||||
ReadPacket(ctx context.Context) ([]byte, error)
|
||||
WritePacket(ctx context.Context, packet []byte) error
|
||||
Close() error
|
||||
LocalAddr() net.Addr
|
||||
RemoteAddr() net.Addr
|
||||
}
|
||||
|
||||
type ControlChannel struct {
|
||||
io PacketIO
|
||||
encode func(*ControlPacket, uint32, uint32) ([]byte, error)
|
||||
decode func([]byte) (*ControlPacket, uint32, uint32, error)
|
||||
clock func() time.Time
|
||||
keyID uint8
|
||||
local SessionID
|
||||
remote SessionID
|
||||
|
||||
mu sync.Mutex
|
||||
sendPacketID uint32
|
||||
sendMessage uint32
|
||||
ackPending []uint32
|
||||
pending map[uint32]*ControlPacket
|
||||
readDeadline time.Time
|
||||
writeDeadline time.Time
|
||||
}
|
||||
|
||||
func NewControlChannel(io PacketIO, crypt ControlCrypt, local SessionID) *ControlChannel {
|
||||
ch := &ControlChannel{
|
||||
io: io,
|
||||
|
||||
clock: time.Now,
|
||||
local: local,
|
||||
pending: make(map[uint32]*ControlPacket),
|
||||
}
|
||||
if crypt != nil {
|
||||
ch.encode = func(p *ControlPacket, pid uint32, t uint32) ([]byte, error) {
|
||||
return EncodeControlPacketCrypt(*p, crypt, pid, t)
|
||||
}
|
||||
ch.decode = func(pkt []byte) (*ControlPacket, uint32, uint32, error) {
|
||||
return DecodeControlPacketCrypt(crypt, pkt)
|
||||
}
|
||||
} else {
|
||||
ch.encode = func(p *ControlPacket, _ uint32, _ uint32) ([]byte, error) {
|
||||
return EncodeControlPacket(*p)
|
||||
}
|
||||
ch.decode = func(pkt []byte) (*ControlPacket, uint32, uint32, error) {
|
||||
cp, err := DecodeControlPacket(pkt)
|
||||
return cp, 0, 0, err
|
||||
}
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
func (c *ControlChannel) LocalSessionID() SessionID {
|
||||
return c.local
|
||||
}
|
||||
|
||||
func (c *ControlChannel) RemoteSessionID() SessionID {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.remote
|
||||
}
|
||||
|
||||
func (c *ControlChannel) SetRemoteSessionID(id SessionID) {
|
||||
c.mu.Lock()
|
||||
c.remote = id
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *ControlChannel) SendReset(ctx context.Context) error {
|
||||
_, err := c.Send(ctx, PControlHardResetClientV2, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ControlChannel) Send(ctx context.Context, opcode Opcode, payload []byte) (uint32, error) {
|
||||
if !opcode.HasMessageID() {
|
||||
return 0, fmt.Errorf("opcode %s cannot carry a reliable message", opcode)
|
||||
}
|
||||
c.mu.Lock()
|
||||
messageID := c.sendMessage
|
||||
c.sendMessage++
|
||||
packet := &ControlPacket{
|
||||
Opcode: opcode,
|
||||
KeyID: c.keyID,
|
||||
LocalSession: c.local,
|
||||
AckIDs: append([]uint32(nil), c.ackPending...),
|
||||
AckRemoteSession: c.remote,
|
||||
MessageID: messageID,
|
||||
Payload: cloneBytes(payload),
|
||||
}
|
||||
c.ackPending = nil
|
||||
c.pending[messageID] = packet
|
||||
c.mu.Unlock()
|
||||
|
||||
if err := c.writeControlPacket(ctx, packet); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return messageID, nil
|
||||
}
|
||||
|
||||
func (c *ControlChannel) SendAck(ctx context.Context) error {
|
||||
c.mu.Lock()
|
||||
if len(c.ackPending) == 0 {
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
packet := &ControlPacket{
|
||||
Opcode: PAckV1,
|
||||
KeyID: c.keyID,
|
||||
LocalSession: c.local,
|
||||
AckIDs: append([]uint32(nil), c.ackPending...),
|
||||
AckRemoteSession: c.remote,
|
||||
}
|
||||
c.ackPending = nil
|
||||
c.mu.Unlock()
|
||||
return c.writeControlPacket(ctx, packet)
|
||||
}
|
||||
|
||||
func (c *ControlChannel) Read(ctx context.Context) (*ControlPacket, error) {
|
||||
for {
|
||||
packet, err := c.readControlPacket(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.mu.Lock()
|
||||
if c.remote == (SessionID{}) && packet.LocalSession != c.local {
|
||||
c.remote = packet.LocalSession
|
||||
}
|
||||
for _, ackID := range packet.AckIDs {
|
||||
delete(c.pending, ackID)
|
||||
}
|
||||
if packet.Opcode.HasMessageID() {
|
||||
c.ackPending = appendAck(c.ackPending, packet.MessageID)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
if packet.Opcode == PAckV1 {
|
||||
continue
|
||||
}
|
||||
return packet, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ControlChannel) PendingMessages() int {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return len(c.pending)
|
||||
}
|
||||
|
||||
func (c *ControlChannel) RetransmitPending(ctx context.Context) error {
|
||||
c.mu.Lock()
|
||||
packets := make([]*ControlPacket, 0, len(c.pending))
|
||||
for _, packet := range c.pending {
|
||||
cp := *packet
|
||||
cp.AckIDs = append([]uint32(nil), c.ackPending...)
|
||||
cp.AckRemoteSession = c.remote
|
||||
packets = append(packets, &cp)
|
||||
}
|
||||
c.ackPending = nil
|
||||
c.mu.Unlock()
|
||||
for _, packet := range packets {
|
||||
if err := c.writeControlPacket(ctx, packet); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ControlChannel) writeControlPacket(ctx context.Context, packet *ControlPacket) error {
|
||||
c.mu.Lock()
|
||||
c.sendPacketID++
|
||||
packetID := c.sendPacketID
|
||||
unixTime := uint32(c.clock().Unix())
|
||||
deadline := c.writeDeadline
|
||||
c.mu.Unlock()
|
||||
if !deadline.IsZero() {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithDeadline(ctx, deadline)
|
||||
defer cancel()
|
||||
}
|
||||
encoded, err := c.encode(packet, packetID, unixTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.io.WritePacket(ctx, encoded)
|
||||
}
|
||||
|
||||
func (c *ControlChannel) encodeAndWrap(ctx context.Context, packet *ControlPacket) ([]byte, error) {
|
||||
c.mu.Lock()
|
||||
c.sendPacketID++
|
||||
packetID := c.sendPacketID
|
||||
unixTime := uint32(c.clock().Unix())
|
||||
c.mu.Unlock()
|
||||
return c.encode(packet, packetID, unixTime)
|
||||
}
|
||||
|
||||
func (c *ControlChannel) readControlPacket(ctx context.Context) (*ControlPacket, error) {
|
||||
c.mu.Lock()
|
||||
deadline := c.readDeadline
|
||||
c.mu.Unlock()
|
||||
if !deadline.IsZero() {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithDeadline(ctx, deadline)
|
||||
defer cancel()
|
||||
}
|
||||
raw, err := c.io.ReadPacket(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
packet, _, _, err := c.decode(raw)
|
||||
return packet, err
|
||||
}
|
||||
|
||||
func (c *ControlChannel) SetDeadline(t time.Time) error {
|
||||
c.mu.Lock()
|
||||
c.readDeadline = t
|
||||
c.writeDeadline = t
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ControlChannel) SetReadDeadline(t time.Time) error {
|
||||
c.mu.Lock()
|
||||
c.readDeadline = t
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ControlChannel) SetWriteDeadline(t time.Time) error {
|
||||
c.mu.Lock()
|
||||
c.writeDeadline = t
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func appendAck(acks []uint32, ack uint32) []uint32 {
|
||||
for _, existing := range acks {
|
||||
if existing == ack {
|
||||
return acks
|
||||
}
|
||||
}
|
||||
return append(acks, ack)
|
||||
}
|
||||
|
||||
type ControlConn struct {
|
||||
channel *ControlChannel
|
||||
readBuf []byte
|
||||
closed bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewControlConn(channel *ControlChannel) *ControlConn {
|
||||
return &ControlConn{channel: channel}
|
||||
}
|
||||
|
||||
func (c *ControlConn) Read(b []byte) (int, error) {
|
||||
c.mu.Lock()
|
||||
if c.closed {
|
||||
c.mu.Unlock()
|
||||
return 0, net.ErrClosed
|
||||
}
|
||||
if len(c.readBuf) > 0 {
|
||||
n := copy(b, c.readBuf)
|
||||
c.readBuf = c.readBuf[n:]
|
||||
c.mu.Unlock()
|
||||
return n, nil
|
||||
}
|
||||
c.mu.Unlock()
|
||||
for {
|
||||
packet, err := c.channel.Read(context.Background())
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if packet.Opcode != PControlV1 {
|
||||
if err := c.channel.SendAck(context.Background()); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := c.channel.SendAck(context.Background()); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(packet.Payload) == 0 {
|
||||
continue
|
||||
}
|
||||
n := copy(b, packet.Payload)
|
||||
if n < len(packet.Payload) {
|
||||
c.mu.Lock()
|
||||
c.readBuf = append(c.readBuf, packet.Payload[n:]...)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ControlConn) Write(b []byte) (int, error) {
|
||||
c.mu.Lock()
|
||||
if c.closed {
|
||||
c.mu.Unlock()
|
||||
return 0, net.ErrClosed
|
||||
}
|
||||
c.mu.Unlock()
|
||||
if _, err := c.channel.Send(context.Background(), PControlV1, b); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (c *ControlConn) Close() error {
|
||||
c.mu.Lock()
|
||||
if c.closed {
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
c.closed = true
|
||||
c.mu.Unlock()
|
||||
return c.channel.io.Close()
|
||||
}
|
||||
|
||||
func (c *ControlConn) LocalAddr() net.Addr {
|
||||
return c.channel.io.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *ControlConn) RemoteAddr() net.Addr {
|
||||
return c.channel.io.RemoteAddr()
|
||||
}
|
||||
|
||||
func (c *ControlConn) SetDeadline(t time.Time) error {
|
||||
return c.channel.SetDeadline(t)
|
||||
}
|
||||
|
||||
func (c *ControlConn) SetReadDeadline(t time.Time) error {
|
||||
return c.channel.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
func (c *ControlConn) SetWriteDeadline(t time.Time) error {
|
||||
return c.channel.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
type streamPacketIO struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
type datagramPacketIO struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
func NewDatagramPacketIO(conn net.Conn) PacketIO {
|
||||
return &datagramPacketIO{conn: conn}
|
||||
}
|
||||
|
||||
func (d *datagramPacketIO) ReadPacket(ctx context.Context) ([]byte, error) {
|
||||
done := make(chan struct{})
|
||||
var (
|
||||
packet []byte
|
||||
err error
|
||||
)
|
||||
go func() {
|
||||
defer close(done)
|
||||
buf := make([]byte, 64*1024)
|
||||
var n int
|
||||
n, err = d.conn.Read(buf)
|
||||
if err == nil {
|
||||
packet = cloneBytes(buf[:n])
|
||||
}
|
||||
}()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-done:
|
||||
return packet, err
|
||||
}
|
||||
}
|
||||
|
||||
func (d *datagramPacketIO) WritePacket(ctx context.Context, packet []byte) error {
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := d.conn.Write(packet)
|
||||
done <- err
|
||||
}()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case err := <-done:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (d *datagramPacketIO) Close() error {
|
||||
return d.conn.Close()
|
||||
}
|
||||
|
||||
func (d *datagramPacketIO) LocalAddr() net.Addr {
|
||||
return d.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (d *datagramPacketIO) RemoteAddr() net.Addr {
|
||||
return d.conn.RemoteAddr()
|
||||
}
|
||||
|
||||
func NewTCPPacketIO(conn net.Conn) PacketIO {
|
||||
return &streamPacketIO{conn: conn}
|
||||
}
|
||||
|
||||
func (s *streamPacketIO) ReadPacket(ctx context.Context) ([]byte, error) {
|
||||
done := make(chan struct{})
|
||||
var (
|
||||
packet []byte
|
||||
err error
|
||||
)
|
||||
go func() {
|
||||
defer close(done)
|
||||
var lenBuf [2]byte
|
||||
if _, err = io.ReadFull(s.conn, lenBuf[:]); err != nil {
|
||||
return
|
||||
}
|
||||
size := int(lenBuf[0])<<8 | int(lenBuf[1])
|
||||
if size == 0 {
|
||||
err = errors.New("empty openvpn tcp packet")
|
||||
return
|
||||
}
|
||||
packet = make([]byte, size)
|
||||
_, err = io.ReadFull(s.conn, packet)
|
||||
}()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-done:
|
||||
return packet, err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *streamPacketIO) WritePacket(ctx context.Context, packet []byte) error {
|
||||
if len(packet) > 0xffff {
|
||||
return fmt.Errorf("openvpn tcp packet too large: %d", len(packet))
|
||||
}
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
frame := make([]byte, 2+len(packet))
|
||||
frame[0] = byte(len(packet) >> 8)
|
||||
frame[1] = byte(len(packet))
|
||||
copy(frame[2:], packet)
|
||||
_, err := s.conn.Write(frame)
|
||||
done <- err
|
||||
}()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case err := <-done:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *streamPacketIO) Close() error {
|
||||
return s.conn.Close()
|
||||
}
|
||||
|
||||
func (s *streamPacketIO) LocalAddr() net.Addr {
|
||||
return s.conn.LocalAddr()
|
||||
}
|
||||
|
||||
func (s *streamPacketIO) RemoteAddr() net.Addr {
|
||||
return s.conn.RemoteAddr()
|
||||
}
|
||||
91
transport/openvpn/data.go
Normal file
91
transport/openvpn/data.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package openvpn
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
PeerIDUnset uint32 = 0xffffff
|
||||
)
|
||||
|
||||
type DataChannel struct {
|
||||
cipher DataCipher
|
||||
keyID uint8
|
||||
peerID uint32
|
||||
compLZO bool
|
||||
mu sync.Mutex
|
||||
sendPacketID uint32
|
||||
}
|
||||
|
||||
func NewDataChannel(cipher DataCipher, peerID uint32, compLZO bool) *DataChannel {
|
||||
return &DataChannel{
|
||||
cipher: cipher,
|
||||
peerID: peerID,
|
||||
compLZO: compLZO,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DataChannel) Encrypt(packet []byte) ([]byte, error) {
|
||||
if d.compLZO {
|
||||
p := make([]byte, 1+len(packet))
|
||||
p[0] = 0xFA
|
||||
copy(p[1:], packet)
|
||||
packet = p
|
||||
}
|
||||
d.mu.Lock()
|
||||
d.sendPacketID++
|
||||
packetID := d.sendPacketID
|
||||
d.mu.Unlock()
|
||||
return d.cipher.Encrypt(d.dataHeader(), packetID, packet)
|
||||
}
|
||||
|
||||
func (d *DataChannel) Decrypt(packet []byte) ([]byte, error) {
|
||||
if len(packet) < 1 {
|
||||
return nil, errors.New("empty openvpn data packet")
|
||||
}
|
||||
opcode, _ := parseOpcodeKeyID(packet[0])
|
||||
headerSize := 1
|
||||
if opcode == PDataV2 {
|
||||
headerSize = 4
|
||||
}
|
||||
plain, err := d.cipher.Decrypt(packet, headerSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d.compLZO {
|
||||
if len(plain) < 1 {
|
||||
return nil, errors.New("openvpn comp-lzo packet too short")
|
||||
}
|
||||
if plain[0] != 0xFA {
|
||||
return nil, fmt.Errorf("openvpn compressed packet not supported (byte: 0x%02x)", plain[0])
|
||||
}
|
||||
plain = plain[1:]
|
||||
}
|
||||
return plain, nil
|
||||
}
|
||||
|
||||
func (d *DataChannel) dataHeader() []byte {
|
||||
if d.peerID != PeerIDUnset {
|
||||
return []byte{
|
||||
opcodeKeyID(PDataV2, d.keyID),
|
||||
byte(d.peerID >> 16),
|
||||
byte(d.peerID >> 8),
|
||||
byte(d.peerID),
|
||||
}
|
||||
}
|
||||
return []byte{opcodeKeyID(PDataV1, d.keyID)}
|
||||
}
|
||||
|
||||
func ParsePeerID(options string) uint32 {
|
||||
for _, field := range splitPushOptions(options) {
|
||||
if len(field) > len("peer-id ") && field[:len("peer-id ")] == "peer-id " {
|
||||
var id uint32
|
||||
if _, err := fmt.Sscanf(field, "peer-id %d", &id); err == nil && id <= PeerIDUnset {
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
return PeerIDUnset
|
||||
}
|
||||
33
transport/openvpn/device.go
Normal file
33
transport/openvpn/device.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package openvpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
wgTun "github.com/sagernet/wireguard-go/tun"
|
||||
)
|
||||
|
||||
type Device interface {
|
||||
wgTun.Device
|
||||
N.Dialer
|
||||
Start() error
|
||||
}
|
||||
|
||||
type DeviceOptions struct {
|
||||
Context context.Context
|
||||
Logger logger.ContextLogger
|
||||
Handler tun.Handler
|
||||
UDPTimeout time.Duration
|
||||
CreateDialer func(interfaceName string) N.Dialer
|
||||
Name string
|
||||
MTU uint32
|
||||
Address []netip.Prefix
|
||||
}
|
||||
|
||||
func NewDevice(options DeviceOptions) (Device, error) {
|
||||
return newStackDevice(options)
|
||||
}
|
||||
309
transport/openvpn/device_stack.go
Normal file
309
transport/openvpn/device_stack.go
Normal file
@@ -0,0 +1,309 @@
|
||||
//go:build with_gvisor
|
||||
|
||||
package openvpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/gvisor/pkg/buffer"
|
||||
"github.com/sagernet/gvisor/pkg/tcpip"
|
||||
"github.com/sagernet/gvisor/pkg/tcpip/adapters/gonet"
|
||||
"github.com/sagernet/gvisor/pkg/tcpip/header"
|
||||
"github.com/sagernet/gvisor/pkg/tcpip/network/ipv4"
|
||||
"github.com/sagernet/gvisor/pkg/tcpip/network/ipv6"
|
||||
"github.com/sagernet/gvisor/pkg/tcpip/stack"
|
||||
"github.com/sagernet/gvisor/pkg/tcpip/transport/icmp"
|
||||
"github.com/sagernet/gvisor/pkg/tcpip/transport/tcp"
|
||||
"github.com/sagernet/gvisor/pkg/tcpip/transport/udp"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/transport/wireguard"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
wgTun "github.com/sagernet/wireguard-go/tun"
|
||||
)
|
||||
|
||||
type stackDevice struct {
|
||||
ctx context.Context
|
||||
logger log.ContextLogger
|
||||
stack *stack.Stack
|
||||
mtu uint32
|
||||
events chan wgTun.Event
|
||||
wgTun.Device
|
||||
outbound chan *stack.PacketBuffer
|
||||
packetOutbound chan *buf.Buffer
|
||||
done chan struct{}
|
||||
dispatcher stack.NetworkDispatcher
|
||||
inet4Address netip.Addr
|
||||
inet6Address netip.Addr
|
||||
}
|
||||
|
||||
func newStackDevice(options DeviceOptions) (*stackDevice, error) {
|
||||
tunDevice := &stackDevice{
|
||||
ctx: options.Context,
|
||||
logger: options.Logger,
|
||||
mtu: options.MTU,
|
||||
events: make(chan wgTun.Event, 1),
|
||||
outbound: make(chan *stack.PacketBuffer, 256),
|
||||
packetOutbound: make(chan *buf.Buffer, 256),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
ipStack, err := tun.NewGVisorStackWithOptions((*wireEndpoint)(tunDevice), stack.NICOptions{}, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var (
|
||||
inet4Address netip.Addr
|
||||
inet6Address netip.Addr
|
||||
)
|
||||
for _, prefix := range options.Address {
|
||||
addr := tun.AddressFromAddr(prefix.Addr())
|
||||
protoAddr := tcpip.ProtocolAddress{
|
||||
AddressWithPrefix: tcpip.AddressWithPrefix{
|
||||
Address: addr,
|
||||
PrefixLen: prefix.Bits(),
|
||||
},
|
||||
}
|
||||
if prefix.Addr().Is4() {
|
||||
inet4Address = prefix.Addr()
|
||||
tunDevice.inet4Address = inet4Address
|
||||
protoAddr.Protocol = ipv4.ProtocolNumber
|
||||
} else {
|
||||
inet6Address = prefix.Addr()
|
||||
tunDevice.inet6Address = inet6Address
|
||||
protoAddr.Protocol = ipv6.ProtocolNumber
|
||||
}
|
||||
gErr := ipStack.AddProtocolAddress(tun.DefaultNIC, protoAddr, stack.AddressProperties{})
|
||||
if gErr != nil {
|
||||
return nil, E.New("parse local address ", protoAddr.AddressWithPrefix, ": ", gErr.String())
|
||||
}
|
||||
}
|
||||
tunDevice.stack = ipStack
|
||||
if options.Handler != nil {
|
||||
ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(options.Context, ipStack, options.Handler).HandlePacket)
|
||||
ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, tun.NewUDPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout).HandlePacket)
|
||||
icmpForwarder := tun.NewICMPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout)
|
||||
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber4, icmpForwarder.HandlePacket)
|
||||
ipStack.SetTransportProtocolHandler(icmp.ProtocolNumber6, icmpForwarder.HandlePacket)
|
||||
}
|
||||
return tunDevice, nil
|
||||
}
|
||||
|
||||
func (w *stackDevice) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
addr := tcpip.FullAddress{
|
||||
NIC: tun.DefaultNIC,
|
||||
Port: destination.Port,
|
||||
Addr: tun.AddressFromAddr(destination.Addr),
|
||||
}
|
||||
bind := tcpip.FullAddress{
|
||||
NIC: tun.DefaultNIC,
|
||||
}
|
||||
var networkProtocol tcpip.NetworkProtocolNumber
|
||||
if destination.IsIPv4() {
|
||||
if !w.inet4Address.IsValid() {
|
||||
return nil, E.New("missing IPv4 local address")
|
||||
}
|
||||
networkProtocol = header.IPv4ProtocolNumber
|
||||
bind.Addr = tun.AddressFromAddr(w.inet4Address)
|
||||
} else {
|
||||
if !w.inet6Address.IsValid() {
|
||||
return nil, E.New("missing IPv6 local address")
|
||||
}
|
||||
networkProtocol = header.IPv6ProtocolNumber
|
||||
bind.Addr = tun.AddressFromAddr(w.inet6Address)
|
||||
}
|
||||
switch N.NetworkName(network) {
|
||||
case N.NetworkTCP:
|
||||
tcpConn, err := wireguard.DialTCPWithBind(ctx, w.stack, bind, addr, networkProtocol)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tcpConn, nil
|
||||
case N.NetworkUDP:
|
||||
udpConn, err := gonet.DialUDP(w.stack, &bind, &addr, networkProtocol)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return udpConn, nil
|
||||
default:
|
||||
return nil, E.Extend(N.ErrUnknownNetwork, network)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *stackDevice) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
bind := tcpip.FullAddress{
|
||||
NIC: tun.DefaultNIC,
|
||||
}
|
||||
var networkProtocol tcpip.NetworkProtocolNumber
|
||||
if destination.IsIPv4() {
|
||||
networkProtocol = header.IPv4ProtocolNumber
|
||||
bind.Addr = tun.AddressFromAddr(w.inet4Address)
|
||||
} else {
|
||||
networkProtocol = header.IPv6ProtocolNumber
|
||||
bind.Addr = tun.AddressFromAddr(w.inet6Address)
|
||||
}
|
||||
udpConn, err := gonet.DialUDP(w.stack, &bind, nil, networkProtocol)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return udpConn, nil
|
||||
}
|
||||
|
||||
func (w *stackDevice) Start() error {
|
||||
w.events <- wgTun.EventUp
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *stackDevice) File() *os.File {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *stackDevice) Read(bufs [][]byte, sizes []int, offset int) (count int, err error) {
|
||||
select {
|
||||
case packet, ok := <-w.outbound:
|
||||
if !ok {
|
||||
return 0, os.ErrClosed
|
||||
}
|
||||
defer packet.DecRef()
|
||||
var copyN int
|
||||
/*rangeIterate(packet.Data().AsRange(), func(view *buffer.View) {
|
||||
copyN += copy(bufs[0][offset+copyN:], view.AsSlice())
|
||||
})*/
|
||||
for _, view := range packet.AsSlices() {
|
||||
copyN += copy(bufs[0][offset+copyN:], view)
|
||||
}
|
||||
sizes[0] = copyN
|
||||
return 1, nil
|
||||
case packet := <-w.packetOutbound:
|
||||
defer packet.Release()
|
||||
sizes[0] = copy(bufs[0][offset:], packet.Bytes())
|
||||
return 1, nil
|
||||
case <-w.done:
|
||||
return 0, os.ErrClosed
|
||||
}
|
||||
}
|
||||
|
||||
func (w *stackDevice) Write(bufs [][]byte, offset int) (count int, err error) {
|
||||
for _, b := range bufs {
|
||||
b = b[offset:]
|
||||
if len(b) == 0 {
|
||||
continue
|
||||
}
|
||||
var networkProtocol tcpip.NetworkProtocolNumber
|
||||
switch header.IPVersion(b) {
|
||||
case header.IPv4Version:
|
||||
networkProtocol = header.IPv4ProtocolNumber
|
||||
case header.IPv6Version:
|
||||
networkProtocol = header.IPv6ProtocolNumber
|
||||
}
|
||||
packetBuffer := stack.NewPacketBuffer(stack.PacketBufferOptions{
|
||||
Payload: buffer.MakeWithData(b),
|
||||
})
|
||||
w.dispatcher.DeliverNetworkPacket(networkProtocol, packetBuffer)
|
||||
packetBuffer.DecRef()
|
||||
count++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (w *stackDevice) Flush() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *stackDevice) MTU() (int, error) {
|
||||
return int(w.mtu), nil
|
||||
}
|
||||
|
||||
func (w *stackDevice) Name() (string, error) {
|
||||
return "sing-box", nil
|
||||
}
|
||||
|
||||
func (w *stackDevice) Events() <-chan wgTun.Event {
|
||||
return w.events
|
||||
}
|
||||
|
||||
func (w *stackDevice) Close() error {
|
||||
close(w.done)
|
||||
close(w.events)
|
||||
w.stack.Close()
|
||||
for _, endpoint := range w.stack.CleanupEndpoints() {
|
||||
endpoint.Abort()
|
||||
}
|
||||
w.stack.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *stackDevice) BatchSize() int {
|
||||
return 1
|
||||
}
|
||||
|
||||
var _ stack.LinkEndpoint = (*wireEndpoint)(nil)
|
||||
|
||||
type wireEndpoint stackDevice
|
||||
|
||||
func (ep *wireEndpoint) MTU() uint32 {
|
||||
return ep.mtu
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) SetMTU(mtu uint32) {
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) MaxHeaderLength() uint16 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) LinkAddress() tcpip.LinkAddress {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) SetLinkAddress(addr tcpip.LinkAddress) {
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) Capabilities() stack.LinkEndpointCapabilities {
|
||||
return stack.CapabilityRXChecksumOffload
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) Attach(dispatcher stack.NetworkDispatcher) {
|
||||
ep.dispatcher = dispatcher
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) IsAttached() bool {
|
||||
return ep.dispatcher != nil
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) Wait() {
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) ARPHardwareType() header.ARPHardwareType {
|
||||
return header.ARPHardwareNone
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) AddHeader(buffer *stack.PacketBuffer) {
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) ParseHeader(ptr *stack.PacketBuffer) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) WritePackets(list stack.PacketBufferList) (int, tcpip.Error) {
|
||||
for _, packetBuffer := range list.AsSlice() {
|
||||
packetBuffer.IncRef()
|
||||
select {
|
||||
case <-ep.done:
|
||||
return 0, &tcpip.ErrClosedForSend{}
|
||||
case ep.outbound <- packetBuffer:
|
||||
}
|
||||
}
|
||||
return list.Len(), nil
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) Close() {
|
||||
}
|
||||
|
||||
func (ep *wireEndpoint) SetOnCloseAction(f func()) {
|
||||
}
|
||||
13
transport/openvpn/device_stack_stub.go
Normal file
13
transport/openvpn/device_stack_stub.go
Normal file
@@ -0,0 +1,13 @@
|
||||
//go:build !with_gvisor
|
||||
|
||||
package openvpn
|
||||
|
||||
import "github.com/sagernet/sing-tun"
|
||||
|
||||
func newStackDevice(options DeviceOptions) (Device, error) {
|
||||
return nil, tun.ErrGVisorNotIncluded
|
||||
}
|
||||
|
||||
func newSystemStackDevice(options DeviceOptions) (Device, error) {
|
||||
return nil, tun.ErrGVisorNotIncluded
|
||||
}
|
||||
250
transport/openvpn/keymethod.go
Normal file
250
transport/openvpn/keymethod.go
Normal file
@@ -0,0 +1,250 @@
|
||||
package openvpn
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
)
|
||||
|
||||
const (
|
||||
KeyMethod2 = 2
|
||||
|
||||
keySourcePreMasterSize = 48
|
||||
keySourceRandomSize = 32
|
||||
|
||||
maxCipherKeyLength = 64
|
||||
maxHMACKeyLength = 64
|
||||
keyBlockSize = 2 * (maxCipherKeyLength + maxHMACKeyLength)
|
||||
|
||||
keyExpansionID = "OpenVPN"
|
||||
)
|
||||
|
||||
type KeySource struct {
|
||||
PreMaster [keySourcePreMasterSize]byte
|
||||
Random1 [keySourceRandomSize]byte
|
||||
Random2 [keySourceRandomSize]byte
|
||||
}
|
||||
|
||||
type KeySource2 struct {
|
||||
Client KeySource
|
||||
Server KeySource
|
||||
}
|
||||
|
||||
type KeyMaterial struct {
|
||||
SendCipherKey []byte
|
||||
SendHMACKey []byte
|
||||
RecvCipherKey []byte
|
||||
RecvHMACKey []byte
|
||||
}
|
||||
|
||||
type KeyMethod2Record struct {
|
||||
Sources KeySource2
|
||||
Options string
|
||||
Username string
|
||||
Password string
|
||||
PeerInfo string
|
||||
}
|
||||
|
||||
func NewClientKeyMethod2Record(options, peerInfo, username, password string) (*KeyMethod2Record, error) {
|
||||
var record KeyMethod2Record
|
||||
if _, err := rand.Read(record.Sources.Client.PreMaster[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := rand.Read(record.Sources.Client.Random1[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := rand.Read(record.Sources.Client.Random2[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
record.Options = options
|
||||
record.PeerInfo = peerInfo
|
||||
record.Username = username
|
||||
record.Password = password
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
func (r *KeyMethod2Record) MarshalClient() ([]byte, error) {
|
||||
if r == nil {
|
||||
return nil, errors.New("nil key method 2 record")
|
||||
}
|
||||
out := make([]byte, 0, 4+1+keySourcePreMasterSize+keySourceRandomSize*2+len(r.Options)+16)
|
||||
out = binary.BigEndian.AppendUint32(out, 0)
|
||||
out = append(out, KeyMethod2)
|
||||
out = append(out, r.Sources.Client.PreMaster[:]...)
|
||||
out = append(out, r.Sources.Client.Random1[:]...)
|
||||
out = append(out, r.Sources.Client.Random2[:]...)
|
||||
out = appendOpenVPNString(out, r.Options)
|
||||
out = appendOpenVPNString(out, r.Username)
|
||||
out = appendOpenVPNString(out, r.Password)
|
||||
out = appendOpenVPNString(out, r.PeerInfo)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func ParseServerKeyMethod2Record(packet []byte) (*KeyMethod2Record, error) {
|
||||
if len(packet) < 4+1+keySourceRandomSize*2 {
|
||||
return nil, errors.New("key method 2 packet too short")
|
||||
}
|
||||
if binary.BigEndian.Uint32(packet[:4]) != 0 {
|
||||
return nil, errors.New("invalid key method 2 prefix")
|
||||
}
|
||||
if packet[4]&0x0f != KeyMethod2 {
|
||||
return nil, fmt.Errorf("unsupported key method %d", packet[4])
|
||||
}
|
||||
offset := 5
|
||||
record := &KeyMethod2Record{}
|
||||
copy(record.Sources.Server.Random1[:], packet[offset:offset+keySourceRandomSize])
|
||||
offset += keySourceRandomSize
|
||||
copy(record.Sources.Server.Random2[:], packet[offset:offset+keySourceRandomSize])
|
||||
offset += keySourceRandomSize
|
||||
|
||||
var err error
|
||||
record.Options, offset, err = readOpenVPNString(packet, offset)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read options: %w", err)
|
||||
}
|
||||
record.Username, offset, _ = readOpenVPNString(packet, offset)
|
||||
record.Password, offset, _ = readOpenVPNString(packet, offset)
|
||||
record.PeerInfo, _, _ = readOpenVPNString(packet, offset)
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func DeriveClientKeyMaterial(sources KeySource2, clientSession, serverSession SessionID, cipherKeyLen int) (*KeyMaterial, error) {
|
||||
if cipherKeyLen != 16 && cipherKeyLen != 32 {
|
||||
return nil, fmt.Errorf("unsupported data cipher key length %d", cipherKeyLen)
|
||||
}
|
||||
var master [48]byte
|
||||
if err := openvpnPRF(
|
||||
sources.Client.PreMaster[:],
|
||||
keyExpansionID+" master secret",
|
||||
sources.Client.Random1[:],
|
||||
sources.Server.Random1[:],
|
||||
nil,
|
||||
nil,
|
||||
master[:],
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyBlock := make([]byte, keyBlockSize)
|
||||
if err := openvpnPRF(
|
||||
master[:],
|
||||
keyExpansionID+" key expansion",
|
||||
sources.Client.Random2[:],
|
||||
sources.Server.Random2[:],
|
||||
clientSession[:],
|
||||
serverSession[:],
|
||||
keyBlock,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clientToServer := keyBlock[:maxCipherKeyLength+maxHMACKeyLength]
|
||||
serverToClient := keyBlock[maxCipherKeyLength+maxHMACKeyLength:]
|
||||
return &KeyMaterial{
|
||||
SendCipherKey: cloneBytes(clientToServer[:cipherKeyLen]),
|
||||
SendHMACKey: cloneBytes(clientToServer[maxCipherKeyLength : maxCipherKeyLength+maxHMACKeyLength]),
|
||||
RecvCipherKey: cloneBytes(serverToClient[:cipherKeyLen]),
|
||||
RecvHMACKey: cloneBytes(serverToClient[maxCipherKeyLength : maxCipherKeyLength+maxHMACKeyLength]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func InstallScriptOptionsString(proto, cipher, auth string) string {
|
||||
protoName := "UDPv4"
|
||||
if proto == ProtoTCP {
|
||||
protoName = "TCPv4_CLIENT"
|
||||
}
|
||||
keysize := "128"
|
||||
switch cipher {
|
||||
case CipherAES192GCM, CipherAES192CBC:
|
||||
keysize = "192"
|
||||
case CipherAES256GCM, CipherAES256CBC, CipherCHACHA20POLY:
|
||||
keysize = "256"
|
||||
}
|
||||
return fmt.Sprintf("V4,dev-type tun,link-mtu 1550,tun-mtu 1500,proto %s,cipher %s,auth %s,keysize %s,key-method 2,tls-client", protoName, cipher, auth, keysize)
|
||||
}
|
||||
|
||||
func InstallScriptPeerInfo(cipher string) string {
|
||||
if cipher != "" {
|
||||
return "IV_VER=sing-box-openvpn\nIV_PROTO=6\nIV_CIPHERS=" + cipher + "\n"
|
||||
}
|
||||
return "IV_VER=sing-box-openvpn\nIV_PROTO=6\nIV_CIPHERS=AES-256-GCM:AES-192-GCM:AES-128-GCM:AES-256-CBC:AES-192-CBC:AES-128-CBC:CHACHA20-POLY1305\n"
|
||||
}
|
||||
|
||||
func appendOpenVPNString(out []byte, s string) []byte {
|
||||
if s == "" {
|
||||
return binary.BigEndian.AppendUint16(out, 0)
|
||||
}
|
||||
if len(s)+1 > 0xffff {
|
||||
s = s[:0xfffe]
|
||||
}
|
||||
out = binary.BigEndian.AppendUint16(out, uint16(len(s)+1))
|
||||
out = append(out, s...)
|
||||
out = append(out, 0)
|
||||
return out
|
||||
}
|
||||
|
||||
func readOpenVPNString(packet []byte, offset int) (string, int, error) {
|
||||
if offset+2 > len(packet) {
|
||||
return "", offset, ioStringEOF
|
||||
}
|
||||
size := int(binary.BigEndian.Uint16(packet[offset : offset+2]))
|
||||
offset += 2
|
||||
if size == 0 {
|
||||
return "", offset, nil
|
||||
}
|
||||
if offset+size > len(packet) {
|
||||
return "", offset, ioStringEOF
|
||||
}
|
||||
raw := packet[offset : offset+size]
|
||||
offset += size
|
||||
if raw[len(raw)-1] == 0 {
|
||||
raw = raw[:len(raw)-1]
|
||||
}
|
||||
return string(raw), offset, nil
|
||||
}
|
||||
|
||||
var ioStringEOF = errors.New("openvpn string truncated")
|
||||
|
||||
func openvpnPRF(secret []byte, label string, clientSeed, serverSeed, clientSession, serverSession []byte, out []byte) error {
|
||||
seed := make([]byte, 0, len(label)+len(clientSeed)+len(serverSeed)+len(clientSession)+len(serverSession))
|
||||
seed = append(seed, label...)
|
||||
seed = append(seed, clientSeed...)
|
||||
seed = append(seed, serverSeed...)
|
||||
seed = append(seed, clientSession...)
|
||||
seed = append(seed, serverSession...)
|
||||
|
||||
split := (len(secret) + 1) / 2
|
||||
s1 := secret[:split]
|
||||
s2 := secret[len(secret)-split:]
|
||||
|
||||
md5Out := pHash(md5.New, s1, seed, len(out))
|
||||
sha1Out := pHash(sha1.New, s2, seed, len(out))
|
||||
for i := range out {
|
||||
out[i] = md5Out[i] ^ sha1Out[i]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pHash(newHash func() hash.Hash, secret, seed []byte, size int) []byte {
|
||||
out := make([]byte, 0, size)
|
||||
a := hmacSum(newHash, secret, seed)
|
||||
for len(out) < size {
|
||||
chunkInput := make([]byte, 0, len(a)+len(seed))
|
||||
chunkInput = append(chunkInput, a...)
|
||||
chunkInput = append(chunkInput, seed...)
|
||||
out = append(out, hmacSum(newHash, secret, chunkInput)...)
|
||||
a = hmacSum(newHash, secret, a)
|
||||
}
|
||||
return out[:size]
|
||||
}
|
||||
|
||||
func hmacSum(newHash func() hash.Hash, key, data []byte) []byte {
|
||||
mac := hmac.New(newHash, key)
|
||||
_, _ = mac.Write(data)
|
||||
return mac.Sum(nil)
|
||||
}
|
||||
92
transport/openvpn/mux.go
Normal file
92
transport/openvpn/mux.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package openvpn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type PacketMux struct {
|
||||
io PacketIO
|
||||
|
||||
control chan []byte
|
||||
data chan []byte
|
||||
done chan struct{}
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func NewPacketMux(io PacketIO) *PacketMux {
|
||||
return &PacketMux{
|
||||
io: io,
|
||||
control: make(chan []byte, 64),
|
||||
data: make(chan []byte, 256),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *PacketMux) Run(ctx context.Context) {
|
||||
defer m.Close()
|
||||
for ctx.Err() == nil {
|
||||
packet, err := m.io.ReadPacket(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(packet) == 0 {
|
||||
continue
|
||||
}
|
||||
opcode, _ := parseOpcodeKeyID(packet[0])
|
||||
ch := m.data
|
||||
if opcode.IsControl() {
|
||||
ch = m.control
|
||||
}
|
||||
select {
|
||||
case ch <- packet:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-m.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *PacketMux) ReadPacket(ctx context.Context) ([]byte, error) {
|
||||
select {
|
||||
case packet := <-m.control:
|
||||
return packet, nil
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-m.done:
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
}
|
||||
|
||||
func (m *PacketMux) ReadDataPacket(ctx context.Context) ([]byte, error) {
|
||||
select {
|
||||
case packet := <-m.data:
|
||||
return packet, nil
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-m.done:
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
}
|
||||
|
||||
func (m *PacketMux) WritePacket(ctx context.Context, packet []byte) error {
|
||||
return m.io.WritePacket(ctx, packet)
|
||||
}
|
||||
|
||||
func (m *PacketMux) Close() error {
|
||||
m.once.Do(func() {
|
||||
close(m.done)
|
||||
_ = m.io.Close()
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *PacketMux) LocalAddr() net.Addr {
|
||||
return m.io.LocalAddr()
|
||||
}
|
||||
|
||||
func (m *PacketMux) RemoteAddr() net.Addr {
|
||||
return m.io.RemoteAddr()
|
||||
}
|
||||
254
transport/openvpn/packet.go
Normal file
254
transport/openvpn/packet.go
Normal file
@@ -0,0 +1,254 @@
|
||||
package openvpn
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ControlCrypt interface {
|
||||
Wrap(header []byte, packetID uint32, unixTime uint32, plaintext []byte) ([]byte, error)
|
||||
Unwrap(packet []byte) (header []byte, packetID uint32, unixTime uint32, plaintext []byte, err error)
|
||||
}
|
||||
|
||||
const (
|
||||
KeyIDMask = 0x07
|
||||
OpcodeShift = 3
|
||||
|
||||
PControlHardResetClientV1 Opcode = 1
|
||||
PControlHardResetServerV1 Opcode = 2
|
||||
PControlSoftResetV1 Opcode = 3
|
||||
PControlV1 Opcode = 4
|
||||
PAckV1 Opcode = 5
|
||||
PDataV1 Opcode = 6
|
||||
PControlHardResetClientV2 Opcode = 7
|
||||
PControlHardResetServerV2 Opcode = 8
|
||||
PDataV2 Opcode = 9
|
||||
PControlHardResetClientV3 Opcode = 10
|
||||
PControlWKCV1 Opcode = 11
|
||||
|
||||
SessionIDSize = 8
|
||||
)
|
||||
|
||||
type Opcode uint8
|
||||
|
||||
func (o Opcode) String() string {
|
||||
switch o {
|
||||
case PControlHardResetClientV1:
|
||||
return "P_CONTROL_HARD_RESET_CLIENT_V1"
|
||||
case PControlHardResetServerV1:
|
||||
return "P_CONTROL_HARD_RESET_SERVER_V1"
|
||||
case PControlSoftResetV1:
|
||||
return "P_CONTROL_SOFT_RESET_V1"
|
||||
case PControlV1:
|
||||
return "P_CONTROL_V1"
|
||||
case PAckV1:
|
||||
return "P_ACK_V1"
|
||||
case PDataV1:
|
||||
return "P_DATA_V1"
|
||||
case PControlHardResetClientV2:
|
||||
return "P_CONTROL_HARD_RESET_CLIENT_V2"
|
||||
case PControlHardResetServerV2:
|
||||
return "P_CONTROL_HARD_RESET_SERVER_V2"
|
||||
case PDataV2:
|
||||
return "P_DATA_V2"
|
||||
case PControlHardResetClientV3:
|
||||
return "P_CONTROL_HARD_RESET_CLIENT_V3"
|
||||
case PControlWKCV1:
|
||||
return "P_CONTROL_WKC_V1"
|
||||
default:
|
||||
return "P_UNKNOWN"
|
||||
}
|
||||
}
|
||||
|
||||
func (o Opcode) IsControl() bool {
|
||||
switch o {
|
||||
case PControlHardResetClientV1, PControlHardResetServerV1, PControlSoftResetV1, PControlV1,
|
||||
PAckV1, PControlHardResetClientV2, PControlHardResetServerV2, PControlHardResetClientV3, PControlWKCV1:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (o Opcode) HasMessageID() bool {
|
||||
return o.IsControl() && o != PAckV1
|
||||
}
|
||||
|
||||
type SessionID [SessionIDSize]byte
|
||||
|
||||
func NewSessionID() (SessionID, error) {
|
||||
var id SessionID
|
||||
_, err := rand.Read(id[:])
|
||||
return id, err
|
||||
}
|
||||
|
||||
type ControlPacket struct {
|
||||
Opcode Opcode
|
||||
KeyID uint8
|
||||
LocalSession SessionID
|
||||
|
||||
AckIDs []uint32
|
||||
AckRemoteSession SessionID
|
||||
|
||||
MessageID uint32
|
||||
Payload []byte
|
||||
}
|
||||
|
||||
func opcodeKeyID(opcode Opcode, keyID uint8) byte {
|
||||
return byte(opcode)<<OpcodeShift | (keyID & KeyIDMask)
|
||||
}
|
||||
|
||||
func parseOpcodeKeyID(b byte) (Opcode, uint8) {
|
||||
return Opcode(b >> OpcodeShift), b & KeyIDMask
|
||||
}
|
||||
|
||||
func EncodeControlPlain(p ControlPacket) ([]byte, error) {
|
||||
if !p.Opcode.IsControl() {
|
||||
return nil, fmt.Errorf("opcode %s is not a control opcode", p.Opcode)
|
||||
}
|
||||
if len(p.AckIDs) > 255 {
|
||||
return nil, fmt.Errorf("too many ack ids: %d", len(p.AckIDs))
|
||||
}
|
||||
|
||||
size := 1 + len(p.AckIDs)*4
|
||||
if len(p.AckIDs) > 0 {
|
||||
size += SessionIDSize
|
||||
}
|
||||
if p.Opcode.HasMessageID() {
|
||||
size += 4 + len(p.Payload)
|
||||
}
|
||||
out := make([]byte, 0, size)
|
||||
out = append(out, byte(len(p.AckIDs)))
|
||||
for _, id := range p.AckIDs {
|
||||
var b [4]byte
|
||||
binary.BigEndian.PutUint32(b[:], id)
|
||||
out = append(out, b[:]...)
|
||||
}
|
||||
if len(p.AckIDs) > 0 {
|
||||
out = append(out, p.AckRemoteSession[:]...)
|
||||
}
|
||||
if p.Opcode.HasMessageID() {
|
||||
var b [4]byte
|
||||
binary.BigEndian.PutUint32(b[:], p.MessageID)
|
||||
out = append(out, b[:]...)
|
||||
out = append(out, p.Payload...)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func EncodeControlPacket(p ControlPacket) ([]byte, error) {
|
||||
plain, err := EncodeControlPlain(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
header := make([]byte, TLSCryptHeaderSize)
|
||||
header[0] = opcodeKeyID(p.Opcode, p.KeyID)
|
||||
copy(header[1:], p.LocalSession[:])
|
||||
out := make([]byte, 0, len(header)+len(plain))
|
||||
out = append(out, header...)
|
||||
out = append(out, plain...)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func EncodeControlPacketCrypt(p ControlPacket, crypt ControlCrypt, packetID uint32, unixTime uint32) ([]byte, error) {
|
||||
plain, err := EncodeControlPlain(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
header := make([]byte, TLSCryptHeaderSize)
|
||||
header[0] = opcodeKeyID(p.Opcode, p.KeyID)
|
||||
copy(header[1:], p.LocalSession[:])
|
||||
return crypt.Wrap(header, packetID, unixTime, plain)
|
||||
}
|
||||
|
||||
func DecodeControlPacket(packet []byte) (*ControlPacket, error) {
|
||||
if len(packet) < TLSCryptHeaderSize {
|
||||
return nil, errors.New("control packet too short")
|
||||
}
|
||||
opcode, keyID := parseOpcodeKeyID(packet[0])
|
||||
if !opcode.IsControl() {
|
||||
return nil, fmt.Errorf("opcode %s is not a control opcode", opcode)
|
||||
}
|
||||
var local SessionID
|
||||
copy(local[:], packet[1:])
|
||||
plain := packet[TLSCryptHeaderSize:]
|
||||
ackIDs, ackRemote, messageID, payload, err := DecodeControlPlain(opcode, plain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ControlPacket{
|
||||
Opcode: opcode,
|
||||
KeyID: keyID,
|
||||
LocalSession: local,
|
||||
AckIDs: ackIDs,
|
||||
AckRemoteSession: ackRemote,
|
||||
MessageID: messageID,
|
||||
Payload: payload,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func DecodeControlPacketCrypt(crypt ControlCrypt, packet []byte) (*ControlPacket, uint32, uint32, error) {
|
||||
header, packetID, unixTime, plain, err := crypt.Unwrap(packet)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
if len(header) != TLSCryptHeaderSize {
|
||||
return nil, 0, 0, fmt.Errorf("invalid control header length %d", len(header))
|
||||
}
|
||||
opcode, keyID := parseOpcodeKeyID(header[0])
|
||||
if !opcode.IsControl() {
|
||||
return nil, 0, 0, fmt.Errorf("opcode %s is not a control opcode", opcode)
|
||||
}
|
||||
var local SessionID
|
||||
copy(local[:], header[1:])
|
||||
|
||||
ackIDs, ackRemote, messageID, payload, err := DecodeControlPlain(opcode, plain)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
return &ControlPacket{
|
||||
Opcode: opcode,
|
||||
KeyID: keyID,
|
||||
LocalSession: local,
|
||||
AckIDs: ackIDs,
|
||||
AckRemoteSession: ackRemote,
|
||||
MessageID: messageID,
|
||||
Payload: payload,
|
||||
}, packetID, unixTime, nil
|
||||
}
|
||||
|
||||
func DecodeControlPlain(opcode Opcode, plain []byte) (ackIDs []uint32, ackRemote SessionID, messageID uint32, payload []byte, err error) {
|
||||
if len(plain) < 1 {
|
||||
return nil, SessionID{}, 0, nil, errors.New("control payload too short")
|
||||
}
|
||||
ackLen := int(plain[0])
|
||||
offset := 1
|
||||
if len(plain) < offset+ackLen*4 {
|
||||
return nil, SessionID{}, 0, nil, errors.New("control ack array truncated")
|
||||
}
|
||||
ackIDs = make([]uint32, ackLen)
|
||||
for i := 0; i < ackLen; i++ {
|
||||
ackIDs[i] = binary.BigEndian.Uint32(plain[offset : offset+4])
|
||||
offset += 4
|
||||
}
|
||||
if ackLen > 0 {
|
||||
if len(plain) < offset+SessionIDSize {
|
||||
return nil, SessionID{}, 0, nil, errors.New("control ack remote session truncated")
|
||||
}
|
||||
copy(ackRemote[:], plain[offset:offset+SessionIDSize])
|
||||
offset += SessionIDSize
|
||||
}
|
||||
if opcode.HasMessageID() {
|
||||
if len(plain) < offset+4 {
|
||||
return nil, SessionID{}, 0, nil, errors.New("control message id truncated")
|
||||
}
|
||||
messageID = binary.BigEndian.Uint32(plain[offset : offset+4])
|
||||
offset += 4
|
||||
payload = cloneBytes(plain[offset:])
|
||||
} else if len(plain) != offset {
|
||||
return nil, SessionID{}, 0, nil, errors.New("ack packet has trailing payload")
|
||||
}
|
||||
return ackIDs, ackRemote, messageID, payload, nil
|
||||
}
|
||||
139
transport/openvpn/push.go
Normal file
139
transport/openvpn/push.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package openvpn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const PushRequest = "PUSH_REQUEST"
|
||||
|
||||
type PushReply struct {
|
||||
Raw string
|
||||
Prefixes []netip.Prefix
|
||||
DNS []netip.Addr
|
||||
PeerID uint32
|
||||
Cipher string
|
||||
Ping uint32
|
||||
MTU uint32
|
||||
CompLZO bool
|
||||
Redirect bool
|
||||
BlockIPv6 bool
|
||||
}
|
||||
|
||||
func ParsePushReply(message string) (*PushReply, error) {
|
||||
message = strings.TrimRight(message, "\x00")
|
||||
if !strings.HasPrefix(message, "PUSH_REPLY") {
|
||||
return nil, fmt.Errorf("unexpected openvpn push message %q", message)
|
||||
}
|
||||
reply := &PushReply{
|
||||
Raw: message,
|
||||
PeerID: PeerIDUnset,
|
||||
}
|
||||
for _, option := range splitPushOptions(message) {
|
||||
fields := strings.Fields(option)
|
||||
if len(fields) == 0 {
|
||||
continue
|
||||
}
|
||||
switch fields[0] {
|
||||
case "ifconfig":
|
||||
if len(fields) >= 3 {
|
||||
prefix, err := parseIPv4Ifconfig(fields[1], fields[2])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reply.Prefixes = append(reply.Prefixes, prefix)
|
||||
}
|
||||
case "ifconfig-ipv6":
|
||||
if len(fields) >= 2 {
|
||||
prefix, err := netip.ParsePrefix(fields[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse pushed ipv6 address %q: %w", fields[1], err)
|
||||
}
|
||||
reply.Prefixes = append(reply.Prefixes, prefix)
|
||||
}
|
||||
case "dhcp-option":
|
||||
if len(fields) >= 3 && fields[1] == "DNS" {
|
||||
if addr, err := netip.ParseAddr(fields[2]); err == nil {
|
||||
reply.DNS = append(reply.DNS, addr)
|
||||
}
|
||||
}
|
||||
case "peer-id":
|
||||
if len(fields) >= 2 {
|
||||
id, err := strconv.ParseUint(fields[1], 10, 24)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse pushed peer-id %q: %w", fields[1], err)
|
||||
}
|
||||
reply.PeerID = uint32(id)
|
||||
}
|
||||
case "redirect-gateway":
|
||||
reply.Redirect = true
|
||||
case "block-ipv6":
|
||||
reply.BlockIPv6 = true
|
||||
case "cipher":
|
||||
if len(fields) >= 2 {
|
||||
reply.Cipher = fields[1]
|
||||
}
|
||||
case "ping":
|
||||
if len(fields) >= 2 {
|
||||
if v, err := strconv.ParseUint(fields[1], 10, 32); err == nil {
|
||||
reply.Ping = uint32(v)
|
||||
}
|
||||
}
|
||||
case "tun-mtu":
|
||||
if len(fields) >= 2 {
|
||||
if v, err := strconv.ParseUint(fields[1], 10, 32); err == nil {
|
||||
reply.MTU = uint32(v)
|
||||
}
|
||||
}
|
||||
case "comp-lzo":
|
||||
reply.CompLZO = true
|
||||
}
|
||||
}
|
||||
if len(reply.Prefixes) == 0 {
|
||||
return nil, fmt.Errorf("openvpn push reply missing ifconfig address")
|
||||
}
|
||||
return reply, nil
|
||||
}
|
||||
|
||||
func splitPushOptions(message string) []string {
|
||||
message = strings.TrimRight(message, "\x00")
|
||||
parts := strings.Split(message, ",")
|
||||
if len(parts) > 0 && parts[0] == "PUSH_REPLY" {
|
||||
parts = parts[1:]
|
||||
}
|
||||
out := parts[:0]
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part != "" {
|
||||
out = append(out, part)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseIPv4Ifconfig(address, mask string) (netip.Prefix, error) {
|
||||
addr, err := netip.ParseAddr(address)
|
||||
if err != nil {
|
||||
return netip.Prefix{}, fmt.Errorf("parse pushed ipv4 address %q: %w", address, err)
|
||||
}
|
||||
maskAddr, err := netip.ParseAddr(mask)
|
||||
if err != nil {
|
||||
return netip.Prefix{}, fmt.Errorf("parse pushed ipv4 mask %q: %w", mask, err)
|
||||
}
|
||||
if !addr.Is4() || !maskAddr.Is4() {
|
||||
return netip.Prefix{}, fmt.Errorf("openvpn ifconfig requires ipv4 address and mask")
|
||||
}
|
||||
maskBytes := maskAddr.As4()
|
||||
ones := 0
|
||||
for _, b := range maskBytes {
|
||||
for i := 7; i >= 0; i-- {
|
||||
if b&(1<<i) == 0 {
|
||||
return netip.PrefixFrom(addr, ones), nil
|
||||
}
|
||||
ones++
|
||||
}
|
||||
}
|
||||
return netip.PrefixFrom(addr, ones), nil
|
||||
}
|
||||
100
transport/openvpn/tlsauth.go
Normal file
100
transport/openvpn/tlsauth.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package openvpn
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
)
|
||||
|
||||
type TLSAuth struct {
|
||||
sendHMACKey []byte
|
||||
recvHMACKey []byte
|
||||
newHash func() hash.Hash
|
||||
hmacSize int
|
||||
}
|
||||
|
||||
func NewTLSAuth(staticKey []byte, keyDirection int, auth string) (*TLSAuth, error) {
|
||||
if len(staticKey) != staticKeySize {
|
||||
return nil, fmt.Errorf("invalid tls-auth static key length %d, expected %d", len(staticKey), staticKeySize)
|
||||
}
|
||||
key0 := staticKey[:keySlotSize]
|
||||
key1 := staticKey[keySlotSize:]
|
||||
var sendSlot, recvSlot []byte
|
||||
if keyDirection == 1 {
|
||||
sendSlot = key1
|
||||
recvSlot = key0
|
||||
} else {
|
||||
sendSlot = key0
|
||||
recvSlot = key1
|
||||
}
|
||||
var newHash func() hash.Hash
|
||||
var hmacSize int
|
||||
switch auth {
|
||||
case AuthSHA256:
|
||||
newHash = sha256.New
|
||||
hmacSize = sha256.Size
|
||||
case AuthSHA384:
|
||||
newHash = sha512.New384
|
||||
hmacSize = 48
|
||||
case AuthSHA512:
|
||||
newHash = sha512.New
|
||||
hmacSize = sha512.Size
|
||||
default:
|
||||
newHash = sha1.New
|
||||
hmacSize = sha1.Size
|
||||
}
|
||||
return &TLSAuth{
|
||||
sendHMACKey: cloneBytes(sendSlot[64 : 64+hmacSize]),
|
||||
recvHMACKey: cloneBytes(recvSlot[64 : 64+hmacSize]),
|
||||
newHash: newHash,
|
||||
hmacSize: hmacSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *TLSAuth) Wrap(header []byte, packetID uint32, unixTime uint32, plaintext []byte) ([]byte, error) {
|
||||
if len(header) != TLSCryptHeaderSize {
|
||||
return nil, fmt.Errorf("invalid tls-auth header length %d, expected %d", len(header), TLSCryptHeaderSize)
|
||||
}
|
||||
var pid [TLSCryptPIDSize]byte
|
||||
binary.BigEndian.PutUint32(pid[:4], packetID)
|
||||
binary.BigEndian.PutUint32(pid[4:], unixTime)
|
||||
mac := hmac.New(a.newHash, a.sendHMACKey)
|
||||
mac.Write(pid[:])
|
||||
mac.Write(header)
|
||||
mac.Write(plaintext)
|
||||
tag := mac.Sum(nil)
|
||||
out := make([]byte, 0, len(header)+a.hmacSize+TLSCryptPIDSize+len(plaintext))
|
||||
out = append(out, header...)
|
||||
out = append(out, tag...)
|
||||
out = append(out, pid[:]...)
|
||||
out = append(out, plaintext...)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (a *TLSAuth) Unwrap(packet []byte) (header []byte, packetID uint32, unixTime uint32, plaintext []byte, err error) {
|
||||
minLen := TLSCryptHeaderSize + a.hmacSize + TLSCryptPIDSize
|
||||
if len(packet) < minLen {
|
||||
return nil, 0, 0, nil, errors.New("tls-auth packet too short")
|
||||
}
|
||||
header = cloneBytes(packet[:TLSCryptHeaderSize])
|
||||
tag := packet[TLSCryptHeaderSize : TLSCryptHeaderSize+a.hmacSize]
|
||||
pidStart := TLSCryptHeaderSize + a.hmacSize
|
||||
pid := packet[pidStart : pidStart+TLSCryptPIDSize]
|
||||
plaintext = cloneBytes(packet[pidStart+TLSCryptPIDSize:])
|
||||
mac := hmac.New(a.newHash, a.recvHMACKey)
|
||||
mac.Write(pid)
|
||||
mac.Write(header)
|
||||
mac.Write(plaintext)
|
||||
tagCheck := mac.Sum(nil)
|
||||
if !hmac.Equal(tag, tagCheck) {
|
||||
return nil, 0, 0, nil, errors.New("tls-auth authentication failed")
|
||||
}
|
||||
packetID = binary.BigEndian.Uint32(pid[:4])
|
||||
unixTime = binary.BigEndian.Uint32(pid[4:])
|
||||
return header, packetID, unixTime, plaintext, nil
|
||||
}
|
||||
128
transport/openvpn/tlscrypt.go
Normal file
128
transport/openvpn/tlscrypt.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package openvpn
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
TLSCryptHeaderSize = 1 + 8
|
||||
TLSCryptPIDSize = 4 + 4
|
||||
TLSCryptTagSize = sha256.Size
|
||||
|
||||
staticKeySize = 256
|
||||
keySlotSize = 128
|
||||
cipherKeySize = 32
|
||||
hmacKeySize = 32
|
||||
)
|
||||
|
||||
type TLSCrypt struct {
|
||||
encryptCipherKey []byte
|
||||
encryptHMACKey []byte
|
||||
decryptCipherKey []byte
|
||||
decryptHMACKey []byte
|
||||
}
|
||||
|
||||
func NewTLSCrypt(staticKey []byte, client bool) (*TLSCrypt, error) {
|
||||
if len(staticKey) != staticKeySize {
|
||||
return nil, fmt.Errorf("invalid tls-crypt static key length %d, expected %d", len(staticKey), staticKeySize)
|
||||
}
|
||||
|
||||
key0 := staticKey[:keySlotSize]
|
||||
key1 := staticKey[keySlotSize:]
|
||||
|
||||
encrypt := key0
|
||||
decrypt := key1
|
||||
if client {
|
||||
encrypt = key1
|
||||
decrypt = key0
|
||||
}
|
||||
|
||||
return &TLSCrypt{
|
||||
encryptCipherKey: cloneBytes(encrypt[:cipherKeySize]),
|
||||
encryptHMACKey: cloneBytes(encrypt[64 : 64+hmacKeySize]),
|
||||
decryptCipherKey: cloneBytes(decrypt[:cipherKeySize]),
|
||||
decryptHMACKey: cloneBytes(decrypt[64 : 64+hmacKeySize]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *TLSCrypt) Wrap(header []byte, packetID uint32, unixTime uint32, plaintext []byte) ([]byte, error) {
|
||||
if len(header) != TLSCryptHeaderSize {
|
||||
return nil, fmt.Errorf("invalid tls-crypt header length %d, expected %d", len(header), TLSCryptHeaderSize)
|
||||
}
|
||||
|
||||
ad := make([]byte, 0, len(header)+TLSCryptPIDSize)
|
||||
ad = append(ad, header...)
|
||||
var pid [TLSCryptPIDSize]byte
|
||||
binary.BigEndian.PutUint32(pid[:4], packetID)
|
||||
binary.BigEndian.PutUint32(pid[4:], unixTime)
|
||||
ad = append(ad, pid[:]...)
|
||||
|
||||
tag := c.hmac(c.encryptHMACKey, ad, plaintext)
|
||||
ciphertext, err := aes256ctr(c.encryptCipherKey, tag[:aes.BlockSize], plaintext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make([]byte, 0, len(ad)+len(tag)+len(ciphertext))
|
||||
out = append(out, ad...)
|
||||
out = append(out, tag...)
|
||||
out = append(out, ciphertext...)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *TLSCrypt) Unwrap(packet []byte) (header []byte, packetID uint32, unixTime uint32, plaintext []byte, err error) {
|
||||
if len(packet) < TLSCryptHeaderSize+TLSCryptPIDSize+TLSCryptTagSize {
|
||||
return nil, 0, 0, nil, errors.New("tls-crypt packet too short")
|
||||
}
|
||||
|
||||
header = cloneBytes(packet[:TLSCryptHeaderSize])
|
||||
adEnd := TLSCryptHeaderSize + TLSCryptPIDSize
|
||||
tagEnd := adEnd + TLSCryptTagSize
|
||||
ad := packet[:adEnd]
|
||||
tag := packet[adEnd:tagEnd]
|
||||
ciphertext := packet[tagEnd:]
|
||||
|
||||
plaintext, err = aes256ctr(c.decryptCipherKey, tag[:aes.BlockSize], ciphertext)
|
||||
if err != nil {
|
||||
return nil, 0, 0, nil, err
|
||||
}
|
||||
|
||||
tagCheck := c.hmac(c.decryptHMACKey, ad, plaintext)
|
||||
if !hmac.Equal(tag, tagCheck) {
|
||||
return nil, 0, 0, nil, errors.New("tls-crypt authentication failed")
|
||||
}
|
||||
|
||||
packetID = binary.BigEndian.Uint32(packet[TLSCryptHeaderSize : TLSCryptHeaderSize+4])
|
||||
unixTime = binary.BigEndian.Uint32(packet[TLSCryptHeaderSize+4 : adEnd])
|
||||
return header, packetID, unixTime, plaintext, nil
|
||||
}
|
||||
|
||||
func (c *TLSCrypt) hmac(key []byte, parts ...[]byte) []byte {
|
||||
mac := hmac.New(sha256.New, key)
|
||||
for _, part := range parts {
|
||||
_, _ = mac.Write(part)
|
||||
}
|
||||
return mac.Sum(nil)
|
||||
}
|
||||
|
||||
func aes256ctr(key, iv, in []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := cloneBytes(in)
|
||||
cipher.NewCTR(block, iv).XORKeyStream(out, out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func cloneBytes(in []byte) []byte {
|
||||
out := make([]byte, len(in))
|
||||
copy(out, in)
|
||||
return out
|
||||
}
|
||||
284
transport/openvpn/tunnel.go
Normal file
284
transport/openvpn/tunnel.go
Normal file
@@ -0,0 +1,284 @@
|
||||
package openvpn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/tls"
|
||||
)
|
||||
|
||||
type TunnelOptions struct {
|
||||
Dialer N.Dialer
|
||||
Servers []option.ServerOptions
|
||||
TLSConfig tls.Config
|
||||
Config *ClientConfig
|
||||
UDPTimeout time.Duration
|
||||
ReconnectDelay time.Duration
|
||||
PingInterval time.Duration
|
||||
}
|
||||
|
||||
type Tunnel struct {
|
||||
ctx context.Context
|
||||
logger logger.ContextLogger
|
||||
options TunnelOptions
|
||||
device Device
|
||||
client *Client
|
||||
mtu uint32
|
||||
serverIndex int
|
||||
|
||||
await chan struct{}
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewTunnel(ctx context.Context, logger logger.ContextLogger, options TunnelOptions) (*Tunnel, error) {
|
||||
if options.ReconnectDelay == 0 {
|
||||
options.ReconnectDelay = 5 * time.Second
|
||||
}
|
||||
return &Tunnel{
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
options: options,
|
||||
await: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *Tunnel) Start() error {
|
||||
go func() {
|
||||
defer close(t.await)
|
||||
client, err := t.getClient()
|
||||
if err != nil {
|
||||
t.logger.Error("OpenVPN connect: ", err)
|
||||
return
|
||||
}
|
||||
t.mtu = 1500
|
||||
if client.push.MTU > 0 {
|
||||
t.mtu = client.push.MTU
|
||||
}
|
||||
deviceOptions := DeviceOptions{
|
||||
Context: t.ctx,
|
||||
Logger: t.logger,
|
||||
UDPTimeout: t.options.UDPTimeout,
|
||||
MTU: t.mtu,
|
||||
Address: client.push.Prefixes,
|
||||
}
|
||||
device, err := NewDevice(deviceOptions)
|
||||
if err != nil {
|
||||
client.Close()
|
||||
t.logger.Error("create OpenVPN device: ", err)
|
||||
return
|
||||
}
|
||||
t.device = device
|
||||
if err := device.Start(); err != nil {
|
||||
client.Close()
|
||||
t.logger.Error("start OpenVPN device: ", err)
|
||||
return
|
||||
}
|
||||
t.maintainTunnel()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Tunnel) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
if !destination.Addr.IsValid() {
|
||||
return nil, E.Cause(os.ErrInvalid, "invalid non-IP destination")
|
||||
}
|
||||
return t.device.DialContext(ctx, network, destination)
|
||||
}
|
||||
|
||||
func (t *Tunnel) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||
if !destination.Addr.IsValid() {
|
||||
return nil, E.Cause(os.ErrInvalid, "invalid non-IP destination")
|
||||
}
|
||||
return t.device.ListenPacket(ctx, destination)
|
||||
}
|
||||
|
||||
func (t *Tunnel) Close() error {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if t.client != nil {
|
||||
t.client.Close()
|
||||
t.client = nil
|
||||
}
|
||||
if t.device != nil {
|
||||
return t.device.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Tunnel) isTunnelInitialized(ctx context.Context) error {
|
||||
select {
|
||||
case <-t.await:
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
if t.device == nil {
|
||||
return E.New("endpoint not initialized")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Tunnel) maintainTunnel() {
|
||||
go func() {
|
||||
bufs := make([][]byte, 1)
|
||||
bufs[0] = make([]byte, t.mtu)
|
||||
sizes := make([]int, 1)
|
||||
for t.ctx.Err() == nil {
|
||||
_, err := t.device.Read(bufs, sizes, 0)
|
||||
if err != nil {
|
||||
if t.ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
client, err := t.getClient()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := client.WriteIPPacket(t.ctx, bufs[0][:sizes[0]]); err != nil {
|
||||
if t.ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
for t.ctx.Err() == nil {
|
||||
client, err := t.getClient()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
packet, err := client.ReadIPPacket(t.ctx)
|
||||
if err != nil {
|
||||
if t.ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
if ok := t.closeClient(client); ok {
|
||||
t.logger.ErrorContext(t.ctx, fmt.Errorf("connection lost: %v", err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
if bytes.Equal(packet, pingPayload) {
|
||||
continue
|
||||
}
|
||||
if _, err := t.device.Write([][]byte{packet}, 0); err != nil {
|
||||
if t.ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
pingInterval := t.options.PingInterval
|
||||
if pingInterval == 0 && t.client != nil && t.client.push.Ping > 0 {
|
||||
pingInterval = time.Duration(t.client.push.Ping) * time.Second
|
||||
}
|
||||
if pingInterval > 0 {
|
||||
go func() {
|
||||
ticker := time.NewTicker(pingInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-t.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
client, err := t.getClient()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
client.WriteIPPacket(t.ctx, pingPayload)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
<-t.ctx.Done()
|
||||
}
|
||||
|
||||
func (t *Tunnel) getClient() (*Client, error) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if t.ctx.Err() != nil {
|
||||
return nil, t.ctx.Err()
|
||||
}
|
||||
if t.client != nil {
|
||||
return t.client, nil
|
||||
}
|
||||
timer := time.NewTimer(0)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
t.logger.InfoContext(t.ctx, "connecting to OpenVPN server")
|
||||
client, err := t.connect()
|
||||
if err != nil {
|
||||
t.logger.ErrorContext(t.ctx, fmt.Errorf("connect failed: %v", err))
|
||||
timer.Reset(t.options.ReconnectDelay)
|
||||
select {
|
||||
case <-t.ctx.Done():
|
||||
return nil, t.ctx.Err()
|
||||
case <-timer.C:
|
||||
}
|
||||
continue
|
||||
}
|
||||
t.client = client
|
||||
t.logger.InfoContext(t.ctx, "connected to OpenVPN server")
|
||||
return client, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tunnel) closeClient(client *Client) bool {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if client == t.client {
|
||||
t.client.Close()
|
||||
t.client = nil
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Tunnel) connect() (*Client, error) {
|
||||
config := t.options.Config
|
||||
server := t.options.Servers[t.serverIndex].Build()
|
||||
t.serverIndex = (t.serverIndex + 1) % len(t.options.Servers)
|
||||
connectCtx, cancel := context.WithTimeout(t.ctx, t.options.ReconnectDelay)
|
||||
defer cancel()
|
||||
var conn net.Conn
|
||||
var err error
|
||||
if config.Proto == ProtoTCP {
|
||||
conn, err = t.options.Dialer.DialContext(connectCtx, N.NetworkTCP, server)
|
||||
} else {
|
||||
conn, err = t.options.Dialer.DialContext(connectCtx, N.NetworkUDP, server)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dial openvpn server: %w", err)
|
||||
}
|
||||
var packetIO PacketIO
|
||||
if config.Proto == ProtoTCP {
|
||||
packetIO = NewTCPPacketIO(conn)
|
||||
} else {
|
||||
packetIO = NewDatagramPacketIO(conn)
|
||||
}
|
||||
client, err := NewClient(config, packetIO, t.options.TLSConfig)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
_, err = client.Handshake(connectCtx)
|
||||
if err != nil {
|
||||
client.Close()
|
||||
return nil, fmt.Errorf("openvpn handshake: %w", err)
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
var pingPayload = []byte{
|
||||
0x2a, 0x18, 0x7b, 0xf3, 0x64, 0x1e, 0xb4, 0xcb,
|
||||
0x07, 0xed, 0x2d, 0x0a, 0x98, 0x1f, 0xc7, 0x48,
|
||||
}
|
||||
Reference in New Issue
Block a user