Compare commits

..
14 Commits
Author SHA1 Message Date
fatedierandGitHub d8cf1800dc Merge pull request #5568 from fatedier/dev
Release v0.71.1
2026-10-14 16:40:54 +08:00
4a23aa181c Release v0.71.0 (#5495)
* web/frpc: support virtual net visitor plugin (#5414)

* web/frpc: fix visitor plugin validation for sudp (#5448)

* web: patch vulnerable transitive dependencies (#5449)

* web: fix Vite config tsconfig includes (#5450)

* test(web): add frontend unit test baseline (#5451)

* build(web): upgrade ESLint to v10 (#5452)

* build(web): upgrade auto import plugins (#5453)

* chore(web): upgrade frontend dependencies (#5454)

* feat: add negotiated binary UDP packets (#5456)

* docs: add release note for #5456 (#5458)

* server: validate control pool counts (#5459)

* feat: use binary codec for SUDP packets (#5461)

* update version to 0.71.0 (#5464)

* fix(frpc): respect feature gates in verify (#5465)

* remove retired Go Report Card badge (#5467)

* udp: handle closed forwarding channel (#5470)

* limit: clamp bandwidth limiter burst (#5471)

* client: reject invalid work connection addresses (#5472)

* ssh: serialize tunnel channel writes (#5473)

* config: reject case-insensitive subdomain domains (#5474)

* docs: add release note for domain validation fix (#5475)

* log: improve prefix handling (#5489)

* web: patch vulnerable transitive dependencies (#5490)

---------

Co-authored-by: barkure <43804451+barkure@users.noreply.github.com>
Co-authored-by: Yarden Shoham <git@yardenshoham.com>
2026-08-14 14:13:20 +08:00
fatedierandGitHub fa3bcca2b0 Merge pull request #5444 from fatedier/dev
Release v0.70.1
2026-07-23 15:34:31 +08:00
fatedierandGitHub 7b6e01f04f Merge pull request #5411 from fatedier/dev
Release v0.70.0
2026-07-11 18:50:35 +08:00
fatedierandGitHub 8dd26c6961 Merge pull request #5350 from fatedier/dev
Release v0.69.1
2026-06-01 18:02:22 +08:00
fatedierandGitHub c8c1e5116c Merge pull request #5323 from fatedier/dev
Release v0.69.0
2026-05-22 00:55:23 +08:00
fatedierandGitHub 4ec8de973f Merge pull request #5287 from fatedier/dev
bump version to v0.68.1
2026-04-14 01:28:33 +08:00
fatedierandGitHub 5bfcea3d0c merge dev to master (#5254)
* ci: bump github actions to latest major versions (#5251)

* docker: copy shared web directory for npm workspace builds
2026-03-20 15:54:26 +08:00
fatedierandGitHub 0a1b4ab21f Merge pull request #5249 from fatedier/dev
bump version
2026-03-20 13:56:28 +08:00
fatedierandGitHub 5f575b8442 Merge pull request #5147 from fatedier/dev
bump version
2026-01-31 14:01:40 +08:00
fatedierandGitHub a1348cdf00 bump version (#5112) 2026-01-04 14:54:13 +08:00
fatedierandGitHub 2f5e1f7945 Merge pull request #4999 from fatedier/dev
bump version
2025-09-25 20:23:42 +08:00
fatedierandGitHub 22ae8166d3 Merge pull request #4925 from fatedier/dev
bump version
2025-08-10 23:26:32 +08:00
fatedierandGitHub af6bc6369d Merge pull request #4849 from fatedier/dev
bump version
2025-06-25 11:51:19 +08:00
15 changed files with 75 additions and 515 deletions
+21 -13
View File
@@ -12,15 +12,17 @@ frp is an open source project with its ongoing development made possible entirel
<h3 align="center">Gold Sponsors</h3> <h3 align="center">Gold Sponsors</h3>
<!--gold sponsors start--> <!--gold sponsors start-->
<div align="center"> <p align="center">
<a href="https://www.rapidproxy.io/?ref=frp" target="_blank">
## Recall.ai - API for meeting recordings <img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_rapidproxy.png">
<br>
If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp), <b>High-performance residential and ISP proxies for developers</b>
</a>
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more. <br>
<sub>90M+ residential IPs worldwide. Rotating IPs, sticky sessions, and traffic that never expires.</sub>
</div> <br>
<sub>From $0.55/GB. Use RAPID10 for 10% off. Try it for free.</sub>
</p>
<p align="center"> <p align="center">
<a href="https://jb.gg/frp" target="_blank"> <a href="https://jb.gg/frp" target="_blank">
@@ -40,6 +42,16 @@ an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and
</a> </a>
</p> </p>
<div align="center">
## Recall.ai - API for meeting recordings
If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp),
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
</div>
<!--gold sponsors end--> <!--gold sponsors end-->
## What is frp? ## What is frp?
@@ -712,10 +724,6 @@ transport.tls.keyFile = "certificate.key"
transport.tls.trustedCaFile = "ca.crt" transport.tls.trustedCaFile = "ca.crt"
``` ```
By default, when neither `transport.tls.trustedCaFile` nor `transport.tls.useSystemCa` is set, `frpc` does not verify the server certificate. Set `transport.tls.useSystemCa = true` to verify it using the host's system CAs. If `trustedCaFile` is also set, its CA certificates are added to the system CA pool; setting only `trustedCaFile` trusts only the CAs in that file. CA loading or certificate verification failures cause the connection to fail without falling back to another trust source or skipping verification.
`useSystemCa` is supported only in `frpc`'s `transport.tls`, and requires `transport.tls.enable = true` unless `transport.protocol = "wss"`. With `wss`, the outer TLS connection is always enabled and verification applies to the TLS endpoint, which may be a reverse proxy. Setting `useSystemCa = true` in `frps`'s `transport.tls` or either process's `webServer.tls` is a configuration error.
**`frps` TLS settings:** **`frps` TLS settings:**
```toml ```toml
+21 -9
View File
@@ -14,15 +14,17 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者
<h3 align="center">Gold Sponsors</h3> <h3 align="center">Gold Sponsors</h3>
<!--gold sponsors start--> <!--gold sponsors start-->
<div align="center"> <p align="center">
<a href="https://www.rapidproxy.io/?ref=frp" target="_blank">
## Recall.ai - API for meeting recordings <img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_rapidproxy.png">
<br>
If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp), <b>High-performance residential and ISP proxies for developers</b>
</a>
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more. <br>
<sub>90M+ residential IPs worldwide. Rotating IPs, sticky sessions, and traffic that never expires.</sub>
</div> <br>
<sub>From $0.55/GB. Use RAPID10 for 10% off. Try it for free.</sub>
</p>
<p align="center"> <p align="center">
<a href="https://jb.gg/frp" target="_blank"> <a href="https://jb.gg/frp" target="_blank">
@@ -42,6 +44,16 @@ an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and
</a> </a>
</p> </p>
<div align="center">
## Recall.ai - API for meeting recordings
If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp),
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
</div>
<!--gold sponsors end--> <!--gold sponsors end-->
## 为什么使用 frp ? ## 为什么使用 frp ?
+12 -2
View File
@@ -1,3 +1,13 @@
## Features ## Improvements
* frpc can verify server certificates using system CAs, optionally combined with CAs from `trustedCaFile`. * Building frp from source now requires Go 1.26.0 or later.
## Fixes
* Updated dependencies to fix SSH tunnel gateway channel deadlocks and panics caused by malformed challenges from NTLM-authenticated HTTP proxies.
* Fixed VirtualNet routing issues during reconnection and shutdown, and excessive retries after repeated connection failures.
* Health checks now reset the failure count after success, so `healthCheck.maxFailed` applies to consecutive failures.
* Fixed control connections getting stuck after message write failures, which could block reconnection.
* Fixed incorrect daily traffic statistics around daylight-saving transitions and skipped local midnights.
* Fixed early-data loss in HTTP virtual-host CONNECT tunnels. Requests with a positive content length or transfer encoding now return HTTP 400.
* Fixed work connections remaining open when server-side bandwidth limiting is enabled for TCP, HTTP, and UDP proxies.
+4 -6
View File
@@ -109,12 +109,11 @@ func (c *defaultConnectorImpl) Open() error {
sn = c.cfg.ServerAddr sn = c.cfg.ServerAddr
} }
if lo.FromPtr(c.cfg.Transport.TLS.Enable) { if lo.FromPtr(c.cfg.Transport.TLS.Enable) {
tlsConfig, err = transport.NewClientTLSConfigWithSystemCA( tlsConfig, err = transport.NewClientTLSConfig(
c.cfg.Transport.TLS.CertFile, c.cfg.Transport.TLS.CertFile,
c.cfg.Transport.TLS.KeyFile, c.cfg.Transport.TLS.KeyFile,
c.cfg.Transport.TLS.TrustedCaFile, c.cfg.Transport.TLS.TrustedCaFile,
sn, sn)
c.cfg.Transport.TLS.UseSystemCa)
} else { } else {
tlsConfig, err = transport.NewClientTLSConfig("", "", "", sn) tlsConfig, err = transport.NewClientTLSConfig("", "", "", sn)
} }
@@ -195,12 +194,11 @@ func (c *defaultConnectorImpl) realConnect() (net.Conn, error) {
sn = c.cfg.ServerAddr sn = c.cfg.ServerAddr
} }
tlsConfig, err = transport.NewClientTLSConfigWithSystemCA( tlsConfig, err = transport.NewClientTLSConfig(
c.cfg.Transport.TLS.CertFile, c.cfg.Transport.TLS.CertFile,
c.cfg.Transport.TLS.KeyFile, c.cfg.Transport.TLS.KeyFile,
c.cfg.Transport.TLS.TrustedCaFile, c.cfg.Transport.TLS.TrustedCaFile,
sn, sn)
c.cfg.Transport.TLS.UseSystemCa)
if err != nil { if err != nil {
xl.Warnf("fail to build tls configuration, err: %v", err) xl.Warnf("fail to build tls configuration, err: %v", err)
return nil, err return nil, err
-6
View File
@@ -131,12 +131,6 @@ transport.tls.enable = true
# transport.tls.trustedCaFile = "ca.crt" # transport.tls.trustedCaFile = "ca.crt"
# transport.tls.serverName = "example.com" # transport.tls.serverName = "example.com"
# If useSystemCa is true, frpc verifies the server certificate using system CAs.
# If trustedCaFile is also set, its CA certificates are added to the system CA pool.
# Only supported in frpc transport.tls; requires tls.enable = true unless protocol is wss.
# CA loading or certificate verification failures cause the connection to fail.
# transport.tls.useSystemCa = false
# If the disableCustomTLSFirstByte is set to false, frpc will establish a connection with frps using the # If the disableCustomTLSFirstByte is set to false, frpc will establish a connection with frps using the
# first custom byte when tls is enabled. # first custom byte when tls is enabled.
# Since v0.50.0, the default value has been changed to true, and the first custom byte is disabled by default. # Since v0.50.0, the default value has been changed to true, and the first custom byte is disabled by default.
+6 -8
View File
@@ -1,14 +1,12 @@
FROM node:22 AS web-builder FROM node:22 AS web-builder
COPY web/package.json /web/package.json
COPY web/shared/ /web/shared/
COPY web/frpc/ /web/frpc/
WORKDIR /web WORKDIR /web
COPY web/package.json web/package-lock.json ./ RUN npm install
COPY web/shared/package.json ./shared/package.json WORKDIR /web/frpc
COPY web/frpc/package.json ./frpc/package.json RUN npm run build
COPY web/frps/package.json ./frps/package.json
RUN npm ci
COPY web/shared/ ./shared/
COPY web/frpc/ ./frpc/
RUN npm run build --workspace frpc
FROM golang:1.26 AS building FROM golang:1.26 AS building
+6 -8
View File
@@ -1,14 +1,12 @@
FROM node:22 AS web-builder FROM node:22 AS web-builder
COPY web/package.json /web/package.json
COPY web/shared/ /web/shared/
COPY web/frps/ /web/frps/
WORKDIR /web WORKDIR /web
COPY web/package.json web/package-lock.json ./ RUN npm install
COPY web/shared/package.json ./shared/package.json WORKDIR /web/frps
COPY web/frpc/package.json ./frpc/package.json RUN npm run build
COPY web/frps/package.json ./frps/package.json
RUN npm ci
COPY web/shared/ ./shared/
COPY web/frps/ ./frps/
RUN npm run build --workspace frps
FROM golang:1.26 AS building FROM golang:1.26 AS building
-4
View File
@@ -79,10 +79,6 @@ type TLSConfig struct {
KeyFile string `json:"keyFile,omitempty"` KeyFile string `json:"keyFile,omitempty"`
// TrustedCaFile specifies the path of the trusted ca file that will load. // TrustedCaFile specifies the path of the trusted ca file that will load.
TrustedCaFile string `json:"trustedCaFile,omitempty"` TrustedCaFile string `json:"trustedCaFile,omitempty"`
// UseSystemCa enables server certificate verification using system CAs.
// If TrustedCaFile is also set, its certificates are added to the system CA pool.
// Only supported in frpc's transport.tls. With neither trust source, frpc skips verification.
UseSystemCa bool `json:"useSystemCa,omitempty"`
// ServerName specifies the custom server name of tls certificate. By // ServerName specifies the custom server name of tls certificate. By
// default, server name if same to ServerAddr. // default, server name if same to ServerAddr.
ServerName string `json:"serverName,omitempty"` ServerName string `json:"serverName,omitempty"`
-4
View File
@@ -163,10 +163,6 @@ func validateTransportConfig(c *v1.ClientTransportConfig) (Warning, error) {
warnings = AppendError(warnings, checkTLSConfig("transport.tls.certFile", c.TLS.CertFile)) warnings = AppendError(warnings, checkTLSConfig("transport.tls.certFile", c.TLS.CertFile))
warnings = AppendError(warnings, checkTLSConfig("transport.tls.keyFile", c.TLS.KeyFile)) warnings = AppendError(warnings, checkTLSConfig("transport.tls.keyFile", c.TLS.KeyFile))
warnings = AppendError(warnings, checkTLSConfig("transport.tls.trustedCaFile", c.TLS.TrustedCaFile)) warnings = AppendError(warnings, checkTLSConfig("transport.tls.trustedCaFile", c.TLS.TrustedCaFile))
// wss keeps TLS on regardless of transport.tls.enable.
if c.TLS.UseSystemCa && c.Protocol != "wss" {
errs = AppendError(errs, fmt.Errorf("transport.tls.useSystemCa requires transport.tls.enable to be true unless transport.protocol is wss"))
}
} }
if !slices.Contains(SupportedTransportProtocols, c.Protocol) { if !slices.Contains(SupportedTransportProtocols, c.Protocol) {
-3
View File
@@ -23,9 +23,6 @@ import (
func validateWebServerConfig(c *v1.WebServerConfig) error { func validateWebServerConfig(c *v1.WebServerConfig) error {
if c.TLS != nil { if c.TLS != nil {
if c.TLS.UseSystemCa {
return fmt.Errorf("webServer.tls.useSystemCa is only supported in frpc transport.tls")
}
if c.TLS.CertFile == "" { if c.TLS.CertFile == "" {
return fmt.Errorf("tls.certFile must be specified when tls is enabled") return fmt.Errorf("tls.certFile must be specified when tls is enabled")
} }
-3
View File
@@ -44,9 +44,6 @@ func (v *ConfigValidator) ValidateServerConfig(c *v1.ServerConfig) (Warning, err
if err := validateWebServerConfig(&c.WebServer); err != nil { if err := validateWebServerConfig(&c.WebServer); err != nil {
errs = AppendError(errs, err) errs = AppendError(errs, err)
} }
if c.Transport.TLS.UseSystemCa {
errs = AppendError(errs, fmt.Errorf("transport.tls.useSystemCa is only supported in frpc transport.tls"))
}
errs = AppendError(errs, ValidatePort(c.BindPort, "bindPort")) errs = AppendError(errs, ValidatePort(c.BindPort, "bindPort"))
errs = AppendError(errs, ValidatePort(c.KCPBindPort, "kcpBindPort")) errs = AppendError(errs, ValidatePort(c.KCPBindPort, "kcpBindPort"))
-146
View File
@@ -1,146 +0,0 @@
// Copyright 2026 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package validation
import (
"encoding/json"
"fmt"
"testing"
"github.com/stretchr/testify/require"
v1 "github.com/fatedier/frp/pkg/config/v1"
)
func TestClientTLSCATrustSources(t *testing.T) {
for _, test := range []struct {
name string
tlsJSON string
useSystemCa bool
caFile string
}{
{name: "default", tlsJSON: `{}`},
{name: "file only", tlsJSON: `{"trustedCaFile":"ca.pem"}`, caFile: "ca.pem"},
{name: "system only", tlsJSON: `{"useSystemCa":true}`, useSystemCa: true},
{name: "combined", tlsJSON: `{"useSystemCa":true,"trustedCaFile":"ca.pem"}`, useSystemCa: true, caFile: "ca.pem"},
} {
t.Run(test.name, func(t *testing.T) {
cfg := &v1.ClientCommonConfig{}
require.NoError(t, json.Unmarshal([]byte(`{"transport":{"tls":`+test.tlsJSON+`}}`), cfg))
require.NoError(t, cfg.Complete())
require.Equal(t, test.useSystemCa, cfg.Transport.TLS.UseSystemCa)
require.Equal(t, test.caFile, cfg.Transport.TLS.TrustedCaFile)
warnings, err := NewConfigValidator(nil).ValidateClientCommonConfig(cfg)
require.NoError(t, warnings)
require.NoError(t, err)
})
}
}
func TestClientSystemCARequiresTLS(t *testing.T) {
for _, protocol := range []string{"tcp", "kcp", "quic", "websocket", "wss"} {
for _, state := range []struct {
name string
enable *bool
}{
{name: "default"},
{name: "enabled", enable: new(true)},
{name: "disabled", enable: new(false)},
} {
t.Run(protocol+"/"+state.name, func(t *testing.T) {
cfg := &v1.ClientCommonConfig{}
cfg.Transport.Protocol = protocol
cfg.Transport.TLS.Enable = state.enable
cfg.Transport.TLS.UseSystemCa = true
require.NoError(t, cfg.Complete())
warnings, err := NewConfigValidator(nil).ValidateClientCommonConfig(cfg)
require.NoError(t, warnings, "useSystemCa must produce an error, not a warning")
if state.name == "disabled" && protocol != "wss" {
require.ErrorContains(t, err, "transport.tls.useSystemCa")
require.ErrorContains(t, err, "transport.tls.enable")
} else {
require.NoError(t, err)
}
})
}
}
}
func TestDisabledTLSFileWarnings(t *testing.T) {
for _, protocol := range []string{"tcp", "kcp", "quic", "websocket", "wss"} {
t.Run(protocol, func(t *testing.T) {
cfg := &v1.ClientCommonConfig{}
cfg.Transport.Protocol = protocol
cfg.Transport.TLS.Enable = new(false)
cfg.Transport.TLS.CertFile = "client.pem"
cfg.Transport.TLS.KeyFile = "client.key"
cfg.Transport.TLS.TrustedCaFile = "ca.pem"
require.NoError(t, cfg.Complete())
warnings, err := NewConfigValidator(nil).ValidateClientCommonConfig(cfg)
require.NoError(t, err)
for _, field := range []string{"certFile", "keyFile", "trustedCaFile"} {
require.ErrorContains(t, warnings, "transport.tls."+field+" is invalid when transport.tls.enable is false")
}
})
}
}
func TestSystemCAUnsupportedLocations(t *testing.T) {
for _, test := range []struct {
name string
server bool
configJSON string
fieldPath string
}{
{
name: "frps transport", server: true,
configJSON: `{"transport":{"tls":{"useSystemCa":%t}}}`, fieldPath: "transport.tls.useSystemCa",
},
{
name: "frpc webServer", configJSON: `{"webServer":{"tls":{"certFile":"server.pem","keyFile":"server.key","useSystemCa":%t}}}`,
fieldPath: "webServer.tls.useSystemCa",
},
{
name: "frps webServer", server: true,
configJSON: `{"webServer":{"tls":{"certFile":"server.pem","keyFile":"server.key","useSystemCa":%t}}}`,
fieldPath: "webServer.tls.useSystemCa",
},
} {
for _, enabled := range []bool{false, true} {
t.Run(fmt.Sprintf("%s/%t", test.name, enabled), func(t *testing.T) {
data := fmt.Appendf(nil, test.configJSON, enabled)
var warnings Warning
var err error
if test.server {
cfg := &v1.ServerConfig{}
require.NoError(t, json.Unmarshal(data, cfg))
require.NoError(t, cfg.Complete())
warnings, err = NewConfigValidator(nil).ValidateServerConfig(cfg)
} else {
cfg := &v1.ClientCommonConfig{}
require.NoError(t, json.Unmarshal(data, cfg))
require.NoError(t, cfg.Complete())
warnings, err = NewConfigValidator(nil).ValidateClientCommonConfig(cfg)
}
require.NoError(t, warnings)
if enabled {
require.EqualError(t, err, test.fieldPath+" is only supported in frpc transport.tls")
} else {
require.NoError(t, err)
}
})
}
}
}
+5 -49
View File
@@ -80,23 +80,17 @@ func newRandomTLSKeyPair() (*tls.Certificate, error) {
// Only support one ca file to add // Only support one ca file to add
func newCertPool(caPath string) (*x509.CertPool, error) { func newCertPool(caPath string) (*x509.CertPool, error) {
pool := x509.NewCertPool() pool := x509.NewCertPool()
if err := appendCertsFromFile(pool, caPath); err != nil {
return nil, err
}
return pool, nil
}
func appendCertsFromFile(pool *x509.CertPool, caPath string) error {
caCrt, err := os.ReadFile(caPath) caCrt, err := os.ReadFile(caPath)
if err != nil { if err != nil {
return err return nil, err
} }
if !pool.AppendCertsFromPEM(caCrt) { if !pool.AppendCertsFromPEM(caCrt) {
return fmt.Errorf("failed to parse CA certificate from file %q: no valid PEM certificates found", caPath) return nil, fmt.Errorf("failed to parse CA certificate from file %q: no valid PEM certificates found", caPath)
} }
return nil return pool, nil
} }
func NewServerTLSConfig(certPath, keyPath, caPath string) (*tls.Config, error) { func NewServerTLSConfig(certPath, keyPath, caPath string) (*tls.Config, error) {
@@ -132,21 +126,6 @@ func NewServerTLSConfig(certPath, keyPath, caPath string) (*tls.Config, error) {
} }
func NewClientTLSConfig(certPath, keyPath, caPath, serverName string) (*tls.Config, error) { func NewClientTLSConfig(certPath, keyPath, caPath, serverName string) (*tls.Config, error) {
return NewClientTLSConfigWithSystemCA(certPath, keyPath, caPath, serverName, false)
}
// NewClientTLSConfigWithSystemCA enables verification against system CAs when
// useSystemCa is true, adding certificates from caPath if it is also set.
// Without either trust source, server certificate verification remains disabled.
func NewClientTLSConfigWithSystemCA(certPath, keyPath, caPath, serverName string, useSystemCa bool) (*tls.Config, error) {
return newClientTLSConfig(certPath, keyPath, caPath, serverName, useSystemCa, x509.SystemCertPool)
}
func newClientTLSConfig(
certPath, keyPath, caPath, serverName string,
useSystemCa bool,
systemCertPool func() (*x509.CertPool, error),
) (*tls.Config, error) {
base := &tls.Config{} base := &tls.Config{}
if certPath != "" && keyPath != "" { if certPath != "" && keyPath != "" {
@@ -160,26 +139,7 @@ func newClientTLSConfig(
base.ServerName = serverName base.ServerName = serverName
switch { if caPath != "" {
case caPath != "" && useSystemCa:
// SystemCertPool returns a copy, so appending does not change the system pool.
// Design basis: Go 1.26.7 crypto/x509/verify.go:559-573 first uses the
// Windows/macOS verifier, then falls back to Go verification using only the
// added CAs if platform verification fails. This platform behavior has not been
// tested here; preserve the system pool instead of copying its subjects.
pool, err := systemCertPool()
if err != nil {
return nil, fmt.Errorf("failed to load system CA pool: %w", err)
}
if pool == nil {
return nil, fmt.Errorf("failed to load system CA pool: no pool returned")
}
if err := appendCertsFromFile(pool, caPath); err != nil {
return nil, err
}
base.RootCAs = pool
base.InsecureSkipVerify = false
case caPath != "":
pool, err := newCertPool(caPath) pool, err := newCertPool(caPath)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -187,11 +147,7 @@ func newClientTLSConfig(
base.RootCAs = pool base.RootCAs = pool
base.InsecureSkipVerify = false base.InsecureSkipVerify = false
case useSystemCa: } else {
// A nil RootCAs makes crypto/tls use the host root store, through the
// platform verifier on macOS and Windows.
base.InsecureSkipVerify = false
default:
base.InsecureSkipVerify = true base.InsecureSkipVerify = true
} }
-250
View File
@@ -1,250 +0,0 @@
// Copyright 2026 The frp Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package transport
import (
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"math/big"
"net"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func newSelfSignedCert(t *testing.T, name string) (tls.Certificate, []byte) {
t.Helper()
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
template := x509.Certificate{
SerialNumber: big.NewInt(1),
IsCA: true,
BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
Subject: pkix.Name{CommonName: name},
DNSNames: []string{name},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
}
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
require.NoError(t, err)
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
cert, err := tls.X509KeyPair(certPEM, keyPEM)
require.NoError(t, err)
return cert, certPEM
}
func TestNewClientTLSConfigVerification(t *testing.T) {
_, systemPEM := newSelfSignedCert(t, "system.example.com")
_, filePEM := newSelfSignedCert(t, "file.example.com")
caFile := filepath.Join(t.TempDir(), "ca.crt")
require.NoError(t, os.WriteFile(caFile, filePEM, 0o600))
systemPool := x509.NewCertPool()
require.True(t, systemPool.AppendCertsFromPEM(systemPEM))
filePool := x509.NewCertPool()
require.True(t, filePool.AppendCertsFromPEM(filePEM))
combinedPool := systemPool.Clone()
require.True(t, combinedPool.AppendCertsFromPEM(filePEM))
for _, test := range []struct {
name string
caPath string
useSystemCa bool
insecureSkipVerify bool
wantPool *x509.CertPool
}{
{name: "no trust source", insecureSkipVerify: true},
{name: "file CA only", caPath: caFile, wantPool: filePool},
{name: "system CA only", useSystemCa: true},
{name: "system and file CAs", caPath: caFile, useSystemCa: true, wantPool: combinedPool},
} {
t.Run(test.name, func(t *testing.T) {
var loadedPool *x509.CertPool
loads := 0
loadSystemPool := func() (*x509.CertPool, error) {
loads++
loadedPool = systemPool.Clone()
return loadedPool, nil
}
cfg, err := newClientTLSConfig("", "", test.caPath, "example.com", test.useSystemCa, loadSystemPool)
require.NoError(t, err)
require.Equal(t, "example.com", cfg.ServerName)
require.Equal(t, test.insecureSkipVerify, cfg.InsecureSkipVerify)
if test.wantPool == nil {
require.Nil(t, cfg.RootCAs)
} else {
require.NotNil(t, cfg.RootCAs)
require.True(t, cfg.RootCAs.Equal(test.wantPool))
}
if test.caPath != "" && test.useSystemCa {
require.Equal(t, 1, loads)
require.Same(t, loadedPool, cfg.RootCAs, "append to the returned system pool")
} else {
require.Zero(t, loads, "only combined trust needs an explicit system pool")
}
})
}
// The four-argument API keeps the original skip/file-only semantics for
// xtcp visitors and plugins, regardless of the new transport option.
cfg, err := NewClientTLSConfig("", "", "", "example.com")
require.NoError(t, err)
require.True(t, cfg.InsecureSkipVerify)
require.Nil(t, cfg.RootCAs)
cfg, err = NewClientTLSConfig("", "", caFile, "example.com")
require.NoError(t, err)
require.False(t, cfg.InsecureSkipVerify)
require.True(t, cfg.RootCAs.Equal(filePool))
}
func TestNewClientTLSConfigCAErrors(t *testing.T) {
_, caPEM := newSelfSignedCert(t, "localhost")
dir := t.TempDir()
caFile := filepath.Join(dir, "ca.crt")
badFile := filepath.Join(dir, "bad.crt")
require.NoError(t, os.WriteFile(caFile, caPEM, 0o600))
require.NoError(t, os.WriteFile(badFile, []byte("not a PEM certificate"), 0o600))
systemErr := errors.New("system trust store unavailable")
for _, test := range []struct {
name string
caPath string
useSystemCa bool
pool *x509.CertPool
poolErr error
wantErr string
}{
{name: "file missing", caPath: filepath.Join(dir, "missing.crt"), wantErr: "missing.crt"},
{name: "file invalid", caPath: badFile, wantErr: "no valid PEM certificates found"},
{
name: "combined file missing", caPath: filepath.Join(dir, "missing.crt"), useSystemCa: true,
pool: x509.NewCertPool(), wantErr: "missing.crt",
},
{
name: "combined file invalid", caPath: badFile, useSystemCa: true,
pool: x509.NewCertPool(), wantErr: "no valid PEM certificates found",
},
{
name: "system pool error", caPath: caFile, useSystemCa: true,
pool: x509.NewCertPool(), poolErr: systemErr, wantErr: "failed to load system CA pool",
},
{name: "nil system pool", caPath: caFile, useSystemCa: true, wantErr: "no pool returned"},
} {
t.Run(test.name, func(t *testing.T) {
cfg, err := newClientTLSConfig("", "", test.caPath, "localhost", test.useSystemCa,
func() (*x509.CertPool, error) { return test.pool, test.poolErr })
require.ErrorContains(t, err, test.wantErr)
require.Nil(t, cfg, "do not return a partially configured TLS client or fall back")
if test.poolErr != nil {
require.ErrorIs(t, err, test.poolErr)
}
})
}
}
func TestClientTLSConfigHandshake(t *testing.T) {
systemCert, systemPEM := newSelfSignedCert(t, "localhost")
fileCert, filePEM := newSelfSignedCert(t, "localhost")
untrustedCert, _ := newSelfSignedCert(t, "localhost")
caFile := filepath.Join(t.TempDir(), "ca.crt")
require.NoError(t, os.WriteFile(caFile, filePEM, 0o600))
systemPool := x509.NewCertPool()
require.True(t, systemPool.AppendCertsFromPEM(systemPEM))
for _, server := range []struct {
name string
cert tls.Certificate
}{
{name: "system CA", cert: systemCert},
{name: "file CA", cert: fileCert},
{name: "untrusted CA", cert: untrustedCert},
} {
t.Run(server.name, func(t *testing.T) {
ln, err := tls.Listen("tcp", "127.0.0.1:0", &tls.Config{Certificates: []tls.Certificate{server.cert}})
require.NoError(t, err)
t.Cleanup(func() { _ = ln.Close() })
go func() {
for {
conn, err := ln.Accept()
if err != nil {
return
}
_ = conn.SetDeadline(time.Now().Add(5 * time.Second))
_ = conn.(*tls.Conn).Handshake()
_ = conn.Close()
}
}()
for _, test := range []struct {
name string
caPath string
useSystemCa bool
serverName string
wantErr bool
}{
{name: "no trust source"},
{name: "file CA only", caPath: caFile, wantErr: server.name != "file CA"},
// All certificates were just generated and are absent from the real
// host store, even on hosts without any installed system roots.
{name: "system CA only rejects unknown certificate", useSystemCa: true, wantErr: true},
{name: "combined CAs", caPath: caFile, useSystemCa: true, wantErr: server.name == "untrusted CA"},
{
name: "combined CAs reject wrong name", caPath: caFile, useSystemCa: true,
serverName: "other.example.com", wantErr: true,
},
} {
t.Run(test.name, func(t *testing.T) {
serverName := test.serverName
if serverName == "" {
serverName = "localhost"
}
var cfg *tls.Config
var err error
if test.caPath != "" && test.useSystemCa {
cfg, err = newClientTLSConfig("", "", test.caPath, serverName, true,
func() (*x509.CertPool, error) { return systemPool.Clone(), nil })
} else {
cfg, err = NewClientTLSConfigWithSystemCA("", "", test.caPath, serverName, test.useSystemCa)
}
require.NoError(t, err)
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: 5 * time.Second}, "tcp", ln.Addr().String(), cfg)
if conn != nil {
_ = conn.Close()
}
if test.wantErr {
var verificationErr *tls.CertificateVerificationError
require.ErrorAs(t, err, &verificationErr)
} else {
require.NoError(t, err)
}
})
}
})
}
}
-4
View File
@@ -177,10 +177,6 @@ var _ = ginkgo.Describe("[Feature: Client-Server]", func() {
client: "transport.tls.enable = false", client: "transport.tls.enable = false",
expectError: true, expectError: true,
}) })
defineClientServerTest("useSystemCa rejects the default self-signed server certificate", f, &generalTestConfigures{
client: "transport.tls.useSystemCa = true",
expectError: true,
})
}) })
ginkgo.Describe("TLS with custom certificate", func() { ginkgo.Describe("TLS with custom certificate", func() {