package cmd import ( "context" "crypto/tls" "crypto/x509" "encoding/json" "errors" "fmt" "io" "net" "net/http" "net/http/httputil" "net/netip" "net/url" "os" "os/signal" "slices" "strconv" "strings" "sync" "syscall" "time" "github.com/caddyserver/certmagic" "github.com/libdns/cloudflare" "github.com/libdns/duckdns" "github.com/libdns/gandi" "github.com/libdns/godaddy" "github.com/libdns/namedotcom" "github.com/libdns/vultr" "github.com/mholt/acmez/acme" "github.com/spf13/cobra" "github.com/spf13/viper" "go.uber.org/zap" "golang.org/x/sync/singleflight" "github.com/apernet/hysteria/app/v2/internal/firewall" "github.com/apernet/hysteria/app/v2/internal/utils" "github.com/apernet/hysteria/core/v2/client" "github.com/apernet/hysteria/core/v2/server" "github.com/apernet/hysteria/extras/v2/auth" "github.com/apernet/hysteria/extras/v2/correctnet" "github.com/apernet/hysteria/extras/v2/masq" "github.com/apernet/hysteria/extras/v2/network" "github.com/apernet/hysteria/extras/v2/obfs" "github.com/apernet/hysteria/extras/v2/outbounds" "github.com/apernet/hysteria/extras/v2/realm" "github.com/apernet/hysteria/extras/v2/sniff" "github.com/apernet/hysteria/extras/v2/trafficlogger" eUtils "github.com/apernet/hysteria/extras/v2/utils" ) const ( defaultListenAddr = ":443" ) var serverCmd = &cobra.Command{ Use: "server", Short: "Server mode", Run: runServerCmd, } func init() { rootCmd.AddCommand(serverCmd) } type serverConfig struct { Listen string `mapstructure:"listen"` Realm serverConfigRealm `mapstructure:"realm"` Obfs serverConfigObfs `mapstructure:"obfs"` Noise serverConfigNoise `mapstructure:"noise"` TLS *serverConfigTLS `mapstructure:"tls"` ACME *serverConfigACME `mapstructure:"acme"` QUIC serverConfigQUIC `mapstructure:"quic"` Congestion serverConfigCongestion `mapstructure:"congestion"` Bandwidth serverConfigBandwidth `mapstructure:"bandwidth"` IgnoreClientBandwidth bool `mapstructure:"ignoreClientBandwidth"` SpeedTest bool `mapstructure:"speedTest"` DisableUDP bool `mapstructure:"disableUDP"` UDPIdleTimeout time.Duration `mapstructure:"udpIdleTimeout"` Auth serverConfigAuth `mapstructure:"auth"` Resolver serverConfigResolver `mapstructure:"resolver"` Sniff serverConfigSniff `mapstructure:"sniff"` ACL serverConfigACL `mapstructure:"acl"` Outbounds []serverConfigOutboundEntry `mapstructure:"outbounds"` TrafficStats serverConfigTrafficStats `mapstructure:"trafficStats"` Masquerade serverConfigMasquerade `mapstructure:"masquerade"` Network *serverConfigNetwork `mapstructure:"network"` } type serverConfigRealm struct { STUNServers []string `mapstructure:"stunServers"` STUNTimeout time.Duration `mapstructure:"stunTimeout"` PunchTimeout time.Duration `mapstructure:"punchTimeout"` HeartbeatInterval time.Duration `mapstructure:"heartbeatInterval"` Insecure bool `mapstructure:"insecure"` IPMode string `mapstructure:"ipMode"` PortMapping realmPortMappingConfig `mapstructure:"portMapping"` } type serverConfigObfsSalamander struct { Password string `mapstructure:"password"` } type serverConfigObfsGecko struct { Password string `mapstructure:"password"` MinPacketSize int `mapstructure:"minPacketSize"` MaxPacketSize int `mapstructure:"maxPacketSize"` } type serverConfigObfsTLSMimic struct { Password string `mapstructure:"password"` } type serverConfigObfs struct { Type string `mapstructure:"type"` Salamander serverConfigObfsSalamander `mapstructure:"salamander"` Gecko serverConfigObfsGecko `mapstructure:"gecko"` TLSMimic serverConfigObfsTLSMimic `mapstructure:"tlsmimic"` MinPadding int `mapstructure:"minPadding"` MaxPadding int `mapstructure:"maxPadding"` } type serverConfigNoiseFileMask struct { Dir string `mapstructure:"dir"` MaxRate string `mapstructure:"maxRate"` MinPacketSize int `mapstructure:"minPacketSize"` MaxPacketSize int `mapstructure:"maxPacketSize"` IdleThreshold time.Duration `mapstructure:"idleThreshold"` } type serverConfigNoise struct { Type string `mapstructure:"type"` FileMask serverConfigNoiseFileMask `mapstructure:"filemask"` } type serverConfigTLS struct { Cert string `mapstructure:"cert"` Key string `mapstructure:"key"` SNIGuard string `mapstructure:"sniGuard"` // "disable", "dns-san", "strict" ClientCA string `mapstructure:"clientCA"` } type serverConfigACME struct { // Common fields Domains []string `mapstructure:"domains"` Email string `mapstructure:"email"` CA string `mapstructure:"ca"` ListenHost string `mapstructure:"listenHost"` Dir string `mapstructure:"dir"` // Type selection Type string `mapstructure:"type"` HTTP serverConfigACMEHTTP `mapstructure:"http"` TLS serverConfigACMETLS `mapstructure:"tls"` DNS serverConfigACMEDNS `mapstructure:"dns"` // Legacy fields for backwards compatibility // Only applicable when Type is empty DisableHTTP bool `mapstructure:"disableHTTP"` DisableTLSALPN bool `mapstructure:"disableTLSALPN"` AltHTTPPort int `mapstructure:"altHTTPPort"` AltTLSALPNPort int `mapstructure:"altTLSALPNPort"` } type serverConfigACMEHTTP struct { AltPort int `mapstructure:"altPort"` } type serverConfigACMETLS struct { AltPort int `mapstructure:"altPort"` } type serverConfigACMEDNS struct { Name string `mapstructure:"name"` Config map[string]string `mapstructure:"config"` } type serverConfigQUIC struct { InitStreamReceiveWindow uint64 `mapstructure:"initStreamReceiveWindow"` MaxStreamReceiveWindow uint64 `mapstructure:"maxStreamReceiveWindow"` InitConnectionReceiveWindow uint64 `mapstructure:"initConnReceiveWindow"` MaxConnectionReceiveWindow uint64 `mapstructure:"maxConnReceiveWindow"` MaxIdleTimeout time.Duration `mapstructure:"maxIdleTimeout"` MaxIncomingStreams int64 `mapstructure:"maxIncomingStreams"` DisablePathMTUDiscovery bool `mapstructure:"disablePathMTUDiscovery"` } type serverConfigBandwidth struct { Up string `mapstructure:"up"` Down string `mapstructure:"down"` } type serverConfigCongestion struct { Type string `mapstructure:"type"` BBRProfile string `mapstructure:"bbrProfile"` } type serverConfigAuthHTTP struct { URL string `mapstructure:"url"` Insecure bool `mapstructure:"insecure"` } type serverConfigAuth struct { Type string `mapstructure:"type"` Password string `mapstructure:"password"` UserPass map[string]string `mapstructure:"userpass"` HTTP serverConfigAuthHTTP `mapstructure:"http"` Command string `mapstructure:"command"` } type serverConfigResolverTCP struct { Addr string `mapstructure:"addr"` Timeout time.Duration `mapstructure:"timeout"` } type serverConfigResolverUDP struct { Addr string `mapstructure:"addr"` Timeout time.Duration `mapstructure:"timeout"` } type serverConfigResolverTLS struct { Addr string `mapstructure:"addr"` Timeout time.Duration `mapstructure:"timeout"` SNI string `mapstructure:"sni"` Insecure bool `mapstructure:"insecure"` } type serverConfigResolverHTTPS struct { Addr string `mapstructure:"addr"` Timeout time.Duration `mapstructure:"timeout"` SNI string `mapstructure:"sni"` Insecure bool `mapstructure:"insecure"` } type serverConfigResolver struct { Type string `mapstructure:"type"` TCP serverConfigResolverTCP `mapstructure:"tcp"` UDP serverConfigResolverUDP `mapstructure:"udp"` TLS serverConfigResolverTLS `mapstructure:"tls"` HTTPS serverConfigResolverHTTPS `mapstructure:"https"` } type serverConfigSniff struct { Enable bool `mapstructure:"enable"` Timeout time.Duration `mapstructure:"timeout"` RewriteDomain bool `mapstructure:"rewriteDomain"` TCPPorts string `mapstructure:"tcpPorts"` UDPPorts string `mapstructure:"udpPorts"` } type serverConfigACL struct { File string `mapstructure:"file"` Inline []string `mapstructure:"inline"` GeoIP string `mapstructure:"geoip"` GeoSite string `mapstructure:"geosite"` GeoUpdateInterval time.Duration `mapstructure:"geoUpdateInterval"` } type serverConfigOutboundDirect struct { Mode string `mapstructure:"mode"` BindIPv4 string `mapstructure:"bindIPv4"` BindIPv6 string `mapstructure:"bindIPv6"` BindDevice string `mapstructure:"bindDevice"` FastOpen bool `mapstructure:"fastOpen"` } type serverConfigOutboundSOCKS5 struct { Addr string `mapstructure:"addr"` Username string `mapstructure:"username"` Password string `mapstructure:"password"` } type serverConfigOutboundHTTP struct { URL string `mapstructure:"url"` Insecure bool `mapstructure:"insecure"` } type serverConfigOutboundTLS struct { SNI string `mapstructure:"sni"` Insecure bool `mapstructure:"insecure"` PinSHA256 string `mapstructure:"pinSHA256"` CA string `mapstructure:"ca"` } type serverConfigOutboundTransportUDP struct { HopInterval time.Duration `mapstructure:"hopInterval"` MinHopInterval time.Duration `mapstructure:"minHopInterval"` MaxHopInterval time.Duration `mapstructure:"maxHopInterval"` } type serverConfigOutboundTransport struct { Type string `mapstructure:"type"` UDP serverConfigOutboundTransportUDP `mapstructure:"udp"` } type serverConfigOutboundHysteria struct { Server string `mapstructure:"server"` Auth string `mapstructure:"auth"` TLS serverConfigOutboundTLS `mapstructure:"tls"` QUIC serverConfigQUIC `mapstructure:"quic"` Bandwidth serverConfigBandwidth `mapstructure:"bandwidth"` Congestion serverConfigCongestion `mapstructure:"congestion"` Obfs serverConfigObfs `mapstructure:"obfs"` Transport serverConfigOutboundTransport `mapstructure:"transport"` FastOpen bool `mapstructure:"fastOpen"` } type serverConfigOutboundEntry struct { Name string `mapstructure:"name"` Type string `mapstructure:"type"` Direct serverConfigOutboundDirect `mapstructure:"direct"` SOCKS5 serverConfigOutboundSOCKS5 `mapstructure:"socks5"` HTTP serverConfigOutboundHTTP `mapstructure:"http"` Hysteria serverConfigOutboundHysteria `mapstructure:"hysteria"` } type serverConfigTrafficStats struct { Listen string `mapstructure:"listen"` Secret string `mapstructure:"secret"` } type serverConfigMasqueradeFile struct { Dir string `mapstructure:"dir"` } type serverConfigMasqueradeProxy struct { URL string `mapstructure:"url"` RewriteHost bool `mapstructure:"rewriteHost"` XForwarded bool `mapstructure:"xForwarded"` Insecure bool `mapstructure:"insecure"` } type serverConfigMasqueradeString struct { Content string `mapstructure:"content"` Headers map[string]string `mapstructure:"headers"` StatusCode int `mapstructure:"statusCode"` } type serverConfigNetworkTUN struct { Name string `mapstructure:"name"` MTU int `mapstructure:"mtu"` } type serverConfigNetwork struct { Enabled bool `mapstructure:"enabled"` Listen string `mapstructure:"listen"` Token string `mapstructure:"token"` TUN serverConfigNetworkTUN `mapstructure:"tun"` Pool string `mapstructure:"pool"` } type serverConfigMasquerade struct { Type string `mapstructure:"type"` File serverConfigMasqueradeFile `mapstructure:"file"` Proxy serverConfigMasqueradeProxy `mapstructure:"proxy"` String serverConfigMasqueradeString `mapstructure:"string"` ListenHTTP string `mapstructure:"listenHTTP"` ListenHTTPS string `mapstructure:"listenHTTPS"` ForceHTTPS bool `mapstructure:"forceHTTPS"` } func (c *serverConfig) fillConn(hyConfig *server.Config) error { if realmAddr, ok, err := parseServerRealmAddr(c.Listen); ok || err != nil { if err != nil { return configError{Field: "listen", Err: err} } return c.fillRealmConn(hyConfig, realmAddr) } listenAddr := c.Listen if listenAddr == "" { listenAddr = defaultListenAddr } uAddr, portUnion, err := resolveServerListenAddr(listenAddr) if err != nil { return configError{Field: "listen", Err: err} } conn, err := correctnet.ListenUDP("udp", uAddr) if err != nil { return configError{Field: "listen", Err: err} } var packetConn net.PacketConn = conn var cleanup io.Closer if len(portUnion) > 0 { cleanup, err = firewall.SetupUDPPortRedirect(uAddr, portUnion) if err != nil { _ = conn.Close() return configError{Field: "listen", Err: err} } } wrapped, err := c.wrapObfs(packetConn) if err != nil { _ = conn.Close() if cleanup != nil { _ = cleanup.Close() } return err } hyConfig.Conn = wrapped hyConfig.Cleanup = cleanup return nil } func parseServerRealmAddr(listen string) (*realm.Addr, bool, error) { addr, err := realm.ParseAddr(listen) if err == nil { return addr, true, nil } if strings.HasPrefix(listen, realm.SchemeHTTPS+":") || strings.HasPrefix(listen, realm.SchemeHTTP+":") { return nil, true, err } return nil, false, nil } func (c *serverConfig) fillRealmConn(hyConfig *server.Config, addr *realm.Addr) error { logger.Debug("realm server mode detected", zap.String("realm", addr.RealmID), zap.String("realmServer", addr.HostPort), zap.String("scheme", addr.RendezvousScheme)) family, network, err := realmIPMode(c.Realm.IPMode) if err != nil { return configError{Field: "realm.ipMode", Err: err} } listenAddr := &net.UDPAddr{} if addr.LocalPort != 0 { listenAddr.Port = addr.LocalPort } conn, err := correctnet.ListenUDP(network, listenAddr) if err != nil { return configError{Field: "listen", Err: err} } logger.Debug("realm server UDP socket opened", zap.String("realm", addr.RealmID), zap.String("local", conn.LocalAddr().String())) punchConn, err := realm.NewPunchPacketConn(conn, 0) if err != nil { _ = conn.Close() return configError{Field: "realm", Err: err} } packetConn, err := c.wrapObfs(punchConn) if err != nil { _ = conn.Close() return err } ctx, cancel := context.WithCancel(context.Background()) runtime, err := c.startRealmServerRuntime(ctx, cancel, addr, punchConn, family) if err != nil { cancel() _ = packetConn.Close() return err } hyConfig.Conn = packetConn hyConfig.Cleanup = runtime return nil } func (c *serverConfig) wrapObfs(conn net.PacketConn) (net.PacketConn, error) { packetConn := conn switch strings.ToLower(c.Obfs.Type) { case "", "plain": case "salamander": wrapped, err := obfs.WrapPacketConnSalamander(packetConn, []byte(c.Obfs.Salamander.Password)) if err != nil { return nil, configError{Field: "obfs.salamander.password", Err: err} } packetConn = wrapped case "gecko": wrapped, err := obfs.WrapPacketConnGecko(packetConn, obfs.GeckoOptions{ Password: []byte(c.Obfs.Gecko.Password), MinPacketSize: c.Obfs.Gecko.MinPacketSize, MaxPacketSize: c.Obfs.Gecko.MaxPacketSize, }) if err != nil { return nil, configError{Field: "obfs.gecko", Err: err} } packetConn = wrapped case "tlsmimic": wrapped, err := obfs.WrapPacketConnTLSMimic(packetConn, []byte(c.Obfs.TLSMimic.Password)) if err != nil { return nil, configError{Field: "obfs.tlsmimic.password", Err: err} } packetConn = wrapped case "auto", "salamander+tlsmimic": psk := c.Obfs.Salamander.Password if psk == "" { psk = c.Obfs.TLSMimic.Password } wrapped, err := obfs.WrapPacketConnMulti(packetConn, obfs.MultiObfsOptions{ Password: psk, MinPadding: c.Obfs.MinPadding, MaxPadding: c.Obfs.MaxPadding, }) if err != nil { return nil, configError{Field: "obfs.multi", Err: err} } packetConn = wrapped default: return nil, configError{Field: "obfs.type", Err: errors.New("unsupported obfuscation type")} } switch strings.ToLower(c.Noise.Type) { case "", "none": case "filemask": maxRate := c.Noise.FileMask.MaxRate if maxRate == "" { maxRate = "512 kbps" } maxRateBPS, err := utils.ConvBandwidth(maxRate) if err != nil { return nil, configError{Field: "noise.filemask.maxRate", Err: err} } wrapped, err := obfs.WrapPacketConnFileMask(packetConn, obfs.FileMaskOptions{ Dir: c.Noise.FileMask.Dir, MaxRate: int(maxRateBPS), MinPacketSize: c.Noise.FileMask.MinPacketSize, MaxPacketSize: c.Noise.FileMask.MaxPacketSize, IdleThreshold: c.Noise.FileMask.IdleThreshold, }) if err != nil { return nil, configError{Field: "noise.filemask", Err: err} } packetConn = wrapped default: return nil, configError{Field: "noise.type", Err: errors.New("unsupported noise type")} } return packetConn, nil } func resolveServerListenAddr(listenAddr string) (*net.UDPAddr, eUtils.PortUnion, error) { host, portStr, err := net.SplitHostPort(listenAddr) if err != nil { uAddr, resolveErr := net.ResolveUDPAddr("udp", listenAddr) return uAddr, nil, resolveErr } if !strings.ContainsAny(portStr, "-,") { uAddr, resolveErr := net.ResolveUDPAddr("udp", listenAddr) return uAddr, nil, resolveErr } portUnion := eUtils.ParsePortUnion(portStr) if portUnion == nil { return nil, nil, fmt.Errorf("%s is not a valid port number or range", portStr) } firstListenAddr := net.JoinHostPort(host, strconv.Itoa(int(portUnion[0].Start))) uAddr, err := net.ResolveUDPAddr("udp", firstListenAddr) return uAddr, portUnion, err } func (c *serverConfig) startRealmServerRuntime(ctx context.Context, cancel context.CancelFunc, addr *realm.Addr, punchConn *realm.PunchPacketConn, family realm.AddrFamily) (*realmServerRuntime, error) { stunServers := c.realmSTUNServers(addr) rClient, err := realm.NewClientFromAddr(addr, c.realmHTTPClient()) if err != nil { return nil, configError{Field: "realm", Err: err} } puncher, err := realm.NewServerPuncher(ctx, punchConn) if err != nil { return nil, configError{Field: "realm", Err: err} } rt := &realmServerRuntime{ cancel: cancel, client: rClient, realmID: addr.RealmID, punchConn: punchConn, stunServers: stunServers, puncher: puncher, config: c.Realm, family: family, } // Gateway port mapping (UPnP/NAT-PMP) runs before STUN. // With the pinhole in place, in a double-NAT setup, // the address STUN observes corresponds to a path whose inner leg // goes through the static mapping rather than a filtered dynamic one. if c.Realm.PortMapping.Enabled { localPort := 0 if udpAddr, ok := punchConn.LocalAddr().(*net.UDPAddr); ok { localPort = udpAddr.Port } rt.mapper = newRealmPortMapper(ctx, addr.RealmID, localPort, c.Realm.PortMapping) } cleanupMapper := func() { if rt.mapper != nil { _ = rt.mapper.Close() } } if _, _, err := rt.refreshAddrsDirect(ctx); err != nil { cleanupMapper() return nil, configError{Field: "realm.stun", Err: err} } initialSession, err := rt.register(ctx) if err != nil { cleanupMapper() return nil, configError{Field: "realm.register", Err: err} } rt.setSession(initialSession) if rt.mapper != nil { go realmPortMapLoop(ctx, addr.RealmID, rt.mapper) } go rt.run(ctx, initialSession) return rt, nil } func (c *serverConfig) realmSTUNServers(addr *realm.Addr) []string { if stunServers := addr.Params["stun"]; len(stunServers) > 0 { return append([]string(nil), stunServers...) } if len(c.Realm.STUNServers) > 0 { return append([]string(nil), c.Realm.STUNServers...) } return append([]string(nil), defaultRealmSTUNServers...) } func (c *serverConfig) realmHTTPClient() *http.Client { if !c.Realm.Insecure { return nil } tr := http.DefaultTransport.(*http.Transport).Clone() tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} return &http.Client{Transport: tr} } const realmConnectSTUNCacheTTL = 10 * time.Second type realmServerRuntime struct { cancel context.CancelFunc client *realm.Client realmID string punchConn *realm.PunchPacketConn stunServers []string puncher *realm.ServerPuncher config serverConfigRealm family realm.AddrFamily mapper *realm.PortMapper // nil if port mapping is disabled or failed mu sync.Mutex session realmSession addrs []netip.AddrPort addrsAt time.Time connectSF singleflight.Group } type realmSession struct { id string ttl int } var ( errRealmSessionInvalid = errors.New("realm session invalid") errRealmSessionLost = errors.New("realm session lost") ) func (r *realmServerRuntime) run(ctx context.Context, sess realmSession) { for ctx.Err() == nil { if err := r.runSession(ctx, sess); err != nil && ctx.Err() == nil { logger.Warn("realm session lost", zap.String("realm", r.realmID), zap.Error(err)) } sess = r.registerWithBackoff(ctx) if sess.id == "" { return } } } func (r *realmServerRuntime) registerWithBackoff(ctx context.Context) realmSession { backoff := time.Second for ctx.Err() == nil { if _, _, err := r.refreshAddrs(ctx); err != nil { logger.Warn("realm STUN refresh before re-register failed", zap.String("realm", r.realmID), zap.Error(err)) } sess, err := r.register(ctx) if err == nil { r.setSession(sess) return sess } if isRealmRegisterFatal(err) { logger.Error("realm re-register rejected; giving up", zap.String("realm", r.realmID), zap.Error(err)) return realmSession{} } logger.Warn("realm re-register failed", zap.String("realm", r.realmID), zap.Error(err)) logger.Debug("realm re-register scheduled", zap.String("realm", r.realmID), zap.String("backoff", formatLogDuration(backoff))) if !sleepContext(ctx, backoff) { return realmSession{} } if backoff < 30*time.Second { backoff *= 2 } } return realmSession{} } func (r *realmServerRuntime) register(ctx context.Context) (realmSession, error) { localAddrs := r.currentAddrs() logger.Debug("realm registration started", zap.String("realm", r.realmID), zap.Strings("addresses", addrPortStrings(localAddrs))) start := time.Now() registerResp, err := r.client.Register(ctx, r.realmID, addrPortStrings(localAddrs)) if err != nil { return realmSession{}, err } sess := realmSession{id: registerResp.SessionID, ttl: registerResp.TTL} logger.Debug("realm registration completed", zap.String("realm", r.realmID), zap.Int("ttl", sess.ttl), zap.String("duration", formatLogDuration(time.Since(start)))) logger.Info("realm registered", zap.String("realm", r.realmID), zap.Strings("addresses", addrPortStrings(localAddrs)), zap.Int("ttl", sess.ttl)) return sess, nil } func (r *realmServerRuntime) runSession(ctx context.Context, sess realmSession) error { sessionCtx, cancel := context.WithCancel(ctx) defer cancel() errCh := make(chan error, 2) go func() { errCh <- r.heartbeatLoop(sessionCtx, sess) }() go func() { errCh <- r.eventsLoop(sessionCtx, sess) }() err := <-errCh cancel() return err } func (r *realmServerRuntime) heartbeatLoop(ctx context.Context, sess realmSession) error { interval := r.config.HeartbeatInterval if interval == 0 { interval = sessionTTLDuration(sess.ttl) / 2 if interval <= 0 { interval = 15 * time.Second } } t := time.NewTicker(interval) defer t.Stop() lastOK := time.Now() lastPublished := r.currentAddrs() for { select { case <-ctx.Done(): logger.Debug("realm heartbeat loop stopped", zap.String("realm", r.realmID)) return ctx.Err() case <-t.C: logger.Debug("realm heartbeat started", zap.String("realm", r.realmID)) start := time.Now() req := realm.HeartbeatRequest{} if current := r.currentAddrs(); !slices.Equal(current, lastPublished) { req.Addresses = addrPortStrings(current) lastPublished = current logger.Debug("realm addresses changed", zap.String("realm", r.realmID), zap.Strings("addresses", req.Addresses)) } resp, err := r.client.Heartbeat(ctx, r.realmID, sess.id, req) if err != nil { if isRealmSessionInvalid(err) { return errRealmSessionInvalid } logger.Warn("realm heartbeat failed", zap.String("realm", r.realmID), zap.Error(err)) if time.Since(lastOK) > sessionTTLDuration(sess.ttl) { return errRealmSessionLost } continue } lastOK = time.Now() logger.Debug("realm heartbeat completed", zap.String("realm", r.realmID), zap.Int("ttl", resp.TTL), zap.Bool("addressesUpdated", len(req.Addresses) > 0), zap.String("duration", formatLogDuration(time.Since(start)))) if r.config.HeartbeatInterval == 0 && resp.TTL > 0 { next := time.Duration(resp.TTL) * time.Second / 2 if next > 0 && next != interval { interval = next t.Reset(interval) } } } } } func (r *realmServerRuntime) eventsLoop(ctx context.Context, sess realmSession) error { backoff := time.Second lastOK := time.Now() for { if ctx.Err() != nil { logger.Debug("realm events loop stopped", zap.String("realm", r.realmID)) return ctx.Err() } logger.Debug("realm events stream connecting", zap.String("realm", r.realmID)) stream, err := r.client.Events(ctx, r.realmID, sess.id) if err != nil { if isRealmSessionInvalid(err) { return errRealmSessionInvalid } logger.Warn("realm events stream failed", zap.String("realm", r.realmID), zap.Error(err)) if time.Since(lastOK) > sessionTTLDuration(sess.ttl) { return errRealmSessionLost } logger.Debug("realm events stream reconnect scheduled", zap.String("realm", r.realmID), zap.String("backoff", formatLogDuration(backoff))) if !sleepContext(ctx, backoff) { return ctx.Err() } if backoff < 30*time.Second { backoff *= 2 } continue } lastOK = time.Now() logger.Debug("realm events stream connected", zap.String("realm", r.realmID)) backoff = time.Second for { ev, err := stream.Next() if err != nil { _ = stream.Close() if ctx.Err() == nil { logger.Warn("realm events stream dropped", zap.String("realm", r.realmID), zap.Error(err)) } break } lastOK = time.Now() logger.Debug("realm punch event received", zap.String("realm", r.realmID), zap.String("attempt", shortAttempt(ev.Nonce)), zap.Strings("addresses", ev.Addresses)) go r.respond(ctx, ev) } } } func (r *realmServerRuntime) connectAddrs(ctx context.Context) ([]netip.AddrPort, error) { if cached := r.cachedAddrs(); cached != nil { return cached, nil } v, err, _ := r.connectSF.Do("stun", func() (any, error) { if cached := r.cachedAddrs(); cached != nil { return cached, nil } addrs, _, err := r.refreshAddrs(ctx) if err != nil { return nil, err } return addrs, nil }) if err != nil { if fallback := r.currentAddrs(); len(fallback) > 0 { return fallback, err } return nil, err } return v.([]netip.AddrPort), nil } func (r *realmServerRuntime) cachedAddrs() []netip.AddrPort { r.mu.Lock() if r.addrs == nil || time.Since(r.addrsAt) >= realmConnectSTUNCacheTTL { r.mu.Unlock() return nil } addrs := append([]netip.AddrPort(nil), r.addrs...) r.mu.Unlock() return r.withMappedAddr(addrs) } func (r *realmServerRuntime) withMappedAddr(addrs []netip.AddrPort) []netip.AddrPort { if r.mapper == nil { return addrs } return mergeMappedAddr(addrs, r.mapper.ExternalAddr()) } func (r *realmServerRuntime) respond(ctx context.Context, ev *realm.PunchEvent) { attempt := shortAttempt(ev.Nonce) peerAddrs, err := parseAddrPorts(ev.Addresses) if err != nil { logger.Warn("invalid realm punch addresses", zap.String("realm", r.realmID), zap.String("attempt", attempt), zap.Error(err)) return } freshAddrs, stunErr := r.connectAddrs(ctx) if stunErr != nil { logger.Warn("realm connect STUN failed; using last-known addresses", zap.String("realm", r.realmID), zap.String("attempt", attempt), zap.Error(stunErr)) } if sess := r.currentSession(); sess.id != "" && len(freshAddrs) > 0 { postCtx, cancel := context.WithTimeout(ctx, 4*time.Second) err := r.client.ConnectResponse(postCtx, r.realmID, sess.id, ev.Nonce, addrPortStrings(freshAddrs)) cancel() if err != nil { logger.Warn("realm connect-response post failed", zap.String("realm", r.realmID), zap.String("attempt", attempt), zap.Error(err)) } } logger.Debug("realm punch response started", zap.String("realm", r.realmID), zap.String("attempt", attempt), zap.Strings("candidates", ev.Addresses)) start := time.Now() result, err := r.puncher.Respond(ctx, ev.Nonce, freshAddrs, peerAddrs, ev.PunchMetadata, realm.PunchConfig{ Timeout: r.config.PunchTimeout, Family: r.family, }) if err != nil { logger.Warn("realm punch failed", zap.String("realm", r.realmID), zap.String("attempt", attempt), zap.Error(err)) return } logger.Debug("realm punch completed", zap.String("realm", r.realmID), zap.String("attempt", attempt), zap.String("peer", result.PeerAddr.String()), zap.String("packet", punchPacketTypeString(result.Packet.Type)), zap.String("duration", formatLogDuration(time.Since(start)))) } func (r *realmServerRuntime) Close() error { r.cancel() sess := r.currentSession() if sess.id == "" { return nil } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() logger.Debug("realm deregister started", zap.String("realm", r.realmID)) if err := r.client.Deregister(ctx, r.realmID, sess.id); err != nil { return err } logger.Info("realm deregistered", zap.String("realm", r.realmID)) return nil } func (r *realmServerRuntime) setSession(sess realmSession) { r.mu.Lock() r.session = sess r.mu.Unlock() } func (r *realmServerRuntime) currentSession() realmSession { r.mu.Lock() defer r.mu.Unlock() return r.session } func (r *realmServerRuntime) refreshAddrs(ctx context.Context) ([]netip.AddrPort, bool, error) { return r.refreshAddrsWith(ctx, func(ctx context.Context, config realm.STUNConfig) ([]netip.AddrPort, error) { return realm.DiscoverWithDemux(ctx, r.punchConn, config) }) } func (r *realmServerRuntime) refreshAddrsDirect(ctx context.Context) ([]netip.AddrPort, bool, error) { return r.refreshAddrsWith(ctx, func(ctx context.Context, config realm.STUNConfig) ([]netip.AddrPort, error) { return realm.Discover(ctx, r.punchConn.PacketConn, config) }) } func (r *realmServerRuntime) refreshAddrsWith(ctx context.Context, discover func(context.Context, realm.STUNConfig) ([]netip.AddrPort, error)) ([]netip.AddrPort, bool, error) { logger.Debug("realm server STUN discovery started", zap.String("realm", r.realmID), zap.Strings("stunServers", r.stunServers)) start := time.Now() addrs, err := discover(ctx, realm.STUNConfig{ Servers: r.stunServers, Timeout: r.config.STUNTimeout, Family: r.family, }) if err != nil { return nil, false, err } r.mu.Lock() changed := !slices.Equal(r.addrs, addrs) if changed { r.addrs = append([]netip.AddrPort(nil), addrs...) } r.addrsAt = time.Now() current := append([]netip.AddrPort(nil), r.addrs...) r.mu.Unlock() logger.Debug("realm server STUN discovery completed", zap.String("realm", r.realmID), zap.Strings("addresses", addrPortStrings(current)), zap.Bool("changed", changed), zap.String("duration", formatLogDuration(time.Since(start)))) return r.withMappedAddr(current), changed, nil } func (r *realmServerRuntime) currentAddrs() []netip.AddrPort { r.mu.Lock() addrs := append([]netip.AddrPort(nil), r.addrs...) r.mu.Unlock() return r.withMappedAddr(addrs) } func sessionTTLDuration(ttl int) time.Duration { if ttl <= 0 { return time.Minute } return time.Duration(ttl) * time.Second } func isRealmSessionInvalid(err error) bool { var statusErr *realm.StatusError return errors.As(err, &statusErr) && (statusErr.StatusCode == http.StatusUnauthorized || statusErr.StatusCode == http.StatusNotFound) } func isRealmRegisterFatal(err error) bool { var statusErr *realm.StatusError return errors.As(err, &statusErr) && statusErr.StatusCode == http.StatusBadRequest } func sleepContext(ctx context.Context, d time.Duration) bool { t := time.NewTimer(d) defer t.Stop() select { case <-ctx.Done(): return false case <-t.C: return true } } func (c *serverConfig) fillTLSConfig(hyConfig *server.Config) error { if c.TLS == nil && c.ACME == nil { return configError{Field: "tls", Err: errors.New("must set either tls or acme")} } if c.TLS != nil && c.ACME != nil { return configError{Field: "tls", Err: errors.New("cannot set both tls and acme")} } if c.TLS != nil { // SNI guard var sniGuard utils.SNIGuardFunc switch strings.ToLower(c.TLS.SNIGuard) { case "", "dns-san": sniGuard = utils.SNIGuardDNSSAN case "strict": sniGuard = utils.SNIGuardStrict case "disable": sniGuard = nil default: return configError{Field: "tls.sniGuard", Err: errors.New("unsupported SNI guard")} } // Local TLS cert if c.TLS.Cert == "" || c.TLS.Key == "" { return configError{Field: "tls", Err: errors.New("empty cert or key path")} } certLoader := &utils.LocalCertificateLoader{ CertFile: c.TLS.Cert, KeyFile: c.TLS.Key, SNIGuard: sniGuard, } // Try loading the cert-key pair here to catch errors early // (e.g. invalid files or insufficient permissions) err := certLoader.InitializeCache() if err != nil { var pathErr *os.PathError if errors.As(err, &pathErr) { if pathErr.Path == c.TLS.Cert { return configError{Field: "tls.cert", Err: pathErr} } if pathErr.Path == c.TLS.Key { return configError{Field: "tls.key", Err: pathErr} } } return configError{Field: "tls", Err: err} } // Use GetCertificate instead of Certificates so that // users can update the cert without restarting the server. hyConfig.TLSConfig.GetCertificate = certLoader.GetCertificate // Client CA if c.TLS.ClientCA != "" { ca, err := os.ReadFile(c.TLS.ClientCA) if err != nil { return configError{Field: "tls.clientCA", Err: err} } cPool := x509.NewCertPool() if !cPool.AppendCertsFromPEM(ca) { return configError{Field: "tls.clientCA", Err: errors.New("failed to parse client CA certificate")} } hyConfig.TLSConfig.ClientCAs = cPool } } else { // ACME dataDir := c.ACME.Dir if dataDir == "" { // If not specified in the config, check the environment variable // before resorting to the default "acme" value. The main reason // we have this is so that our setup script can set it to the // user's home directory. dataDir = envOrDefaultString(appACMEDirEnv, "acme") } cmCfg := &certmagic.Config{ RenewalWindowRatio: certmagic.DefaultRenewalWindowRatio, KeySource: certmagic.DefaultKeyGenerator, Storage: &certmagic.FileStorage{Path: dataDir}, Logger: logger, } cmIssuer := certmagic.NewACMEIssuer(cmCfg, certmagic.ACMEIssuer{ Email: c.ACME.Email, Agreed: true, ListenHost: c.ACME.ListenHost, Logger: logger, }) switch strings.ToLower(c.ACME.CA) { case "letsencrypt", "le", "": // Default to Let's Encrypt cmIssuer.CA = certmagic.LetsEncryptProductionCA case "zerossl", "zero": cmIssuer.CA = certmagic.ZeroSSLProductionCA eab, err := genZeroSSLEAB(c.ACME.Email) if err != nil { return configError{Field: "acme.ca", Err: err} } cmIssuer.ExternalAccount = eab default: return configError{Field: "acme.ca", Err: errors.New("unsupported CA")} } switch strings.ToLower(c.ACME.Type) { case "http": cmIssuer.DisableHTTPChallenge = false cmIssuer.DisableTLSALPNChallenge = true cmIssuer.DNS01Solver = nil cmIssuer.AltHTTPPort = c.ACME.HTTP.AltPort case "tls": cmIssuer.DisableHTTPChallenge = true cmIssuer.DisableTLSALPNChallenge = false cmIssuer.DNS01Solver = nil cmIssuer.AltTLSALPNPort = c.ACME.TLS.AltPort case "dns": cmIssuer.DisableHTTPChallenge = true cmIssuer.DisableTLSALPNChallenge = true if c.ACME.DNS.Name == "" { return configError{Field: "acme.dns.name", Err: errors.New("empty DNS provider name")} } if c.ACME.DNS.Config == nil { return configError{Field: "acme.dns.config", Err: errors.New("empty DNS provider config")} } switch strings.ToLower(c.ACME.DNS.Name) { case "cloudflare": cmIssuer.DNS01Solver = &certmagic.DNS01Solver{ DNSProvider: &cloudflare.Provider{ APIToken: c.ACME.DNS.Config["cloudflare_api_token"], }, } case "duckdns": cmIssuer.DNS01Solver = &certmagic.DNS01Solver{ DNSProvider: &duckdns.Provider{ APIToken: c.ACME.DNS.Config["duckdns_api_token"], OverrideDomain: c.ACME.DNS.Config["duckdns_override_domain"], }, } case "gandi": cmIssuer.DNS01Solver = &certmagic.DNS01Solver{ DNSProvider: &gandi.Provider{ BearerToken: c.ACME.DNS.Config["gandi_api_token"], }, } case "godaddy": cmIssuer.DNS01Solver = &certmagic.DNS01Solver{ DNSProvider: &godaddy.Provider{ APIToken: c.ACME.DNS.Config["godaddy_api_token"], }, } case "namedotcom": cmIssuer.DNS01Solver = &certmagic.DNS01Solver{ DNSProvider: &namedotcom.Provider{ Token: c.ACME.DNS.Config["namedotcom_token"], User: c.ACME.DNS.Config["namedotcom_user"], Server: c.ACME.DNS.Config["namedotcom_server"], }, } case "vultr": cmIssuer.DNS01Solver = &certmagic.DNS01Solver{ DNSProvider: &vultr.Provider{ APIToken: c.ACME.DNS.Config["vultr_api_token"], }, } default: return configError{Field: "acme.dns.name", Err: errors.New("unsupported DNS provider")} } case "": // Legacy compatibility mode cmIssuer.DisableHTTPChallenge = c.ACME.DisableHTTP cmIssuer.DisableTLSALPNChallenge = c.ACME.DisableTLSALPN cmIssuer.AltHTTPPort = c.ACME.AltHTTPPort cmIssuer.AltTLSALPNPort = c.ACME.AltTLSALPNPort default: return configError{Field: "acme.type", Err: errors.New("unsupported ACME type")} } cmCfg.Issuers = []certmagic.Issuer{cmIssuer} cmCache := certmagic.NewCache(certmagic.CacheOptions{ GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) { return cmCfg, nil }, Logger: logger, }) cmCfg = certmagic.New(cmCache, *cmCfg) if len(c.ACME.Domains) == 0 { return configError{Field: "acme.domains", Err: errors.New("empty domains")} } err := cmCfg.ManageSync(context.Background(), c.ACME.Domains) if err != nil { return configError{Field: "acme.domains", Err: err} } hyConfig.TLSConfig.GetCertificate = cmCfg.GetCertificate } return nil } func genZeroSSLEAB(email string) (*acme.EAB, error) { req, err := http.NewRequest( http.MethodPost, "https://api.zerossl.com/acme/eab-credentials-email", strings.NewReader(url.Values{"email": []string{email}}.Encode()), ) if err != nil { return nil, fmt.Errorf("failed to creare ZeroSSL EAB request: %w", err) } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("User-Agent", certmagic.UserAgent) resp, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to send ZeroSSL EAB request: %w", err) } defer func() { _ = resp.Body.Close() }() var result struct { Success bool `json:"success"` Error struct { Code int `json:"code"` Type string `json:"type"` } `json:"error"` EABKID string `json:"eab_kid"` EABHMACKey string `json:"eab_hmac_key"` } if err = json.NewDecoder(resp.Body).Decode(&result); err != nil { return nil, fmt.Errorf("failed decoding ZeroSSL EAB API response: %w", err) } if result.Error.Code != 0 { return nil, fmt.Errorf("failed getting ZeroSSL EAB credentials: HTTP %d: %s (code %d)", resp.StatusCode, result.Error.Type, result.Error.Code) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("failed getting EAB credentials: HTTP %d", resp.StatusCode) } return &acme.EAB{ KeyID: result.EABKID, MACKey: result.EABHMACKey, }, nil } func (c *serverConfig) fillQUICConfig(hyConfig *server.Config) error { hyConfig.QUICConfig = server.QUICConfig{ InitialStreamReceiveWindow: c.QUIC.InitStreamReceiveWindow, MaxStreamReceiveWindow: c.QUIC.MaxStreamReceiveWindow, InitialConnectionReceiveWindow: c.QUIC.InitConnectionReceiveWindow, MaxConnectionReceiveWindow: c.QUIC.MaxConnectionReceiveWindow, MaxIdleTimeout: c.QUIC.MaxIdleTimeout, MaxIncomingStreams: c.QUIC.MaxIncomingStreams, DisablePathMTUDiscovery: c.QUIC.DisablePathMTUDiscovery, } return nil } func serverConfigOutboundDirectToOutbound(c serverConfigOutboundDirect) (outbounds.PluggableOutbound, error) { opts := outbounds.DirectOutboundOptions{} switch strings.ToLower(c.Mode) { case "", "auto": opts.Mode = outbounds.DirectOutboundModeAuto case "64": opts.Mode = outbounds.DirectOutboundMode64 case "46": opts.Mode = outbounds.DirectOutboundMode46 case "6": opts.Mode = outbounds.DirectOutboundMode6 case "4": opts.Mode = outbounds.DirectOutboundMode4 default: return nil, configError{Field: "outbounds.direct.mode", Err: errors.New("unsupported mode")} } bindIP := len(c.BindIPv4) > 0 || len(c.BindIPv6) > 0 bindDevice := len(c.BindDevice) > 0 if bindIP && bindDevice { return nil, configError{Field: "outbounds.direct", Err: errors.New("cannot bind both IP and device")} } if bindIP { ip4, ip6 := net.ParseIP(c.BindIPv4), net.ParseIP(c.BindIPv6) if len(c.BindIPv4) > 0 && ip4 == nil { return nil, configError{Field: "outbounds.direct.bindIPv4", Err: errors.New("invalid IPv4 address")} } if len(c.BindIPv6) > 0 && ip6 == nil { return nil, configError{Field: "outbounds.direct.bindIPv6", Err: errors.New("invalid IPv6 address")} } opts.BindIP4 = ip4 opts.BindIP6 = ip6 } if bindDevice { opts.DeviceName = c.BindDevice } opts.FastOpen = c.FastOpen return outbounds.NewDirectOutboundWithOptions(opts) } func serverConfigOutboundSOCKS5ToOutbound(c serverConfigOutboundSOCKS5) (outbounds.PluggableOutbound, error) { if c.Addr == "" { return nil, configError{Field: "outbounds.socks5.addr", Err: errors.New("empty socks5 address")} } return outbounds.NewSOCKS5Outbound(c.Addr, c.Username, c.Password), nil } func serverConfigOutboundHTTPToOutbound(c serverConfigOutboundHTTP) (outbounds.PluggableOutbound, error) { if c.URL == "" { return nil, configError{Field: "outbounds.http.url", Err: errors.New("empty http address")} } return outbounds.NewHTTPOutbound(c.URL, c.Insecure) } func serverConfigOutboundHysteriaToOutbound(c serverConfigOutboundHysteria) (outbounds.PluggableOutbound, error) { if c.Server == "" { return nil, configError{Field: "outbounds.hysteria.server", Err: errors.New("empty server address")} } if c.Auth == "" { return nil, configError{Field: "outbounds.hysteria.auth", Err: errors.New("empty auth")} } // TLS config tlsCfg := outbounds.HysteriaOutboundTLSConfig{ ServerName: c.TLS.SNI, InsecureSkipVerify: c.TLS.Insecure, PinSHA256: c.TLS.PinSHA256, } if c.TLS.CA != "" { ca, err := os.ReadFile(c.TLS.CA) if err != nil { return nil, configError{Field: "outbounds.hysteria.tls.ca", Err: err} } cPool := x509.NewCertPool() if !cPool.AppendCertsFromPEM(ca) { return nil, configError{Field: "outbounds.hysteria.tls.ca", Err: errors.New("failed to parse CA certificate")} } tlsCfg.RootCAs = cPool } // Normalize congestion type normalizedType, err := normalizeCongestionType(c.Congestion.Type) if err != nil { return nil, configError{Field: "outbounds.hysteria.congestion.type", Err: err} } var bbrProfile string if normalizedType == congestionTypeBBR { bbrProfile, err = normalizeBBRProfile(c.Congestion.BBRProfile) if err != nil { return nil, configError{Field: "outbounds.hysteria.congestion.bbrProfile", Err: err} } } // Obfuscation config obfsPassword := "" switch strings.ToLower(c.Obfs.Type) { case "salamander": obfsPassword = c.Obfs.Salamander.Password case "gecko": obfsPassword = c.Obfs.Gecko.Password case "tlsmimic": obfsPassword = c.Obfs.TLSMimic.Password } cfg := &outbounds.HysteriaOutboundConfig{ ServerAddr: c.Server, Auth: c.Auth, TLSConfig: tlsCfg, QUICConfig: client.QUICConfig{}, BandwidthConfig: client.BandwidthConfig{}, CongestionType: normalizedType, BBRProfile: bbrProfile, FastOpen: c.FastOpen, Obfs: outbounds.HysteriaOutboundObfsConfig{ Type: c.Obfs.Type, Password: obfsPassword, MinPacketSize: c.Obfs.Gecko.MinPacketSize, MaxPacketSize: c.Obfs.Gecko.MaxPacketSize, }, Transport: outbounds.HysteriaOutboundTransportConfig{ Type: c.Transport.Type, HopInterval: c.Transport.UDP.HopInterval, MinHopInterval: c.Transport.UDP.MinHopInterval, MaxHopInterval: c.Transport.UDP.MaxHopInterval, }, } // QUIC config if c.QUIC.InitStreamReceiveWindow != 0 { cfg.QUICConfig.InitialStreamReceiveWindow = c.QUIC.InitStreamReceiveWindow } if c.QUIC.MaxStreamReceiveWindow != 0 { cfg.QUICConfig.MaxStreamReceiveWindow = c.QUIC.MaxStreamReceiveWindow } if c.QUIC.InitConnectionReceiveWindow != 0 { cfg.QUICConfig.InitialConnectionReceiveWindow = c.QUIC.InitConnectionReceiveWindow } if c.QUIC.MaxConnectionReceiveWindow != 0 { cfg.QUICConfig.MaxConnectionReceiveWindow = c.QUIC.MaxConnectionReceiveWindow } if c.QUIC.MaxIdleTimeout != 0 { cfg.QUICConfig.MaxIdleTimeout = c.QUIC.MaxIdleTimeout } if c.QUIC.DisablePathMTUDiscovery { cfg.QUICConfig.DisablePathMTUDiscovery = true } // Bandwidth config if c.Bandwidth.Up != "" { up, err := utils.ConvBandwidth(c.Bandwidth.Up) if err != nil { return nil, configError{Field: "outbounds.hysteria.bandwidth.up", Err: err} } cfg.BandwidthConfig.MaxTx = up } if c.Bandwidth.Down != "" { down, err := utils.ConvBandwidth(c.Bandwidth.Down) if err != nil { return nil, configError{Field: "outbounds.hysteria.bandwidth.down", Err: err} } cfg.BandwidthConfig.MaxRx = down } return outbounds.NewHysteriaOutbound(cfg) } func (c *serverConfig) fillRequestHook(hyConfig *server.Config) error { if c.Sniff.Enable { s := &sniff.Sniffer{ Timeout: c.Sniff.Timeout, RewriteDomain: c.Sniff.RewriteDomain, } if c.Sniff.TCPPorts != "" { s.TCPPorts = eUtils.ParsePortUnion(c.Sniff.TCPPorts) if s.TCPPorts == nil { return configError{Field: "sniff.tcpPorts", Err: errors.New("invalid port union")} } } if c.Sniff.UDPPorts != "" { s.UDPPorts = eUtils.ParsePortUnion(c.Sniff.UDPPorts) if s.UDPPorts == nil { return configError{Field: "sniff.udpPorts", Err: errors.New("invalid port union")} } } hyConfig.RequestHook = s } return nil } func (c *serverConfig) fillOutboundConfig(hyConfig *server.Config) error { // Resolver, ACL, actual outbound are all implemented through the Outbound interface. // Depending on the config, we build a chain like this: // Resolver(ACL(Outbounds...)) // Outbounds var obs []outbounds.OutboundEntry if len(c.Outbounds) == 0 { // Guarantee we have at least one outbound obs = []outbounds.OutboundEntry{{ Name: "default", Outbound: outbounds.NewDirectOutboundSimple(outbounds.DirectOutboundModeAuto), }} } else { obs = make([]outbounds.OutboundEntry, len(c.Outbounds)) for i, entry := range c.Outbounds { if entry.Name == "" { return configError{Field: "outbounds.name", Err: errors.New("empty outbound name")} } var ob outbounds.PluggableOutbound var err error switch strings.ToLower(entry.Type) { case "direct": ob, err = serverConfigOutboundDirectToOutbound(entry.Direct) case "socks5": ob, err = serverConfigOutboundSOCKS5ToOutbound(entry.SOCKS5) case "http": ob, err = serverConfigOutboundHTTPToOutbound(entry.HTTP) case "hysteria": ob, err = serverConfigOutboundHysteriaToOutbound(entry.Hysteria) default: err = configError{Field: "outbounds.type", Err: errors.New("unsupported outbound type")} } if err != nil { return err } obs[i] = outbounds.OutboundEntry{Name: entry.Name, Outbound: ob} } } var uOb outbounds.PluggableOutbound // "unified" outbound // ACL hasACL := false if c.ACL.File != "" && len(c.ACL.Inline) > 0 { return configError{Field: "acl", Err: errors.New("cannot set both acl.file and acl.inline")} } gLoader := &utils.GeoLoader{ GeoIPFilename: c.ACL.GeoIP, GeoSiteFilename: c.ACL.GeoSite, UpdateInterval: c.ACL.GeoUpdateInterval, DownloadFunc: geoDownloadFunc, DownloadErrFunc: geoDownloadErrFunc, } if c.ACL.File != "" { hasACL = true acl, err := outbounds.NewACLEngineFromFile(c.ACL.File, obs, gLoader) if err != nil { return configError{Field: "acl.file", Err: err} } uOb = acl } else if len(c.ACL.Inline) > 0 { hasACL = true acl, err := outbounds.NewACLEngineFromString(strings.Join(c.ACL.Inline, "\n"), obs, gLoader) if err != nil { return configError{Field: "acl.inline", Err: err} } uOb = acl } else { // No ACL, use the first outbound uOb = obs[0].Outbound } // Resolver switch strings.ToLower(c.Resolver.Type) { case "", "system": if hasACL { // If the user uses ACL, we must put a resolver in front of it, // for IP rules to work on domain requests. uOb = outbounds.NewSystemResolver(uOb) } // Otherwise we can just rely on outbound handling on its own. case "tcp": if c.Resolver.TCP.Addr == "" { return configError{Field: "resolver.tcp.addr", Err: errors.New("empty resolver address")} } uOb = outbounds.NewStandardResolverTCP(c.Resolver.TCP.Addr, c.Resolver.TCP.Timeout, uOb) case "udp": if c.Resolver.UDP.Addr == "" { return configError{Field: "resolver.udp.addr", Err: errors.New("empty resolver address")} } uOb = outbounds.NewStandardResolverUDP(c.Resolver.UDP.Addr, c.Resolver.UDP.Timeout, uOb) case "tls", "tcp-tls": if c.Resolver.TLS.Addr == "" { return configError{Field: "resolver.tls.addr", Err: errors.New("empty resolver address")} } uOb = outbounds.NewStandardResolverTLS(c.Resolver.TLS.Addr, c.Resolver.TLS.Timeout, c.Resolver.TLS.SNI, c.Resolver.TLS.Insecure, uOb) case "https", "http": if c.Resolver.HTTPS.Addr == "" { return configError{Field: "resolver.https.addr", Err: errors.New("empty resolver address")} } uOb = outbounds.NewDoHResolver(c.Resolver.HTTPS.Addr, c.Resolver.HTTPS.Timeout, c.Resolver.HTTPS.SNI, c.Resolver.HTTPS.Insecure, uOb) default: return configError{Field: "resolver.type", Err: errors.New("unsupported resolver type")} } // Speed test if c.SpeedTest { uOb = outbounds.NewSpeedtestHandler(uOb) } hyConfig.Outbound = &outbounds.PluggableOutboundAdapter{PluggableOutbound: uOb} return nil } func (c *serverConfig) fillBandwidthConfig(hyConfig *server.Config) error { var err error if c.Bandwidth.Up != "" { hyConfig.BandwidthConfig.MaxTx, err = utils.ConvBandwidth(c.Bandwidth.Up) if err != nil { return configError{Field: "bandwidth.up", Err: err} } } if c.Bandwidth.Down != "" { hyConfig.BandwidthConfig.MaxRx, err = utils.ConvBandwidth(c.Bandwidth.Down) if err != nil { return configError{Field: "bandwidth.down", Err: err} } } return nil } func (c *serverConfig) fillCongestionConfig(hyConfig *server.Config) error { normalizedType, err := normalizeCongestionType(c.Congestion.Type) if err != nil { return configError{Field: "congestion.type", Err: err} } hyConfig.CongestionConfig.Type = normalizedType if normalizedType == congestionTypeBBR { normalizedProfile, err := normalizeBBRProfile(c.Congestion.BBRProfile) if err != nil { return configError{Field: "congestion.bbrProfile", Err: err} } hyConfig.CongestionConfig.BBRProfile = normalizedProfile } return nil } func (c *serverConfig) fillIgnoreClientBandwidth(hyConfig *server.Config) error { hyConfig.IgnoreClientBandwidth = c.IgnoreClientBandwidth return nil } func (c *serverConfig) fillDisableUDP(hyConfig *server.Config) error { hyConfig.DisableUDP = c.DisableUDP return nil } func (c *serverConfig) fillUDPIdleTimeout(hyConfig *server.Config) error { hyConfig.UDPIdleTimeout = c.UDPIdleTimeout return nil } func (c *serverConfig) fillAuthenticator(hyConfig *server.Config) error { if c.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 configError{Field: "auth.password", Err: errors.New("empty auth password")} } hyConfig.Authenticator = &auth.PasswordAuthenticator{Password: c.Auth.Password} return nil case "userpass": if len(c.Auth.UserPass) == 0 { return configError{Field: "auth.userpass", Err: errors.New("empty auth userpass")} } hyConfig.Authenticator = auth.NewUserPassAuthenticator(c.Auth.UserPass) return nil case "http", "https": if c.Auth.HTTP.URL == "" { return configError{Field: "auth.http.url", Err: errors.New("empty auth http url")} } hyConfig.Authenticator = auth.NewHTTPAuthenticator(c.Auth.HTTP.URL, c.Auth.HTTP.Insecure) return nil case "command", "cmd": if c.Auth.Command == "" { return configError{Field: "auth.command", Err: errors.New("empty auth command")} } hyConfig.Authenticator = &auth.CommandAuthenticator{Cmd: c.Auth.Command} return nil default: return configError{Field: "auth.type", Err: errors.New("unsupported auth type")} } } func (c *serverConfig) fillEventLogger(hyConfig *server.Config) error { hyConfig.EventLogger = &serverLogger{} return nil } func (c *serverConfig) fillTrafficLogger(hyConfig *server.Config) error { if c.TrafficStats.Listen != "" { tss := trafficlogger.NewTrafficStatsServer(c.TrafficStats.Secret) hyConfig.TrafficLogger = tss go runTrafficStatsServer(c.TrafficStats.Listen, tss) } return nil } // fillMasqHandler must be called after fillConn, as we may need to extract the QUIC // port number from Conn for MasqTCPServer. func (c *serverConfig) fillMasqHandler(hyConfig *server.Config) error { var handler http.Handler switch strings.ToLower(c.Masquerade.Type) { case "", "404": handler = http.NotFoundHandler() case "file": if c.Masquerade.File.Dir == "" { return configError{Field: "masquerade.file.dir", Err: errors.New("empty file directory")} } handler = http.FileServer(http.Dir(c.Masquerade.File.Dir)) case "proxy": if c.Masquerade.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 configError{Field: "masquerade.proxy.url", Err: err} } if u.Scheme != "http" && u.Scheme != "https" { return configError{Field: "masquerade.proxy.url", Err: fmt.Errorf("unsupported protocol scheme \"%s\"", u.Scheme)} } transport := http.DefaultTransport if c.Masquerade.Proxy.Insecure { transport = &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, // use default configs from http.DefaultTransport Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } } handler = &httputil.ReverseProxy{ Rewrite: func(r *httputil.ProxyRequest) { r.SetURL(u) // SetURL rewrites the Host header, // but we don't want that if rewriteHost is false if !c.Masquerade.Proxy.RewriteHost { r.Out.Host = r.In.Host } if c.Masquerade.Proxy.XForwarded { r.SetXForwarded() } }, Transport: transport, ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { logger.Error("HTTP reverse proxy error", zap.Error(err)) w.WriteHeader(http.StatusBadGateway) }, } case "string": if c.Masquerade.String.Content == "" { return configError{Field: "masquerade.string.content", Err: errors.New("empty string content")} } if c.Masquerade.String.StatusCode != 0 && (c.Masquerade.String.StatusCode < 200 || c.Masquerade.String.StatusCode > 599 || c.Masquerade.String.StatusCode == 233) { // 233 is reserved for Hysteria authentication return configError{Field: "masquerade.string.statusCode", Err: errors.New("invalid status code (must be 200-599, except 233)")} } handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { for k, v := range c.Masquerade.String.Headers { w.Header().Set(k, v) } if c.Masquerade.String.StatusCode != 0 { w.WriteHeader(c.Masquerade.String.StatusCode) } else { w.WriteHeader(http.StatusOK) // Use 200 OK by default } _, _ = w.Write([]byte(c.Masquerade.String.Content)) }) default: return configError{Field: "masquerade.type", Err: errors.New("unsupported masquerade type")} } hyConfig.MasqHandler = &masqHandlerLogWrapper{H: handler, QUIC: true} if c.Masquerade.ListenHTTP != "" || c.Masquerade.ListenHTTPS != "" { if c.Masquerade.ListenHTTP != "" && c.Masquerade.ListenHTTPS == "" { return configError{Field: "masquerade.listenHTTPS", Err: errors.New("having only HTTP server without HTTPS is not supported")} } s := masq.MasqTCPServer{ QUICPort: extractPortFromAddr(hyConfig.Conn.LocalAddr().String()), HTTPSPort: extractPortFromAddr(c.Masquerade.ListenHTTPS), Handler: &masqHandlerLogWrapper{H: handler, QUIC: false}, TLSConfig: &tls.Config{ Certificates: hyConfig.TLSConfig.Certificates, GetCertificate: hyConfig.TLSConfig.GetCertificate, }, ForceHTTPS: c.Masquerade.ForceHTTPS, } go runMasqTCPServer(&s, c.Masquerade.ListenHTTP, c.Masquerade.ListenHTTPS) } return nil } // 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.fillRequestHook, c.fillOutboundConfig, c.fillCongestionConfig, c.fillBandwidthConfig, c.fillIgnoreClientBandwidth, c.fillDisableUDP, c.fillUDPIdleTimeout, c.fillAuthenticator, c.fillEventLogger, c.fillTrafficLogger, c.fillMasqHandler, } for _, f := range fillers { if err := f(hyConfig); err != nil { return nil, err } } return hyConfig, nil } func runServerCmd(cmd *cobra.Command, args []string) { logger.Info("server mode") runServer(defaultViper) } func runServer(v *viper.Viper) { if err := v.ReadInConfig(); err != nil { logger.Fatal("failed to read server config", zap.Error(err)) } var config serverConfig if err := v.Unmarshal(&config); err != nil { logger.Fatal("failed to parse server config", zap.Error(err)) } hyConfig, err := config.Config() if err != nil { logger.Fatal("failed to load server config", zap.Error(err)) } s, err := server.NewServer(hyConfig) if err != nil { logger.Fatal("failed to initialize server", zap.Error(err)) } if config.Listen != "" { logger.Info("server up and running", zap.String("listen", config.Listen)) } else { logger.Info("server up and running", zap.String("listen", defaultListenAddr)) } if !disableUpdateCheck { go runCheckUpdateServer() } var ns *network.NetworkServer if config.Network != nil && config.Network.Enabled { ns, err = network.NewNetworkServer(network.Config{ Enabled: true, Listen: config.Network.Listen, Token: config.Network.Token, TUN: network.TUNConfig{ Name: config.Network.TUN.Name, MTU: config.Network.TUN.MTU, }, Pool: config.Network.Pool, }) if err != nil { logger.Fatal("failed to initialize network server", zap.Error(err)) } go func() { if err := ns.Serve(); err != nil { logger.Fatal("network server error", zap.Error(err)) } }() } signalChan := make(chan os.Signal, 1) signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) defer signal.Stop(signalChan) serveErrChan := make(chan error, 1) go func() { serveErrChan <- s.Serve() }() select { case <-signalChan: logger.Info("received signal, shutting down gracefully") if ns != nil { ns.Close() } if err := s.Close(); err != nil { logger.Error("failed to shut down server cleanly", zap.Error(err)) } if err := <-serveErrChan; err != nil { logger.Info("server stopped", zap.Error(err)) } case err := <-serveErrChan: if err != nil { logger.Fatal("failed to serve", zap.Error(err)) } } } func runTrafficStatsServer(listen string, handler http.Handler) { logger.Info("traffic stats server up and running", zap.String("listen", listen)) if err := correctnet.HTTPListenAndServe(listen, handler); err != nil { logger.Fatal("failed to serve traffic stats", zap.Error(err)) } } func runMasqTCPServer(s *masq.MasqTCPServer, httpAddr, httpsAddr string) { errChan := make(chan error, 2) if httpAddr != "" { go func() { logger.Info("masquerade HTTP server up and running", zap.String("listen", httpAddr)) errChan <- s.ListenAndServeHTTP(httpAddr) }() } if httpsAddr != "" { go func() { logger.Info("masquerade HTTPS server up and running", zap.String("listen", httpsAddr)) errChan <- s.ListenAndServeHTTPS(httpsAddr) }() } err := <-errChan if err != nil { logger.Fatal("failed to serve masquerade HTTP(S)", zap.Error(err)) } } func geoDownloadFunc(filename, url string) { logger.Info("downloading database", zap.String("filename", filename), zap.String("url", url)) } func geoDownloadErrFunc(err error) { if err != nil { logger.Error("failed to download database", zap.Error(err)) } } type serverLogger struct{} func (l *serverLogger) Connect(addr net.Addr, id string, tx uint64) { logger.Info("client connected", zap.String("addr", addr.String()), zap.String("id", id), zap.Uint64("tx", tx)) } func (l *serverLogger) Disconnect(addr net.Addr, id string, err error) { logger.Info("client disconnected", zap.String("addr", addr.String()), zap.String("id", id), zap.Error(err)) } func (l *serverLogger) TCPRequest(addr net.Addr, id, reqAddr string) { logger.Debug("TCP request", zap.String("addr", addr.String()), zap.String("id", id), zap.String("reqAddr", reqAddr)) } func (l *serverLogger) TCPError(addr net.Addr, id, reqAddr string, err error) { if err == nil { logger.Debug("TCP closed", zap.String("addr", addr.String()), zap.String("id", id), zap.String("reqAddr", reqAddr)) } else { logger.Warn("TCP error", zap.String("addr", addr.String()), zap.String("id", id), zap.String("reqAddr", reqAddr), zap.Error(err)) } } func (l *serverLogger) UDPRequest(addr net.Addr, id string, sessionID uint32, reqAddr string) { logger.Debug("UDP request", zap.String("addr", addr.String()), zap.String("id", id), zap.Uint32("sessionID", sessionID), zap.String("reqAddr", reqAddr)) } func (l *serverLogger) UDPError(addr net.Addr, id string, sessionID uint32, err error) { if err == nil { logger.Debug("UDP closed", zap.String("addr", addr.String()), zap.String("id", id), zap.Uint32("sessionID", sessionID)) } else { logger.Warn("UDP error", zap.String("addr", addr.String()), zap.String("id", id), zap.Uint32("sessionID", sessionID), zap.Error(err)) } } type masqHandlerLogWrapper struct { H http.Handler QUIC bool } func (m *masqHandlerLogWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) { logger.Debug("masquerade request", zap.String("addr", r.RemoteAddr), zap.String("method", r.Method), zap.String("host", r.Host), zap.String("url", r.URL.String()), zap.Bool("quic", m.QUIC)) m.H.ServeHTTP(w, r) } func extractPortFromAddr(addr string) int { _, portStr, err := net.SplitHostPort(addr) if err != nil { return 0 } port, err := strconv.Atoi(portStr) if err != nil { return 0 } return port }