feat: rework config handling code & add QR

This commit is contained in:
tobyxdd 2023-07-10 19:46:58 -07:00
parent 4c24edaac1
commit e21e5c67a8
6 changed files with 196 additions and 56 deletions

View file

@ -4,11 +4,13 @@ import (
"crypto/x509"
"errors"
"net"
"net/url"
"os"
"strings"
"sync"
"time"
"github.com/mdp/qrterminal/v3"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"go.uber.org/zap"
@ -20,6 +22,11 @@ import (
"github.com/apernet/hysteria/extras/obfs"
)
// Client flags
var (
showQR bool
)
var clientCmd = &cobra.Command{
Use: "client",
Short: "Client mode",
@ -27,9 +34,14 @@ var clientCmd = &cobra.Command{
}
func init() {
initClientFlags()
rootCmd.AddCommand(clientCmd)
}
func initClientFlags() {
clientCmd.Flags().BoolVar(&showQR, "qr", false, "show QR code for server config sharing")
}
type clientConfig struct {
Server string `mapstructure:"server"`
Auth string `mapstructure:"auth"`
@ -84,17 +96,15 @@ type forwardingEntry struct {
UDPTimeout time.Duration `mapstructure:"udpTimeout"`
}
// Config validates the fields and returns a ready-to-use Hysteria client config
func (c *clientConfig) Config() (*client.Config, error) {
hyConfig := &client.Config{}
// ConnFactory
func (c *clientConfig) fillConnFactory(hyConfig *client.Config) error {
switch strings.ToLower(c.Obfs.Type) {
case "", "plain":
// Default, do nothing
return nil
case "salamander":
ob, err := obfs.NewSalamanderObfuscator([]byte(c.Obfs.Salamander.Password))
if err != nil {
return nil, configError{Field: "obfs.salamander.password", Err: err}
return configError{Field: "obfs.salamander.password", Err: err}
}
hyConfig.ConnFactory = &obfsConnFactory{
NewFunc: func(addr net.Addr) (net.PacketConn, error) {
@ -102,41 +112,55 @@ func (c *clientConfig) Config() (*client.Config, error) {
},
Obfuscator: ob,
}
return nil
default:
return nil, configError{Field: "obfs.type", Err: errors.New("unsupported obfuscation type")}
return configError{Field: "obfs.type", Err: errors.New("unsupported obfuscation type")}
}
// ServerAddr
}
func (c *clientConfig) fillServerAddr(hyConfig *client.Config) error {
if c.Server == "" {
return nil, configError{Field: "server", Err: errors.New("server address is empty")}
return configError{Field: "server", Err: errors.New("server address is empty")}
}
host, hostPort := parseServerAddrString(c.Server)
addr, err := net.ResolveUDPAddr("udp", hostPort)
if err != nil {
return nil, configError{Field: "server", Err: err}
return configError{Field: "server", Err: err}
}
hyConfig.ServerAddr = addr
// Auth
hyConfig.Auth = c.Auth
// TLSConfig
// Special handling for SNI
if c.TLS.SNI == "" {
// Use server hostname as SNI
hyConfig.TLSConfig.ServerName = host
} else {
}
return nil
}
func (c *clientConfig) fillAuth(hyConfig *client.Config) error {
hyConfig.Auth = c.Auth
return nil
}
func (c *clientConfig) fillTLSConfig(hyConfig *client.Config) error {
if c.TLS.SNI != "" {
hyConfig.TLSConfig.ServerName = c.TLS.SNI
}
hyConfig.TLSConfig.InsecureSkipVerify = c.TLS.Insecure
if c.TLS.CA != "" {
ca, err := os.ReadFile(c.TLS.CA)
if err != nil {
return nil, configError{Field: "tls.ca", Err: err}
return configError{Field: "tls.ca", Err: err}
}
cPool := x509.NewCertPool()
if !cPool.AppendCertsFromPEM(ca) {
return nil, configError{Field: "tls.ca", Err: errors.New("failed to parse CA certificate")}
return configError{Field: "tls.ca", Err: errors.New("failed to parse CA certificate")}
}
hyConfig.TLSConfig.RootCAs = cPool
}
// QUICConfig
return nil
}
func (c *clientConfig) fillQUICConfig(hyConfig *client.Config) error {
hyConfig.QUICConfig = client.QUICConfig{
InitialStreamReceiveWindow: c.QUIC.InitStreamReceiveWindow,
MaxStreamReceiveWindow: c.QUIC.MaxStreamReceiveWindow,
@ -146,24 +170,76 @@ func (c *clientConfig) Config() (*client.Config, error) {
KeepAlivePeriod: c.QUIC.KeepAlivePeriod,
DisablePathMTUDiscovery: c.QUIC.DisablePathMTUDiscovery,
}
// BandwidthConfig
return nil
}
func (c *clientConfig) fillBandwidthConfig(hyConfig *client.Config) error {
if c.Bandwidth.Up == "" || c.Bandwidth.Down == "" {
return nil, configError{Field: "bandwidth", Err: errors.New("both up and down bandwidth must be set")}
return configError{Field: "bandwidth", Err: errors.New("both up and down bandwidth must be set")}
}
var err error
hyConfig.BandwidthConfig.MaxTx, err = convBandwidth(c.Bandwidth.Up)
if err != nil {
return nil, configError{Field: "bandwidth.up", Err: err}
return configError{Field: "bandwidth.up", Err: err}
}
hyConfig.BandwidthConfig.MaxRx, err = convBandwidth(c.Bandwidth.Down)
if err != nil {
return nil, configError{Field: "bandwidth.down", Err: err}
return configError{Field: "bandwidth.down", Err: err}
}
// FastOpen
hyConfig.FastOpen = c.FastOpen
return nil
}
func (c *clientConfig) fillFastOpen(hyConfig *client.Config) error {
hyConfig.FastOpen = c.FastOpen
return nil
}
// Config validates the fields and returns a ready-to-use Hysteria client config
func (c *clientConfig) Config() (*client.Config, error) {
hyConfig := &client.Config{}
fillers := []func(*client.Config) error{
c.fillConnFactory,
c.fillServerAddr,
c.fillAuth,
c.fillTLSConfig,
c.fillQUICConfig,
c.fillBandwidthConfig,
c.fillFastOpen,
}
for _, f := range fillers {
if err := f(hyConfig); err != nil {
return nil, err
}
}
return hyConfig, nil
}
// ShareURI generates a URI for sharing the config with others.
// Note that only the fields necessary for a client to connect to the server are included.
// It doesn't include local modes, for example.
func (c *clientConfig) ShareURI() string {
q := url.Values{}
switch strings.ToLower(c.Obfs.Type) {
case "salamander":
q.Set("obfs", "salamander")
q.Set("obfs-password", c.Obfs.Salamander.Password)
}
if c.TLS.SNI != "" {
q.Set("sni", c.TLS.SNI)
}
if c.TLS.Insecure {
q.Set("insecure", "1")
}
u := url.URL{
Scheme: "hysteria2",
User: url.User(c.Auth),
Host: c.Server,
Path: "/",
RawQuery: q.Encode(),
}
return u.String()
}
func runClient(cmd *cobra.Command, args []string) {
logger.Info("client mode")
@ -185,6 +261,17 @@ func runClient(cmd *cobra.Command, args []string) {
}
defer c.Close()
uri := config.ShareURI()
logger.Info("use this URI to share your server", zap.String("uri", uri))
if showQR {
qrterminal.GenerateWithConfig(uri, qrterminal.Config{
Level: qrterminal.L,
Writer: os.Stdout,
BlackChar: qrterminal.BLACK,
WhiteChar: qrterminal.WHITE,
})
}
// Modes
var wg sync.WaitGroup
hasMode := false

View file

@ -87,49 +87,50 @@ type serverConfigACME struct {
Dir string `mapstructure:"dir"`
}
// Config validates the fields and returns a ready-to-use Hysteria server config
func (c *serverConfig) Config() (*server.Config, error) {
hyConfig := &server.Config{}
// Conn
func (c *serverConfig) fillConn(hyConfig *server.Config) error {
listenAddr := c.Listen
if listenAddr == "" {
listenAddr = ":443"
}
uAddr, err := net.ResolveUDPAddr("udp", listenAddr)
if err != nil {
return nil, configError{Field: "listen", Err: err}
return configError{Field: "listen", Err: err}
}
conn, err := net.ListenUDP("udp", uAddr)
if err != nil {
return nil, configError{Field: "listen", Err: err}
return configError{Field: "listen", Err: err}
}
switch strings.ToLower(c.Obfs.Type) {
case "", "plain":
hyConfig.Conn = conn
return nil
case "salamander":
ob, err := obfs.NewSalamanderObfuscator([]byte(c.Obfs.Salamander.Password))
if err != nil {
return nil, configError{Field: "obfs.salamander.password", Err: err}
return configError{Field: "obfs.salamander.password", Err: err}
}
hyConfig.Conn = obfs.WrapPacketConn(conn, ob)
return nil
default:
return nil, configError{Field: "obfs.type", Err: errors.New("unsupported obfuscation type")}
return configError{Field: "obfs.type", Err: errors.New("unsupported obfuscation type")}
}
// TLSConfig
}
func (c *serverConfig) fillTLSConfig(hyConfig *server.Config) error {
if c.TLS == nil && c.ACME == nil {
return nil, configError{Field: "tls", Err: errors.New("must set either tls or acme")}
return configError{Field: "tls", Err: errors.New("must set either tls or acme")}
}
if c.TLS != nil && c.ACME != nil {
return nil, configError{Field: "tls", Err: errors.New("cannot set both tls and acme")}
return configError{Field: "tls", Err: errors.New("cannot set both tls and acme")}
}
if c.TLS != nil {
// Local TLS cert
if c.TLS.Cert == "" || c.TLS.Key == "" {
return nil, configError{Field: "tls", Err: errors.New("empty cert or key path")}
return configError{Field: "tls", Err: errors.New("empty cert or key path")}
}
cert, err := tls.LoadX509KeyPair(c.TLS.Cert, c.TLS.Key)
if err != nil {
return nil, configError{Field: "tls", Err: err}
return configError{Field: "tls", Err: err}
}
hyConfig.TLSConfig.Certificates = []tls.Certificate{cert}
} else {
@ -160,7 +161,7 @@ func (c *serverConfig) Config() (*server.Config, error) {
case "zerossl", "zero":
cmIssuer.CA = certmagic.ZeroSSLProductionCA
default:
return nil, configError{Field: "acme.ca", Err: errors.New("unknown CA")}
return configError{Field: "acme.ca", Err: errors.New("unknown CA")}
}
cmCfg.Issuers = []certmagic.Issuer{cmIssuer}
cmCache := certmagic.NewCache(certmagic.CacheOptions{
@ -172,15 +173,18 @@ func (c *serverConfig) Config() (*server.Config, error) {
cmCfg = certmagic.New(cmCache, *cmCfg)
if len(c.ACME.Domains) == 0 {
return nil, configError{Field: "acme.domains", Err: errors.New("empty domains")}
return configError{Field: "acme.domains", Err: errors.New("empty domains")}
}
err = cmCfg.ManageSync(context.Background(), c.ACME.Domains)
err := cmCfg.ManageSync(context.Background(), c.ACME.Domains)
if err != nil {
return nil, configError{Field: "acme.domains", Err: err}
return configError{Field: "acme.domains", Err: err}
}
hyConfig.TLSConfig.GetCertificate = cmCfg.GetCertificate
}
// QUICConfig
return nil
}
func (c *serverConfig) fillQUICConfig(hyConfig *server.Config) error {
hyConfig.QUICConfig = server.QUICConfig{
InitialStreamReceiveWindow: c.QUIC.InitStreamReceiveWindow,
MaxStreamReceiveWindow: c.QUIC.MaxStreamReceiveWindow,
@ -190,52 +194,70 @@ func (c *serverConfig) Config() (*server.Config, error) {
MaxIncomingStreams: c.QUIC.MaxIncomingStreams,
DisablePathMTUDiscovery: c.QUIC.DisablePathMTUDiscovery,
}
// BandwidthConfig
return nil
}
func (c *serverConfig) fillBandwidthConfig(hyConfig *server.Config) error {
var err error
if c.Bandwidth.Up != "" {
hyConfig.BandwidthConfig.MaxTx, err = convBandwidth(c.Bandwidth.Up)
if err != nil {
return nil, configError{Field: "bandwidth.up", Err: err}
return configError{Field: "bandwidth.up", Err: err}
}
}
if c.Bandwidth.Down != "" {
hyConfig.BandwidthConfig.MaxRx, err = convBandwidth(c.Bandwidth.Down)
if err != nil {
return nil, configError{Field: "bandwidth.down", Err: err}
return configError{Field: "bandwidth.down", Err: err}
}
}
// DisableUDP
return nil
}
func (c *serverConfig) fillDisableUDP(hyConfig *server.Config) error {
hyConfig.DisableUDP = c.DisableUDP
// Authenticator
return nil
}
func (c *serverConfig) fillAuthenticator(hyConfig *server.Config) error {
if c.Auth.Type == "" {
return nil, configError{Field: "auth.type", Err: errors.New("empty auth type")}
return configError{Field: "auth.type", Err: errors.New("empty auth type")}
}
switch strings.ToLower(c.Auth.Type) {
case "password":
if c.Auth.Password == "" {
return nil, configError{Field: "auth.password", Err: errors.New("empty auth password")}
return configError{Field: "auth.password", Err: errors.New("empty auth password")}
}
hyConfig.Authenticator = &auth.PasswordAuthenticator{Password: c.Auth.Password}
return nil
default:
return nil, configError{Field: "auth.type", Err: errors.New("unsupported auth type")}
return configError{Field: "auth.type", Err: errors.New("unsupported auth type")}
}
// EventLogger
}
func (c *serverConfig) fillEventLogger(hyConfig *server.Config) error {
hyConfig.EventLogger = &serverLogger{}
// MasqHandler
return nil
}
func (c *serverConfig) fillMasqHandler(hyConfig *server.Config) error {
switch strings.ToLower(c.Masquerade.Type) {
case "", "404":
hyConfig.MasqHandler = http.NotFoundHandler()
return nil
case "file":
if c.Masquerade.File.Dir == "" {
return nil, configError{Field: "masquerade.file.dir", Err: errors.New("empty file directory")}
return configError{Field: "masquerade.file.dir", Err: errors.New("empty file directory")}
}
hyConfig.MasqHandler = http.FileServer(http.Dir(c.Masquerade.File.Dir))
return nil
case "proxy":
if c.Masquerade.Proxy.URL == "" {
return nil, configError{Field: "masquerade.proxy.url", Err: errors.New("empty proxy url")}
return configError{Field: "masquerade.proxy.url", Err: errors.New("empty proxy url")}
}
u, err := url.Parse(c.Masquerade.Proxy.URL)
if err != nil {
return nil, configError{Field: "masquerade.proxy.url", Err: err}
return configError{Field: "masquerade.proxy.url", Err: err}
}
hyConfig.MasqHandler = &httputil.ReverseProxy{
Rewrite: func(r *httputil.ProxyRequest) {
@ -251,8 +273,29 @@ func (c *serverConfig) Config() (*server.Config, error) {
w.WriteHeader(http.StatusBadGateway)
},
}
return nil
default:
return nil, configError{Field: "masquerade.type", Err: errors.New("unsupported masquerade type")}
return configError{Field: "masquerade.type", Err: errors.New("unsupported masquerade type")}
}
}
// Config validates the fields and returns a ready-to-use Hysteria server config
func (c *serverConfig) Config() (*server.Config, error) {
hyConfig := &server.Config{}
fillers := []func(*server.Config) error{
c.fillConn,
c.fillTLSConfig,
c.fillQUICConfig,
c.fillBandwidthConfig,
c.fillDisableUDP,
c.fillAuthenticator,
c.fillEventLogger,
c.fillMasqHandler,
}
for _, f := range fillers {
if err := f(hyConfig); err != nil {
return nil, err
}
}
return hyConfig, nil
}

View file

@ -6,6 +6,7 @@ require (
github.com/apernet/hysteria/core v0.0.0-00010101000000-000000000000
github.com/apernet/hysteria/extras v0.0.0-00010101000000-000000000000
github.com/caddyserver/certmagic v0.17.2
github.com/mdp/qrterminal/v3 v3.1.1
github.com/spf13/cobra v1.7.0
github.com/spf13/viper v1.15.0
github.com/txthinking/socks5 v0.0.0-20230325130024-4230056ae301
@ -49,6 +50,7 @@ require (
golang.org/x/tools v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
rsc.io/qr v0.2.0 // indirect
)
replace github.com/quic-go/quic-go => github.com/apernet/quic-go v0.36.1-0.20230627042819-0a89ea8e4c8d

View file

@ -154,6 +154,8 @@ github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mdp/qrterminal/v3 v3.1.1 h1:cIPwg3QU0OIm9+ce/lRfWXhPwEjOSKwk3HBwL3HBTyc=
github.com/mdp/qrterminal/v3 v3.1.1/go.mod h1:5lJlXe7Jdr8wlPDdcsJttv1/knsRgzXASyr4dcGZqNU=
github.com/mholt/acmez v1.0.4 h1:N3cE4Pek+dSolbsofIkAYz6H1d3pE+2G0os7QHslf80=
github.com/mholt/acmez v1.0.4/go.mod h1:qFGLZ4u+ehWINeJZjzPlsnjJBCPAADWTcIqE/7DAYQY=
github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo=
@ -572,5 +574,7 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View file

@ -2,7 +2,10 @@ module github.com/apernet/hysteria/extras
go 1.20
require github.com/apernet/hysteria/core v0.0.0-00010101000000-000000000000
require (
github.com/apernet/hysteria/core v0.0.0-00010101000000-000000000000
golang.org/x/crypto v0.4.0
)
require (
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
@ -13,7 +16,6 @@ require (
github.com/quic-go/qtls-go1-19 v0.3.2 // indirect
github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
github.com/quic-go/quic-go v0.0.0-00010101000000-000000000000 // indirect
golang.org/x/crypto v0.4.0 // indirect
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/net v0.10.0 // indirect

View file

@ -27,7 +27,9 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=