github.com/metacubex/mihomo@v1.18.5/adapter/outbound/hysteria2.go (about) 1 package outbound 2 3 import ( 4 "context" 5 "crypto/tls" 6 "errors" 7 "fmt" 8 "net" 9 "runtime" 10 "strconv" 11 "time" 12 13 CN "github.com/metacubex/mihomo/common/net" 14 "github.com/metacubex/mihomo/common/utils" 15 "github.com/metacubex/mihomo/component/ca" 16 "github.com/metacubex/mihomo/component/dialer" 17 "github.com/metacubex/mihomo/component/proxydialer" 18 C "github.com/metacubex/mihomo/constant" 19 "github.com/metacubex/mihomo/log" 20 tuicCommon "github.com/metacubex/mihomo/transport/tuic/common" 21 22 "github.com/metacubex/sing-quic/hysteria2" 23 24 M "github.com/sagernet/sing/common/metadata" 25 "github.com/zhangyunhao116/fastrand" 26 ) 27 28 func init() { 29 hysteria2.SetCongestionController = tuicCommon.SetCongestionController 30 } 31 32 const minHopInterval = 5 33 const defaultHopInterval = 30 34 35 type Hysteria2 struct { 36 *Base 37 38 option *Hysteria2Option 39 client *hysteria2.Client 40 dialer proxydialer.SingDialer 41 } 42 43 type Hysteria2Option struct { 44 BasicOption 45 Name string `proxy:"name"` 46 Server string `proxy:"server"` 47 Port int `proxy:"port,omitempty"` 48 Ports string `proxy:"ports,omitempty"` 49 HopInterval int `proxy:"hop-interval,omitempty"` 50 Up string `proxy:"up,omitempty"` 51 Down string `proxy:"down,omitempty"` 52 Password string `proxy:"password,omitempty"` 53 Obfs string `proxy:"obfs,omitempty"` 54 ObfsPassword string `proxy:"obfs-password,omitempty"` 55 SNI string `proxy:"sni,omitempty"` 56 SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` 57 Fingerprint string `proxy:"fingerprint,omitempty"` 58 ALPN []string `proxy:"alpn,omitempty"` 59 CustomCA string `proxy:"ca,omitempty"` 60 CustomCAString string `proxy:"ca-str,omitempty"` 61 CWND int `proxy:"cwnd,omitempty"` 62 UdpMTU int `proxy:"udp-mtu,omitempty"` 63 } 64 65 func (h *Hysteria2) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { 66 options := h.Base.DialOptions(opts...) 67 h.dialer.SetDialer(dialer.NewDialer(options...)) 68 c, err := h.client.DialConn(ctx, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) 69 if err != nil { 70 return nil, err 71 } 72 return NewConn(CN.NewRefConn(c, h), h), nil 73 } 74 75 func (h *Hysteria2) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { 76 options := h.Base.DialOptions(opts...) 77 h.dialer.SetDialer(dialer.NewDialer(options...)) 78 pc, err := h.client.ListenPacket(ctx) 79 if err != nil { 80 return nil, err 81 } 82 if pc == nil { 83 return nil, errors.New("packetConn is nil") 84 } 85 return newPacketConn(CN.NewRefPacketConn(CN.NewThreadSafePacketConn(pc), h), h), nil 86 } 87 88 func closeHysteria2(h *Hysteria2) { 89 if h.client != nil { 90 _ = h.client.CloseWithError(errors.New("proxy removed")) 91 } 92 } 93 94 func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { 95 addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) 96 var salamanderPassword string 97 if len(option.Obfs) > 0 { 98 if option.ObfsPassword == "" { 99 return nil, errors.New("missing obfs password") 100 } 101 switch option.Obfs { 102 case hysteria2.ObfsTypeSalamander: 103 salamanderPassword = option.ObfsPassword 104 default: 105 return nil, fmt.Errorf("unknown obfs type: %s", option.Obfs) 106 } 107 } 108 109 serverName := option.Server 110 if option.SNI != "" { 111 serverName = option.SNI 112 } 113 114 tlsConfig := &tls.Config{ 115 ServerName: serverName, 116 InsecureSkipVerify: option.SkipCertVerify, 117 MinVersion: tls.VersionTLS13, 118 } 119 120 var err error 121 tlsConfig, err = ca.GetTLSConfig(tlsConfig, option.Fingerprint, option.CustomCA, option.CustomCAString) 122 if err != nil { 123 return nil, err 124 } 125 126 if len(option.ALPN) > 0 { 127 tlsConfig.NextProtos = option.ALPN 128 } 129 130 if option.UdpMTU == 0 { 131 // "1200" from quic-go's MaxDatagramSize 132 // "-3" from quic-go's DatagramFrame.MaxDataLen 133 option.UdpMTU = 1200 - 3 134 } 135 136 singDialer := proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer()) 137 138 clientOptions := hysteria2.ClientOptions{ 139 Context: context.TODO(), 140 Dialer: singDialer, 141 Logger: log.SingLogger, 142 SendBPS: StringToBps(option.Up), 143 ReceiveBPS: StringToBps(option.Down), 144 SalamanderPassword: salamanderPassword, 145 Password: option.Password, 146 TLSConfig: tlsConfig, 147 UDPDisabled: false, 148 CWND: option.CWND, 149 UdpMTU: option.UdpMTU, 150 ServerAddress: func(ctx context.Context) (*net.UDPAddr, error) { 151 return resolveUDPAddrWithPrefer(ctx, "udp", addr, C.NewDNSPrefer(option.IPVersion)) 152 }, 153 } 154 155 var ranges utils.IntRanges[uint16] 156 var serverAddress []string 157 if option.Ports != "" { 158 ranges, err = utils.NewUnsignedRanges[uint16](option.Ports) 159 if err != nil { 160 return nil, err 161 } 162 ranges.Range(func(port uint16) bool { 163 serverAddress = append(serverAddress, net.JoinHostPort(option.Server, strconv.Itoa(int(port)))) 164 return true 165 }) 166 if len(serverAddress) > 0 { 167 clientOptions.ServerAddress = func(ctx context.Context) (*net.UDPAddr, error) { 168 return resolveUDPAddrWithPrefer(ctx, "udp", serverAddress[fastrand.Intn(len(serverAddress))], C.NewDNSPrefer(option.IPVersion)) 169 } 170 171 if option.HopInterval == 0 { 172 option.HopInterval = defaultHopInterval 173 } else if option.HopInterval < minHopInterval { 174 option.HopInterval = minHopInterval 175 } 176 clientOptions.HopInterval = time.Duration(option.HopInterval) * time.Second 177 } 178 } 179 if option.Port == 0 && len(serverAddress) == 0 { 180 return nil, errors.New("invalid port") 181 } 182 183 client, err := hysteria2.NewClient(clientOptions) 184 if err != nil { 185 return nil, err 186 } 187 188 outbound := &Hysteria2{ 189 Base: &Base{ 190 name: option.Name, 191 addr: addr, 192 tp: C.Hysteria2, 193 udp: true, 194 iface: option.Interface, 195 rmark: option.RoutingMark, 196 prefer: C.NewDNSPrefer(option.IPVersion), 197 }, 198 option: &option, 199 client: client, 200 dialer: singDialer, 201 } 202 runtime.SetFinalizer(outbound, closeHysteria2) 203 204 return outbound, nil 205 }