Przeglądaj źródła

proxy supports configuring annotations, which will be displayed in the frps dashboard (#4000)

fatedier 11 miesięcy temu
rodzic
commit
3529158f31

+ 2 - 2
README.md

@@ -526,6 +526,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 +536,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

+ 1 - 9
Release.md

@@ -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.

Plik diff jest za duży
+ 0 - 4
assets/frps/static/index-1gecbKzv.js


Plik diff jest za duży
+ 0 - 0
assets/frps/static/index-Lf6B06jY.css


Plik diff jest za duży
+ 4 - 0
assets/frps/static/index-ky4re_ta.js


Plik diff jest za duży
+ 0 - 0
assets/frps/static/index-rzPDshRD.css


+ 2 - 2
assets/frps/static/index.html

@@ -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>

+ 7 - 2
conf/frpc_full_example.toml

@@ -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"

+ 3 - 0
go.mod

@@ -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 - 0
go.sum

@@ -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=

+ 6 - 3
pkg/config/v1/proxy.go

@@ -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 {

+ 40 - 3
pkg/config/v1/validation/proxy.go

@@ -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
+}

+ 1 - 0
pkg/msg/msg.go

@@ -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"`

+ 54 - 0
test/e2e/v1/basic/annotations.go

@@ -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())
+	})
+})

+ 3 - 1
web/frps/components.d.ts

@@ -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']

+ 23 - 21
web/frps/src/components/ProxyView.vue

@@ -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)
 }

+ 46 - 1
web/frps/src/components/ProxyViewExpand.vue

@@ -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>

+ 8 - 0
web/frps/src/utils/proxy.ts

@@ -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 =

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików