Support IPv4 HTTP public gateway entries
This commit is contained in:
1 parent
6f01d31f97
commit
6d5973bea3
11 files changed
+146
-32
No files matched your search
@@ -169,7 +169,7 @@ func (s *Server) setTLSDomain(c *gin.Context) {
|
||||
status := tlsservice.Status()
|
||||
newBase := tlsservice.PublicBaseURL(scheme, strings.ToLower(strings.TrimSpace(req.Domain)), port)
|
||||
if newBase == "" || port < 0 || port > 65535 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "公网域名、协议或端口无效"})
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "公网地址、协议或端口无效;IPv4 地址仅支持 HTTP"})
|
||||
return
|
||||
}
|
||||
// The Go listener cannot be rebound in place; Nginx owns its own sockets.
|
||||
|
||||
@@ -68,8 +68,8 @@ func RenderWithOptions(opts RenderOptions, routes gin.RoutesInfo, authPort int)
|
||||
if scheme == "" {
|
||||
scheme = "https"
|
||||
}
|
||||
if !tlsservice.ValidDomain(domain) {
|
||||
return "", "", fmt.Errorf("请输入有效的单个完整域名")
|
||||
if !tlsservice.ValidPublicHost(scheme, domain) {
|
||||
return "", "", fmt.Errorf("请输入有效域名;HTTP 也支持 IPv4 地址")
|
||||
}
|
||||
if !tlsservice.ValidScheme(scheme) {
|
||||
return "", "", fmt.Errorf("协议仅支持 http 或 https")
|
||||
|
||||
@@ -111,6 +111,18 @@ func TestRenderHTTPCustomPort(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderHTTPIPv4AndRejectHTTPSIPv4(t *testing.T) {
|
||||
opts := RenderOptions{Domain: "121.4.111.208", Scheme: "http", Port: 28080}
|
||||
site, httpsSite, err := RenderWithOptions(opts, nil, 0)
|
||||
if err != nil || httpsSite != "" || !strings.Contains(site, "listen 28080;") || !strings.Contains(site, "server_name 121.4.111.208;") {
|
||||
t.Fatalf("HTTP IPv4 rendering failed: %v, %q, %q", err, site, httpsSite)
|
||||
}
|
||||
opts.Scheme = "https"
|
||||
if _, _, err := RenderWithOptions(opts, nil, 0); err == nil {
|
||||
t.Fatal("HTTPS IPv4 must be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderHTTPSCustomPort(t *testing.T) {
|
||||
routes := gin.RoutesInfo{{Method: "GET", Path: "/api/health"}}
|
||||
httpSite, httpsSite, err := RenderWithOptions(RenderOptions{Domain: "vpn.example.com", Scheme: "https", Port: 8443}, routes, 0)
|
||||
|
||||
@@ -58,7 +58,7 @@ func parseManagedEntry(body string) (nginxconfig.RenderOptions, error) {
|
||||
}
|
||||
domains := managedDomain.FindAllStringSubmatch(body, -1)
|
||||
ports := portPattern.FindStringSubmatch(body)
|
||||
if len(domains) == 0 || len(ports) != 2 || !tlsservice.ValidDomain(domains[0][1]) {
|
||||
if len(domains) == 0 || len(ports) != 2 || !tlsservice.ValidPublicHost(scheme, domains[0][1]) {
|
||||
return nginxconfig.RenderOptions{}, errors.New("无法识别托管站点的公网地址")
|
||||
}
|
||||
for _, match := range domains[1:] {
|
||||
@@ -363,8 +363,8 @@ func RunEntryWithAuth(opts nginxconfig.RenderOptions, routes gin.RoutesInfo, aut
|
||||
scheme = "https"
|
||||
}
|
||||
opts.Domain, opts.Scheme = domain, scheme
|
||||
if !tlsservice.ValidDomain(domain) {
|
||||
return errors.New("请先保存有效域名")
|
||||
if !tlsservice.ValidPublicHost(scheme, domain) {
|
||||
return errors.New("请先保存有效域名;HTTP 也支持 IPv4 地址")
|
||||
}
|
||||
if !tlsservice.ValidScheme(scheme) {
|
||||
return errors.New("协议仅支持 http 或 https")
|
||||
|
||||
@@ -12,6 +12,7 @@ func TestParseManagedEntry(t *testing.T) {
|
||||
{Domain: "vpn.example.com", Scheme: "https", Port: 443},
|
||||
{Domain: "vpn.example.com", Scheme: "https", Port: 8443},
|
||||
{Domain: "vpn.example.com", Scheme: "http", Port: 28080},
|
||||
{Domain: "121.4.111.208", Scheme: "http", Port: 28080},
|
||||
} {
|
||||
httpSite, httpsSite, err := nginxconfig.RenderWithOptions(want, nil, 0)
|
||||
if err != nil {
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
)
|
||||
|
||||
var domainPattern = regexp.MustCompile(`^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$`)
|
||||
var numericHostPattern = regexp.MustCompile(`^[0-9.]+$`)
|
||||
|
||||
type configuration struct {
|
||||
domain string
|
||||
@@ -72,7 +73,22 @@ func DataDir() string {
|
||||
}
|
||||
|
||||
func ValidDomain(domain string) bool {
|
||||
return len(domain) <= 253 && domainPattern.MatchString(domain) && net.ParseIP(domain) == nil
|
||||
return len(domain) <= 253 && domainPattern.MatchString(domain) && !numericHostPattern.MatchString(domain)
|
||||
}
|
||||
|
||||
// ValidPublicHost allows a literal IPv4 address only for an HTTP gateway.
|
||||
// Certificate issuance and HTTPS remain restricted to DNS names.
|
||||
func ValidPublicHost(scheme, host string) bool {
|
||||
if !ValidScheme(scheme) {
|
||||
return false
|
||||
}
|
||||
if scheme == "https" {
|
||||
return ValidDomain(host)
|
||||
}
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
return ip.To4() != nil && !ip.IsUnspecified() && !ip.IsMulticast()
|
||||
}
|
||||
return ValidDomain(host)
|
||||
}
|
||||
|
||||
func ValidScheme(scheme string) bool {
|
||||
@@ -94,7 +110,7 @@ func EffectivePort(scheme string, port int) int {
|
||||
func PublicBaseURL(scheme, host string, port int) string {
|
||||
scheme = strings.ToLower(strings.TrimSpace(scheme))
|
||||
host = strings.ToLower(strings.TrimSpace(host))
|
||||
if !ValidScheme(scheme) || host == "" || !ValidDomain(host) {
|
||||
if !ValidPublicHost(scheme, host) {
|
||||
return ""
|
||||
}
|
||||
eff := EffectivePort(scheme, port)
|
||||
@@ -171,9 +187,6 @@ func configFromEnvironment(dataDir string) (configuration, error) {
|
||||
}
|
||||
domain = strings.ToLower(raw)
|
||||
}
|
||||
if domain != "" && !ValidDomain(domain) {
|
||||
return configuration{}, fmt.Errorf("VPN_TLS_DOMAIN 不是有效的单个完整域名")
|
||||
}
|
||||
schemeEnv := strings.TrimSpace(os.Getenv("VPN_TLS_SCHEME"))
|
||||
scheme := normalizeScheme(schemeEnv)
|
||||
if schemeEnv == "" {
|
||||
@@ -186,6 +199,9 @@ func configFromEnvironment(dataDir string) (configuration, error) {
|
||||
if !ValidScheme(scheme) {
|
||||
return configuration{}, fmt.Errorf("VPN_TLS_SCHEME 仅支持 http 或 https")
|
||||
}
|
||||
if domain != "" && !ValidPublicHost(scheme, domain) {
|
||||
return configuration{}, fmt.Errorf("VPN_TLS_DOMAIN 必须是有效域名,HTTP 模式也可使用 IPv4 地址")
|
||||
}
|
||||
port := 0
|
||||
if raw := strings.TrimSpace(os.Getenv("VPN_TLS_PORT")); raw != "" {
|
||||
var err error
|
||||
@@ -239,8 +255,8 @@ func ConfigurePublicEntry(dataDir, scheme, domain string, port int) error {
|
||||
if !ValidScheme(scheme) {
|
||||
return fmt.Errorf("协议仅支持 http 或 https")
|
||||
}
|
||||
if !ValidDomain(domain) {
|
||||
return fmt.Errorf("请输入单个有效的完整域名")
|
||||
if !ValidPublicHost(scheme, domain) {
|
||||
return fmt.Errorf("请输入有效域名;HTTP 也支持 IPv4 地址")
|
||||
}
|
||||
if port < 0 || port > 65535 {
|
||||
return fmt.Errorf("端口必须是 0-65535(0 表示协议默认端口)")
|
||||
@@ -304,7 +320,7 @@ func AdoptNginxConfiguration(dataDir string) error {
|
||||
// UseManagedNginxEntry keeps the in-process identity issuer aligned with the
|
||||
// live Nginx site if an older version left an unapplied draft on disk.
|
||||
func UseManagedNginxEntry(dataDir, scheme, domain string, port int) error {
|
||||
if !ValidScheme(scheme) || !ValidDomain(domain) || port < 1 || port > 65535 {
|
||||
if !ValidPublicHost(scheme, domain) || port < 1 || port > 65535 {
|
||||
return fmt.Errorf("运行中的 Nginx 公网地址无效")
|
||||
}
|
||||
state.Lock()
|
||||
|
||||
@@ -43,6 +43,35 @@ func TestDomainValidationAndRestart(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPIPv4PublicEntry(t *testing.T) {
|
||||
for _, host := range []string{"121.4.111.208", "127.0.0.1", "vpn.example.com"} {
|
||||
if !ValidPublicHost("http", host) {
|
||||
t.Fatalf("valid HTTP host rejected: %s", host)
|
||||
}
|
||||
}
|
||||
for _, host := range []string{"999.4.111.208", "121.4.111.208:28080", "0.0.0.0", "224.0.0.1", "::1", "123.456", "121.4.111.208/evil"} {
|
||||
if ValidPublicHost("http", host) {
|
||||
t.Fatalf("invalid HTTP host accepted: %s", host)
|
||||
}
|
||||
}
|
||||
if ValidPublicHost("https", "121.4.111.208") {
|
||||
t.Fatal("HTTPS IPv4 must require a domain")
|
||||
}
|
||||
if got := PublicBaseURL("http", "121.4.111.208", 28080); got != "http://121.4.111.208:28080" {
|
||||
t.Fatalf("unexpected public URL: %s", got)
|
||||
}
|
||||
t.Setenv("VPN_TLS_DOMAIN", "")
|
||||
t.Setenv("VPN_TLS_SCHEME", "")
|
||||
t.Setenv("VPN_TLS_PORT", "")
|
||||
dir := t.TempDir()
|
||||
if err := ConfigurePublicEntry(dir, "http", "121.4.111.208", 28080); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := ConfigurePublicEntry(dir, "https", "121.4.111.208", 443); err == nil {
|
||||
t.Fatal("HTTPS IPv4 was persisted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailedHTTPSBindReleasesHTTPPort(t *testing.T) {
|
||||
t.Setenv("VPN_TLS_DOMAIN", "test-link.appx365.com")
|
||||
occupied, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
|
||||
Reference in new issue
Block a user