mirror of
https://github.com/fatedier/frp.git
synced 2026-10-18 17:43:28 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8cf1800dc | ||
|
|
4a23aa181c | ||
|
|
fa3bcca2b0 | ||
|
|
7b6e01f04f | ||
|
|
8dd26c6961 | ||
|
|
c8c1e5116c | ||
|
|
4ec8de973f | ||
|
|
5bfcea3d0c | ||
|
|
0a1b4ab21f | ||
|
|
5f575b8442 | ||
|
|
a1348cdf00 | ||
|
|
2f5e1f7945 | ||
|
|
22ae8166d3 | ||
|
|
af6bc6369d |
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user