Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 38 additions & 22 deletions adapter/outbound/openvpn.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,30 +42,44 @@ type OpenVPN struct {

type OpenVPNOption struct {
BasicOption
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Proto string `proxy:"proto,omitempty"`
Dev string `proxy:"dev,omitempty"`
Cipher string `proxy:"cipher,omitempty"`
Auth string `proxy:"auth,omitempty"`
CompLZO string `proxy:"comp-lzo,omitempty"`
CA string `proxy:"ca"`
Cert string `proxy:"cert,omitempty"`
Key string `proxy:"key,omitempty"`
TLSCrypt string `proxy:"tls-crypt,omitempty"`
Username string `proxy:"username,omitempty"`
Password string `proxy:"password,omitempty"`
Ping int `proxy:"ping,omitempty"`
PingRestart int `proxy:"ping-restart,omitempty"`
MTU int `proxy:"mtu,omitempty"`
UDP bool `proxy:"udp,omitempty"`
Name string `proxy:"name"`
Server string `proxy:"server"`
Port int `proxy:"port"`
Proto string `proxy:"proto,omitempty"`
Dev string `proxy:"dev,omitempty"`
Cipher string `proxy:"cipher,omitempty"`
Auth string `proxy:"auth,omitempty"`
CompLZO string `proxy:"comp-lzo,omitempty"`
CA string `proxy:"ca"`
Cert string `proxy:"cert,omitempty"`
Key string `proxy:"key,omitempty"`
TLSCrypt string `proxy:"tls-crypt,omitempty"`
TLSAuth string `proxy:"tls-auth,omitempty"`
KeyDirection *int `proxy:"key-direction,omitempty"`
Scramble string `proxy:"scramble,omitempty"`
Username string `proxy:"username,omitempty"`
Password string `proxy:"password,omitempty"`
Ping int `proxy:"ping,omitempty"`
PingRestart int `proxy:"ping-restart,omitempty"`
MTU int `proxy:"mtu,omitempty"`
UDP bool `proxy:"udp,omitempty"`

RemoteDnsResolve bool `proxy:"remote-dns-resolve,omitempty"`
Dns []string `proxy:"dns,omitempty"`
}

func NewOpenVPN(option OpenVPNOption) (*OpenVPN, error) {
keyDirection := ovpn.KeyDirectionBidirectional
if option.KeyDirection != nil {
switch *option.KeyDirection {
case 0:
keyDirection = ovpn.KeyDirectionNormal
case 1:
keyDirection = ovpn.KeyDirectionInverse
default:
return nil, fmt.Errorf("unsupported openvpn key-direction %d", *option.KeyDirection)
}
}
cfg := &ovpn.ClientConfig{
RemoteHost: option.Server,
RemotePort: uint16(option.Port),
Expand All @@ -78,6 +92,9 @@ func NewOpenVPN(option OpenVPNOption) (*OpenVPN, error) {
Cert: []byte(option.Cert),
Key: []byte(option.Key),
TLSCrypt: []byte(option.TLSCrypt),
TLSAuth: []byte(option.TLSAuth),
KeyDirection: keyDirection,
ScrambleRaw: option.Scramble,
Username: option.Username,
Password: option.Password,
PingInterval: time.Duration(option.Ping) * time.Second,
Expand All @@ -86,7 +103,6 @@ func NewOpenVPN(option OpenVPNOption) (*OpenVPN, error) {
if err := cfg.Prepare(); err != nil {
return nil, err
}

outbound := &OpenVPN{
Base: NewBase(BaseOption{
Name: option.Name,
Expand Down Expand Up @@ -239,6 +255,7 @@ func (o *OpenVPN) run(ctx context.Context) (wireguard.Device, resolver.Resolver,
}
return nil, nil, E.Cause(err, "connect OpenVPN server")
}
log.Debugln("[OpenVPN](%s) connected %s over %s", o.name, o.addr, o.config.Proto)
client, err := ovpn.NewClient(o.config, packetIO)
if err != nil {
_ = packetIO.Close()
Expand Down Expand Up @@ -333,13 +350,13 @@ func (o *OpenVPN) openPacketIO(ctx context.Context) (ovpn.PacketIO, error) {
if err != nil {
return nil, err
}
return ovpn.NewDatagramPacketIO(conn), nil
return ovpn.NewScramblePacketIO(ovpn.NewDatagramPacketIO(conn), o.config.Scramble), nil
case ovpn.ProtoTCP:
conn, err := o.dialer.DialContext(ctx, "tcp", o.addr)
if err != nil {
return nil, err
}
return ovpn.NewTCPPacketIO(conn), nil
return ovpn.NewScramblePacketIO(ovpn.NewTCPPacketIO(conn), o.config.Scramble), nil
default:
return nil, fmt.Errorf("unsupported openvpn proto %q", o.config.Proto)
}
Expand Down Expand Up @@ -422,7 +439,6 @@ func (o *OpenVPN) startPacketLoops() {
}
return
}
log.Debugln("[OpenVPN](%s) sent ping packet after %s idle", o.name, sinceSend.Round(time.Second))
}
case <-runCtx.Done():
return
Expand Down
81 changes: 44 additions & 37 deletions docs/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,13 @@ external-doh-server: /dns-query

# routing-mark:6666 # 配置 fwmark 仅用于 Linux
experimental:
# Disable quic-go GSO support. This may result in reduced performance on Linux.
# This is not recommended for most users.
# Only users encountering issues with quic-go's internal implementation should enable this,
# and they should disable it as soon as the issue is resolved.
# This field will be removed when quic-go fixes all their issues in GSO.
# This equivalent to the environment variable QUIC_GO_DISABLE_GSO=1.
#quic-go-disable-gso: true
# Disable quic-go GSO support. This may result in reduced performance on Linux.
# This is not recommended for most users.
# Only users encountering issues with quic-go's internal implementation should enable this,
# and they should disable it as soon as the issue is resolved.
# This field will be removed when quic-go fixes all their issues in GSO.
# This equivalent to the environment variable QUIC_GO_DISABLE_GSO=1.
#quic-go-disable-gso: true

# 类似于 /etc/hosts, 仅支持配置单个 IP
hosts:
Expand Down Expand Up @@ -579,12 +579,12 @@ proxies: # socks5
password: [YOUR_SS_PASSWORD]
client-fingerprint:
chrome # One of: chrome, ios, firefox or safari
# 可以是 chrome, ios, firefox, safari 中的一个
# 可以是 chrome, ios, firefox, safari 中的一个
plugin: restls
plugin-opts:
host:
"www.microsoft.com" # Must be a TLS 1.3 server
# 应当是一个 TLS 1.3 服务器
# 应当是一个 TLS 1.3 服务器
password: [YOUR_RESTLS_PASSWORD]
version-hint: "tls13"
# Control your post-handshake traffic through restls-script
Expand All @@ -602,12 +602,12 @@ proxies: # socks5
password: [YOUR_SS_PASSWORD]
client-fingerprint:
chrome # One of: chrome, ios, firefox or safari
# 可以是 chrome, ios, firefox, safari 中的一个
# 可以是 chrome, ios, firefox, safari 中的一个
plugin: restls
plugin-opts:
host:
"vscode.dev" # Must be a TLS 1.2 server
# 应当是一个 TLS 1.2 服务器
# 应当是一个 TLS 1.2 服务器
password: [YOUR_RESTLS_PASSWORD]
version-hint: "tls12"
restls-script: "1000?100<1,500~100,350~100,600~100,400~200"
Expand Down Expand Up @@ -654,29 +654,29 @@ proxies: # socks5
uuid: uuid
alterId: 32
cipher: auto
# udp: true
# tls: true
# fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取
# 下面两项如果填写则开启 mTLS(需要同时填写)
# certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径
# private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径
# client-fingerprint: chrome # Available: "chrome","firefox","safari","ios","random", currently only support TLS transport in TCP/GRPC/WS/HTTP for VLESS/Vmess and trojan.
# skip-cert-verify: true
# servername: example.com # priority over wss host
# network: ws
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析,不为空则通过该值指定,格式为经过base64编码的ech参数(dig +short TYPE65 tls-ech.dev)
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
# # query-server-name: xxx.com # 可选项,不为空时用于指定通过dns解析时的域名
# ws-opts:
# udp: true
# tls: true
# fingerprint: xxxx # 配置指纹将实现 SSL Pining 效果, 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取
# 下面两项如果填写则开启 mTLS(需要同时填写)
# certificate: ./client.crt # 证书 PEM 格式,或者 证书的路径
# private-key: ./client.key # 证书对应的私钥 PEM 格式,或者私钥路径
# client-fingerprint: chrome # Available: "chrome","firefox","safari","ios","random", currently only support TLS transport in TCP/GRPC/WS/HTTP for VLESS/Vmess and trojan.
# skip-cert-verify: true
# servername: example.com # priority over wss host
# network: ws
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析,不为空则通过该值指定,格式为经过base64编码的ech参数(dig +short TYPE65 tls-ech.dev)
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
# # query-server-name: xxx.com # 可选项,不为空时用于指定通过dns解析时的域名
# ws-opts:
# path: /path
# headers:
# Host: v2ray.com
# max-early-data: 2048
# early-data-header-name: Sec-WebSocket-Protocol
# v2ray-http-upgrade: false
# v2ray-http-upgrade-fast-open: false
# v2ray-http-upgrade: false
# v2ray-http-upgrade-fast-open: false

- name: "vmess-h2"
type: vmess
Expand Down Expand Up @@ -832,7 +832,7 @@ proxies: # socks5
# max-connections: 1 # Maximum connections. Conflict with max-streams.
# min-streams: 0 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams.
# max-streams: 0 # Maximum multiplexed streams in a connection before opening a new connection. Conflict with max-connections and min-streams.

reality-opts:
public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE
short-id: 10f897e26c4b9478
Expand Down Expand Up @@ -1204,6 +1204,13 @@ proxies: # socks5
# 00000000000000000000000000000000
# ...
# -----END OpenVPN Static key V1-----
# tls-auth: | # 从 .ovpn 中复制 <tls-auth></tls-auth> 内的内容;与 tls-crypt 二选一
# -----BEGIN OpenVPN Static key V1-----
# 00000000000000000000000000000000
# ...
# -----END OpenVPN Static key V1-----
# key-direction: 1 # 对应 .ovpn 的 key-direction,可选值 0/1;省略表示双向 key
# scramble: obfuscate password # 支持 xormask <mask> / xorptrpos / reverse / obfuscate <mask>;scramble <mask> 等同 xormask
# ping: 10 # 默认值为 0
# ping-restart: 60 # 默认值为 0
# mtu: 1500
Expand Down Expand Up @@ -1348,7 +1355,7 @@ proxies: # socks5
- name: sudoku
type: sudoku
server: server_ip/domain # 1.2.3.4 or domain
port: 443
port: 443
key: "<client_key>" # 如果你使用sudoku生成的ED25519密钥对,请填写密钥对中的私钥,否则填入和服务端相同的uuid
aead-method: chacha20-poly1305 # 可选:chacha20-poly1305、aes-128-gcm、none(不建议;none 不提供 AEAD 保护)
padding-min: 2 # 最小填充率(0-100)
Expand Down Expand Up @@ -1406,7 +1413,7 @@ proxies: # socks5
# min-streams: 5 # Minimum multiplexed streams in a connection before opening a new connection. Conflict with max-streams.
# max-streams: 0 # Maximum multiplexed streams in a connection before opening a new connection. Conflict with max-connections and min-streams.

# dns 出站会将请求劫持到内部 dns 模块,所有请求均在内部处理
# dns 出站会将请求劫持到内部 dns 模块,所有请求均在内部处理
- name: "dns-out"
type: dns

Expand Down Expand Up @@ -1503,8 +1510,8 @@ proxy-providers:
# age-secret-key: AGE-SECRET-KEY-1ZTQLLN0A4U3ZTT3DCZKYN0CGZEZQLWX2DFTXUWMT4ZHR0N2UG6LSW9NT0N
header:
User-Agent:
- "Clash/v1.18.0"
- "mihomo/1.18.3"
- "Clash/v1.18.0"
- "mihomo/1.18.3"
# Accept:
# - 'application/vnd.github.v3.raw'
# Authorization:
Expand Down Expand Up @@ -2179,14 +2186,14 @@ listeners:
# proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错)
stack: system # gvisor / mixed
dns-hijack:
- 0.0.0.0:53 # 需要劫持的 DNS
- 0.0.0.0:53 # 需要劫持的 DNS
# auto-detect-interface: false # 自动识别出口网卡
# auto-route: false # 配置路由表
# mtu: 9000 # 最大传输单元
inet4-address: # 必须手动设置 ipv4 地址段
- 198.19.0.1/30
- 198.19.0.1/30
inet6-address: # 必须手动设置 ipv6 地址段
- "fdfe:dcba:9877::1/126"
- "fdfe:dcba:9877::1/126"
# strict-route: true # 将所有连接路由到 tun 来防止泄漏,但你的设备将无法其他设备被访问
# inet4-route-address: # 启用 auto-route 时使用自定义路由而不是默认路由
# - 0.0.0.0/1
Expand Down
25 changes: 20 additions & 5 deletions transport/openvpn/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"sync/atomic"
"time"

"github.com/metacubex/mihomo/log"
"github.com/metacubex/tls"
"golang.org/x/sync/semaphore"
)
Expand Down Expand Up @@ -45,13 +46,21 @@ func NewClient(config *ClientConfig, io PacketIO) (*Client, error) {
if io == nil {
return nil, errors.New("nil openvpn packet io")
}
var crypt *TLSCrypt
var wrap ControlPacketWrapper
if len(config.TLSCryptKey) > 0 {
var err error
crypt, err = NewTLSCrypt(config.TLSCryptKey, true)
wrap, err = NewTLSCrypt(config.TLSCryptKey, true)
if err != nil {
return nil, err
}
log.Debugln("[OpenVPN] enabled tls-crypt control channel wrapping")
} else if len(config.TLSAuthKey) > 0 {
var err error
wrap, err = NewTLSAuth(config.TLSAuthKey, config.KeyDirection, config.Auth)
if err != nil {
return nil, err
}
log.Debugln("[OpenVPN] enabled tls-auth control channel authentication: auth=%s key-direction=%s", config.Auth, KeyDirectionString(config.KeyDirection))
}
local, err := NewSessionID()
if err != nil {
Expand All @@ -63,7 +72,7 @@ func NewClient(config *ClientConfig, io PacketIO) (*Client, error) {
client := &Client{
config: config,
mux: mux,
control: NewControlChannel(mux, crypt, local),
control: NewControlChannel(mux, wrap, local),
cancel: cancel,
writeSem: semaphore.NewWeighted(1),
}
Expand All @@ -84,9 +93,11 @@ func (c *Client) Handshake(ctx context.Context) (*PushReply, error) {
if err := c.control.SendReset(ctx); err != nil {
return nil, fmt.Errorf("send hard reset: %w", err)
}
log.Debugln("[OpenVPN] sent hard reset to start handshake")
if err := c.waitServerReset(ctx); err != nil {
return nil, err
}
log.Debugln("[OpenVPN] received server hard reset")

tlsConfig, err := c.tlsConfig()
if err != nil {
Expand All @@ -100,6 +111,7 @@ func (c *Client) Handshake(ctx context.Context) (*PushReply, error) {
if err := c.tlsConn.HandshakeContext(ctx); err != nil {
return nil, fmt.Errorf("openvpn tls handshake: %w", err)
}
log.Debugln("[OpenVPN] TLS handshake complete")

clientRecord, err := NewClientKeyMethod2Record(
InstallScriptOptionsString(c.config.Proto, c.config.Cipher, c.config.Auth, c.config.CompLZO),
Expand All @@ -121,6 +133,7 @@ func (c *Client) Handshake(ctx context.Context) (*PushReply, error) {
if err != nil {
return nil, err
}
log.Debugln("[OpenVPN] received key method 2 server record: options=%q peer-info=%q", serverRecord.Options, serverRecord.PeerInfo)

sources := clientRecord.Sources
sources.Server = serverRecord.Sources.Server
Expand All @@ -136,6 +149,7 @@ func (c *Client) Handshake(ctx context.Context) (*PushReply, error) {
if err != nil {
return nil, err
}
log.Debugln("[OpenVPN] received PUSH_REPLY: peer-id=%d routes=%v prefixes=%v raw=%q", push.PeerID, push.Routes, push.Prefixes, push.Raw)
c.push = push
c.data, err = NewDataChannel(keys, c.config.Cipher, c.config.Auth, push.PeerID)
if err != nil {
Expand All @@ -162,7 +176,7 @@ func (c *Client) writeDataPacket(ctx context.Context, packet []byte, compress bo
return err
}
defer c.writeSem.Release(1)
if compress && c.config.CompLZO == CompLzoYes {
if compress && c.config.CompLZO != "" {
compressed, err := lzo1xCompressSafe(packet)
if err != nil {
return err
Expand Down Expand Up @@ -192,13 +206,14 @@ func (c *Client) ReadIPPacket(ctx context.Context) ([]byte, error) {
}
plain, err := c.data.Decrypt(packet)
if err != nil {
log.Debugln("[OpenVPN] discard data packet: len=%d err=%v", len(packet), err)
continue
}
c.markReceive()
if IsPingPacket(plain) {
continue
}
if c.config.CompLZO == CompLzoYes && len(plain) > 0 {
if c.config.CompLZO != "" && len(plain) > 0 {
return lzo1xDecompressSafe(plain)
}
return plain, nil
Expand Down
Loading