Compare commits

...

6 Commits

Author SHA1 Message Date
charf1998
6d5e3d9f57
Merge 971cf0cebfa95f758c481a1b5f136e7c382c41f3 into dc34a68542b8566ce22de983dd890428922838fc 2024-02-19 17:11:01 +08:00
fatedier
dc34a68542
support go1.22 and drop support for go1.20 (#4001) 2024-02-19 16:45:25 +08:00
fatedier
3529158f31
proxy supports configuring annotations, which will be displayed in the frps dashboard (#4000) 2024-02-19 16:28:27 +08:00
Gerhard Tan
9152c59570
fix nil map error when using plugin headers in legacy format (#3996)
* fix nil map error when using plugin headers in legacy format

* create map on demand

* better initialization
2024-02-19 13:03:37 +08:00
Apflkuacha
2af2cf7dbd
Updated readme for the HTTP Proxy (#3999)
Improved the http proxy command by using %h and %p to avoid typing the host name twice
2024-02-19 11:43:09 +08:00
charf1998
971cf0cebf
Rename stale.yml to stale.xml 2024-01-29 20:03:04 +01:00
27 changed files with 311 additions and 158 deletions

View File

@ -2,7 +2,7 @@ version: 2
jobs:
go-version-latest:
docker:
- image: cimg/go:1.21-node
- image: cimg/go:1.22-node
resource_class: large
steps:
- checkout
@ -10,7 +10,7 @@ jobs:
- run: make alltest
go-version-last:
docker:
- image: cimg/go:1.20-node
- image: cimg/go:1.21-node
resource_class: large
steps:
- checkout

View File

@ -19,15 +19,15 @@ jobs:
steps:
# environment
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: '0'
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
# get image tag name
- name: Get Image Tag Name
@ -38,13 +38,13 @@ jobs:
echo "TAG_NAME=${{ github.event.inputs.tag }}" >> $GITHUB_ENV
fi
- name: Login to DockerHub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Login to the GPR
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@ -72,7 +72,7 @@ jobs:
${{ env.TAG_FRPC_GPR }}
- name: Build and push frps
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
file: ./dockerfiles/Dockerfile-for-frps

View File

@ -14,12 +14,12 @@ jobs:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v4
- uses: actions/setup-go@v5
with:
go-version: '1.21'
- uses: actions/checkout@v3
go-version: '1.22'
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
uses: golangci/golangci-lint-action@v4
with:
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
version: v1.55

View File

@ -8,21 +8,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: '1.21'
go-version: '1.22'
- name: Make All
run: |
./package.sh
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v4
uses: goreleaser/goreleaser-action@v5
with:
version: latest
args: release --clean --release-notes=./Release.md

View File

@ -11,10 +11,6 @@
<a href="https://workos.com/?utm_campaign=github_repo&utm_medium=referral&utm_content=frp&utm_source=github" target="_blank">
<img width="350px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_workos.png">
</a>
<a>&nbsp</a>
<a href="https://www.nango.dev?utm_source=github&utm_medium=oss-banner&utm_campaign=fatedier-frp" target="_blank">
<img width="400px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_nango.png">
</a>
</p>
<!--gold sponsors end-->
@ -201,11 +197,11 @@ This example implements multiple SSH services exposed through the same port usin
4. To access internal machine A using SSH ProxyCommand, assuming the username is "test":
`ssh -o 'proxycommand socat - PROXY:x.x.x.x:machine-a.example.com:22,proxyport=5002' test@machine-a`
`ssh -o 'proxycommand socat - PROXY:x.x.x.x:%h:%p,proxyport=5002' test@machine-a.example.com`
5. To access internal machine B, the only difference is the domain name, assuming the username is "test":
`ssh -o 'proxycommand socat - PROXY:x.x.x.x:machine-b.example.com:22,proxyport=5002' test@machine-b`
`ssh -o 'proxycommand socat - PROXY:x.x.x.x:%h:%p,proxyport=5002' test@machine-b.example.com`
### Accessing Internal Web Services with Custom Domains in LAN
@ -526,6 +522,8 @@ Check frp's status and proxies' statistics information by Dashboard.
Configure a port for dashboard to enable this feature:
```toml
# The default value is 127.0.0.1. Change it to 0.0.0.0 when you want to access it from a public network.
webServer.addr = "0.0.0.0"
webServer.port = 7500
# dashboard's username and password are both optional
webServer.user = "admin"
@ -534,8 +532,6 @@ webServer.password = "admin"
Then visit `http://[serverAddr]:7500` to see the dashboard, with username and password both being `admin`.
Note that if you want your server to be accessed from public networks, then also add `webServer.addr = "0.0.0.0"` line. For security reasons (credits [#3709](https://github.com/fatedier/frp/issues/3709)), value `127.0.0.1` is used by default.
Additionally, you can use HTTPS port by using your domains wildcard or normal SSL certificate:
```toml

View File

@ -13,10 +13,6 @@ frp 是一个专注于内网穿透的高性能的反向代理应用,支持 TCP
<a href="https://workos.com/?utm_campaign=github_repo&utm_medium=referral&utm_content=frp&utm_source=github" target="_blank">
<img width="350px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_workos.png">
</a>
<a>&nbsp</a>
<a href="https://www.nango.dev?utm_source=github&utm_medium=oss-banner&utm_campaign=fatedier-frp" target="_blank">
<img width="400px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_nango.png">
</a>
</p>
<!--gold sponsors end-->

View File

@ -1,11 +1,3 @@
### Deprecation Notices
* Using an underscore in a flag name is deprecated and has been replaced by a hyphen. The underscore format will remain compatible for some time, until it is completely removed in a future version. For example, `--remote_port` is replaced with `--remote-port`.
### Features
* The `Refresh` and `ClearOfflineProxies` buttons have been added to the Dashboard of frps.
### Fixes
* The host/domain matching in the routing rules has been changed to be case-insensitive.
* Proxy supports configuring annotations, which will be displayed in the frps dashboard.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,8 +4,8 @@
<head>
<meta charset="utf-8">
<title>frps dashboard</title>
<script type="module" crossorigin src="./index-1gecbKzv.js"></script>
<link rel="stylesheet" crossorigin href="./index-Lf6B06jY.css">
<script type="module" crossorigin src="./index-ky4re_ta.js"></script>
<link rel="stylesheet" crossorigin href="./index-rzPDshRD.css">
</head>
<body>

View File

@ -164,11 +164,16 @@ healthCheck.type = "tcp"
healthCheck.timeoutSeconds = 3
# If continuous failed in 3 times, the proxy will be removed from frps
healthCheck.maxFailed = 3
# every 10 seconds will do a health check
# Every 10 seconds will do a health check
healthCheck.intervalSeconds = 10
# additional meta info for each proxy
# Additional meta info for each proxy. It will be passed to the server-side plugin for use.
metadatas.var1 = "abc"
metadatas.var2 = "123"
# You can add some extra information to the proxy through annotations.
# These annotations will be displayed on the frps dashboard.
[proxies.annotations]
key1 = "value1"
"prefix/key2" = "value2"
[[proxies]]
name = "ssh_random"

View File

@ -1,4 +1,4 @@
FROM golang:1.21 AS building
FROM golang:1.22 AS building
COPY . /building
WORKDIR /building

View File

@ -1,4 +1,4 @@
FROM golang:1.21 AS building
FROM golang:1.22 AS building
COPY . /building
WORKDIR /building

5
go.mod
View File

@ -1,6 +1,6 @@
module github.com/fatedier/frp
go 1.20
go 1.21
require (
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
@ -24,6 +24,7 @@ require (
github.com/spf13/cobra v1.8.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.4
github.com/tidwall/gjson v1.17.1
golang.org/x/crypto v0.17.0
golang.org/x/net v0.17.0
golang.org/x/oauth2 v0.10.0
@ -64,6 +65,8 @@ require (
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect
github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
golang.org/x/mod v0.10.0 // indirect

6
go.sum
View File

@ -145,6 +145,12 @@ github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 h1:89CEmDvlq/F7S
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161/go.mod h1:wM7WEvslTq+iOEAMDLSzhVuOt5BRZ05WirO+b09GHQU=
github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b h1:fj5tQ8acgNUr6O8LEplsxDhUIe2573iLkJc+PqnzZTI=
github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b/go.mod h1:5XA7W9S6mni3h5uvOC75dA3m9CCCaS83lltmc0ukdi4=
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E=

View File

@ -175,6 +175,9 @@ func transformHeadersFromPluginParams(params map[string]string) v1.HeaderOperati
continue
}
if k = strings.TrimPrefix(k, "plugin_header_"); k != "" {
if out.Set == nil {
out.Set = make(map[string]string)
}
out.Set[k] = v
}
}

View File

@ -105,9 +105,10 @@ type DomainConfig struct {
}
type ProxyBaseConfig struct {
Name string `json:"name"`
Type string `json:"type"`
Transport ProxyTransport `json:"transport,omitempty"`
Name string `json:"name"`
Type string `json:"type"`
Annotations map[string]string `json:"annotations,omitempty"`
Transport ProxyTransport `json:"transport,omitempty"`
// metadata info for each proxy
Metadatas map[string]string `json:"metadatas,omitempty"`
LoadBalancer LoadBalancerConfig `json:"loadBalancer,omitempty"`
@ -138,6 +139,7 @@ func (c *ProxyBaseConfig) MarshalToMsg(m *msg.NewProxy) {
m.Group = c.LoadBalancer.Group
m.GroupKey = c.LoadBalancer.GroupKey
m.Metas = c.Metadatas
m.Annotations = c.Annotations
}
func (c *ProxyBaseConfig) UnmarshalFromMsg(m *msg.NewProxy) {
@ -154,6 +156,7 @@ func (c *ProxyBaseConfig) UnmarshalFromMsg(m *msg.NewProxy) {
c.LoadBalancer.Group = m.Group
c.LoadBalancer.GroupKey = m.GroupKey
c.Metadatas = m.Metas
c.Annotations = m.Annotations
}
type TypedProxyConfig struct {

View File

@ -20,6 +20,7 @@ import (
"strings"
"github.com/samber/lo"
"k8s.io/apimachinery/pkg/util/validation"
v1 "github.com/fatedier/frp/pkg/config/v1"
)
@ -29,10 +30,12 @@ func validateProxyBaseConfigForClient(c *v1.ProxyBaseConfig) error {
return errors.New("name should not be empty")
}
if err := ValidateAnnotations(c.Annotations); err != nil {
return err
}
if !lo.Contains([]string{"", "v1", "v2"}, c.Transport.ProxyProtocolVersion) {
return fmt.Errorf("not support proxy protocol version: %s", c.Transport.ProxyProtocolVersion)
}
if !lo.Contains([]string{"client", "server"}, c.Transport.BandwidthLimitMode) {
return fmt.Errorf("bandwidth limit mode should be client or server")
}
@ -61,7 +64,10 @@ func validateProxyBaseConfigForClient(c *v1.ProxyBaseConfig) error {
return nil
}
func validateProxyBaseConfigForServer(c *v1.ProxyBaseConfig, s *v1.ServerConfig) error {
func validateProxyBaseConfigForServer(c *v1.ProxyBaseConfig) error {
if err := ValidateAnnotations(c.Annotations); err != nil {
return err
}
return nil
}
@ -161,7 +167,7 @@ func validateSUDPProxyConfigForClient(c *v1.SUDPProxyConfig) error {
func ValidateProxyConfigurerForServer(c v1.ProxyConfigurer, s *v1.ServerConfig) error {
base := c.GetBaseConfig()
if err := validateProxyBaseConfigForServer(base, s); err != nil {
if err := validateProxyBaseConfigForServer(base); err != nil {
return err
}
@ -231,3 +237,34 @@ func validateXTCPProxyConfigForServer(c *v1.XTCPProxyConfig, s *v1.ServerConfig)
func validateSUDPProxyConfigForServer(c *v1.SUDPProxyConfig, s *v1.ServerConfig) error {
return nil
}
// ValidateAnnotations validates that a set of annotations are correctly defined.
func ValidateAnnotations(annotations map[string]string) error {
if len(annotations) == 0 {
return nil
}
var errs error
for k := range annotations {
for _, msg := range validation.IsQualifiedName(strings.ToLower(k)) {
errs = AppendError(errs, fmt.Errorf("annotation key %s is invalid: %s", k, msg))
}
}
if err := ValidateAnnotationsSize(annotations); err != nil {
errs = AppendError(errs, err)
}
return errs
}
const TotalAnnotationSizeLimitB int = 256 * (1 << 10) // 256 kB
func ValidateAnnotationsSize(annotations map[string]string) error {
var totalSize int64
for k, v := range annotations {
totalSize += (int64)(len(k)) + (int64)(len(v))
}
if totalSize > (int64)(TotalAnnotationSizeLimitB) {
return fmt.Errorf("annotations size %d is larger than limit %d", totalSize, TotalAnnotationSizeLimitB)
}
return nil
}

View File

@ -108,6 +108,7 @@ type NewProxy struct {
Group string `json:"group,omitempty"`
GroupKey string `json:"group_key,omitempty"`
Metas map[string]string `json:"metas,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
// tcp and udp only
RemotePort int `json:"remote_port,omitempty"`

View File

@ -0,0 +1,54 @@
package basic
import (
"fmt"
"io"
"net/http"
"github.com/onsi/ginkgo/v2"
"github.com/tidwall/gjson"
"github.com/fatedier/frp/test/e2e/framework"
"github.com/fatedier/frp/test/e2e/framework/consts"
)
var _ = ginkgo.Describe("[Feature: Annotations]", func() {
f := framework.NewDefaultFramework()
ginkgo.It("Set Proxy Annotations", func() {
webPort := f.AllocPort()
serverConf := consts.DefaultServerConfig + fmt.Sprintf(`
webServer.port = %d
`, webPort)
p1Port := f.AllocPort()
clientConf := consts.DefaultClientConfig + fmt.Sprintf(`
[[proxies]]
name = "p1"
type = "tcp"
localPort = {{ .%s }}
remotePort = %d
[proxies.annotations]
"frp.e2e.test/foo" = "value1"
"frp.e2e.test/bar" = "value2"
`, framework.TCPEchoServerPort, p1Port)
f.RunProcesses([]string{serverConf}, []string{clientConf})
framework.NewRequestExpect(f).Port(p1Port).Ensure()
// check annotations in frps
resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/api/proxy/tcp/%s", webPort, "p1"))
framework.ExpectNoError(err)
framework.ExpectEqual(resp.StatusCode, 200)
defer resp.Body.Close()
content, err := io.ReadAll(resp.Body)
framework.ExpectNoError(err)
annotations := gjson.Get(string(content), "conf.annotations").Map()
framework.ExpectEqual("value1", annotations["frp.e2e.test/foo"].String())
framework.ExpectEqual("value2", annotations["frp.e2e.test/bar"].String())
})
})

View File

@ -9,19 +9,21 @@ declare module 'vue' {
export interface GlobalComponents {
ElButton: typeof import('element-plus/es')['ElButton']
ElCol: typeof import('element-plus/es')['ElCol']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDivider: typeof import('element-plus/es')['ElDivider']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElRow: typeof import('element-plus/es')['ElRow']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
ElTag: typeof import('element-plus/es')['ElTag']
ElText: typeof import('element-plus/es')['ElText']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
LongSpan: typeof import('./src/components/LongSpan.vue')['default']
ProxiesHTTP: typeof import('./src/components/ProxiesHTTP.vue')['default']

View File

@ -30,27 +30,6 @@
>
<el-table-column type="expand">
<template #default="props">
<el-popover
placement="right"
width="600"
style="margin-left: 0px"
trigger="click"
>
<template #default>
<Traffic :proxyName="props.row.name" />
</template>
<template #reference>
<el-button
type="primary"
size="large"
:name="props.row.name"
style="margin-bottom: 10px"
>Traffic Statistics
</el-button>
</template>
</el-popover>
<ProxyViewExpand :row="props.row" :proxyType="proxyType" />
</template>
</el-table-column>
@ -82,8 +61,27 @@
<el-tag v-else type="danger">{{ scope.row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column label="Operations">
<template #default="scope">
<el-button
type="primary"
:name="scope.row.name"
style="margin-bottom: 10px"
@click="dialogVisibleName = scope.row.name; dialogVisible = true"
>Traffic
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<el-dialog
v-model="dialogVisible"
destroy-on-close="true"
:title="dialogVisibleName"
width="700px">
<Traffic :proxyName="dialogVisibleName" />
</el-dialog>
</template>
<script setup lang="ts">
@ -92,6 +90,7 @@ import type { TableColumnCtx } from 'element-plus'
import type { BaseProxy } from '../utils/proxy.js'
import { ElMessage } from 'element-plus'
import ProxyViewExpand from './ProxyViewExpand.vue'
import { ref } from 'vue'
defineProps<{
proxies: BaseProxy[]
@ -100,6 +99,9 @@ defineProps<{
const emit = defineEmits(['refresh'])
const dialogVisible = ref(false)
const dialogVisibleName = ref("")
const formatTrafficIn = (row: BaseProxy, _: TableColumnCtx<BaseProxy>) => {
return Humanize.fileSize(row.trafficIn)
}

View File

@ -60,11 +60,56 @@
<span>{{ row.lastCloseTime }}</span>
</el-form-item>
</el-form>
<div v-if="row.annotations && row.annotations.size > 0">
<el-divider />
<el-text class="title-text" size="large">Annotations</el-text>
<ul>
<li v-for="item in annotationsArray()">
<span class="annotation-key">{{ item.key }}</span>
<span>{{ item.value }}</span>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
defineProps<{
const props = defineProps<{
row: any
proxyType: string
}>()
// annotationsArray returns an array of key-value pairs from the annotations map.
const annotationsArray = (): Array<{ key: string; value: string }> => {
const array: Array<{ key: string; value: any }> = [];
if (props.row.annotations) {
props.row.annotations.forEach((value: any, key: string) => {
array.push({ key, value });
});
}
return array;
}
</script>
<style>
ul {
list-style-type: none;
padding: 5px;
}
ul li {
justify-content: space-between;
padding: 5px;
}
ul .annotation-key {
width: 300px;
display: inline-block;
vertical-align: middle;
}
.title-text {
color: #99a9bf;
}
</style>

View File

@ -1,6 +1,7 @@
class BaseProxy {
name: string
type: string
annotations: Map<string, string>
encryption: boolean
compression: boolean
conns: number
@ -21,6 +22,13 @@ class BaseProxy {
constructor(proxyStats: any) {
this.name = proxyStats.name
this.type = ''
this.annotations = new Map<string, string>()
if (proxyStats.conf?.annotations) {
for (const key in proxyStats.conf.annotations) {
this.annotations.set(key, proxyStats.conf.annotations[key])
}
}
this.encryption = false
this.compression = false
this.encryption =