diff --git a/app/cmd/server.go b/app/cmd/server.go index 62b9f18..0beab1e 100644 --- a/app/cmd/server.go +++ b/app/cmd/server.go @@ -8,6 +8,7 @@ import ( "net/http" "net/http/httputil" "net/url" + "strconv" "strings" "time" @@ -19,6 +20,7 @@ import ( "github.com/apernet/hysteria/app/internal/utils" "github.com/apernet/hysteria/core/server" "github.com/apernet/hysteria/extras/auth" + "github.com/apernet/hysteria/extras/masq" "github.com/apernet/hysteria/extras/obfs" "github.com/apernet/hysteria/extras/outbounds" "github.com/apernet/hysteria/extras/trafficlogger" @@ -177,9 +179,12 @@ type serverConfigMasqueradeProxy struct { } type serverConfigMasquerade struct { - Type string `mapstructure:"type"` - File serverConfigMasqueradeFile `mapstructure:"file"` - Proxy serverConfigMasqueradeProxy `mapstructure:"proxy"` + Type string `mapstructure:"type"` + File serverConfigMasqueradeFile `mapstructure:"file"` + Proxy serverConfigMasqueradeProxy `mapstructure:"proxy"` + ListenHTTP string `mapstructure:"listenHTTP"` + ListenHTTPS string `mapstructure:"listenHTTPS"` + ForceHTTPS bool `mapstructure:"forceHTTPS"` } func (c *serverConfig) fillConn(hyConfig *server.Config) error { @@ -524,6 +529,8 @@ func (c *serverConfig) fillTrafficLogger(hyConfig *server.Config) error { 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) { @@ -559,7 +566,24 @@ func (c *serverConfig) fillMasqHandler(hyConfig *server.Config) error { default: return configError{Field: "masquerade.type", Err: errors.New("unsupported masquerade type")} } - hyConfig.MasqHandler = &masqHandlerLogWrapper{H: handler} + 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 } @@ -626,6 +650,26 @@ func runTrafficStatsServer(listen string, handler http.Handler) { } } +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 geoipDownloadFunc(filename, url string) { logger.Info("downloading GeoIP database", zap.String("filename", filename), zap.String("url", url)) } @@ -671,10 +715,28 @@ func (l *serverLogger) UDPError(addr net.Addr, id string, sessionID uint32, err } type masqHandlerLogWrapper struct { - H http.Handler + 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())) + 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 +} diff --git a/app/cmd/server_test.go b/app/cmd/server_test.go index 45d7e33..73bce1c 100644 --- a/app/cmd/server_test.go +++ b/app/cmd/server_test.go @@ -136,6 +136,9 @@ func TestServerConfig(t *testing.T) { URL: "https://some.site.net", RewriteHost: true, }, + ListenHTTP: ":80", + ListenHTTPS: ":443", + ForceHTTPS: true, }, }) } diff --git a/app/cmd/server_test.yaml b/app/cmd/server_test.yaml index 432fb9f..27e09f0 100644 --- a/app/cmd/server_test.yaml +++ b/app/cmd/server_test.yaml @@ -102,3 +102,6 @@ masquerade: proxy: url: https://some.site.net rewriteHost: true + listenHTTP: :80 + listenHTTPS: :443 + forceHTTPS: true diff --git a/extras/masq/server.go b/extras/masq/server.go new file mode 100644 index 0000000..cb7439b --- /dev/null +++ b/extras/masq/server.go @@ -0,0 +1,62 @@ +package masq + +import ( + "crypto/tls" + "fmt" + "net/http" +) + +// MasqTCPServer covers the TCP parts of a standard web server (TCP based HTTP/HTTPS). +// We provide this as an option for masquerading, as some may consider a server +// "suspicious" if it only serves the QUIC protocol and not standard HTTP/HTTPS. +type MasqTCPServer struct { + QUICPort int + HTTPSPort int + Handler http.Handler + TLSConfig *tls.Config + ForceHTTPS bool // Always 301 redirect from HTTP to HTTPS +} + +func (s *MasqTCPServer) ListenAndServeHTTP(addr string) error { + return http.ListenAndServe(addr, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if s.ForceHTTPS { + if s.HTTPSPort == 0 || s.HTTPSPort == 443 { + // Omit port if it's the default + http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusMovedPermanently) + } else { + http.Redirect(w, r, fmt.Sprintf("https://%s:%d%s", r.Host, s.HTTPSPort, r.RequestURI), http.StatusMovedPermanently) + } + return + } + s.Handler.ServeHTTP(&altSvcHijackResponseWriter{ + Port: s.QUICPort, + ResponseWriter: w, + }, r) + })) +} + +func (s *MasqTCPServer) ListenAndServeHTTPS(addr string) error { + server := &http.Server{ + Addr: addr, + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + s.Handler.ServeHTTP(&altSvcHijackResponseWriter{ + Port: s.QUICPort, + ResponseWriter: w, + }, r) + }), + TLSConfig: s.TLSConfig, + } + return server.ListenAndServeTLS("", "") +} + +// altSvcHijackResponseWriter makes sure that the Alt-Svc's port +// is always set with our own value, no matter what the handler sets. +type altSvcHijackResponseWriter struct { + Port int + http.ResponseWriter +} + +func (w *altSvcHijackResponseWriter) WriteHeader(statusCode int) { + w.Header().Set("Alt-Svc", fmt.Sprintf(`h3=":%d"; ma=2592000`, w.Port)) + w.ResponseWriter.WriteHeader(statusCode) +}