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  }