diff --git a/.golangci.yml b/.golangci.yml index 0fe5d726a5d..18cbaf0be8a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -120,16 +120,18 @@ issues: # - composite literal uses unkeyed fields exclude-rules: - # Exclude some linters from running on test files. - - path: _test\.go$|^tests/|^samples/ - linters: - - errcheck - - maligned - - # keep it until we only support go1.20 - - linters: - - staticcheck - text: "SA1019: rand.Seed has been deprecated" + # Exclude some linters from running on test files. + - path: _test\.go$|^tests/|^samples/ + linters: + - errcheck + - maligned + - linters: + - revive + - stylecheck + text: "use underscores in Go names" + - linters: + - revive + text: "unused-parameter" # Independently from option `exclude` we use default exclude patterns, # it can be disabled by this option. To list all diff --git a/Makefile.cross-compiles b/Makefile.cross-compiles index 515a3c262b3..3c123df8af6 100644 --- a/Makefile.cross-compiles +++ b/Makefile.cross-compiles @@ -2,7 +2,7 @@ export PATH := $(GOPATH)/bin:$(PATH) export GO111MODULE=on LDFLAGS := -s -w -os-archs=darwin:amd64 darwin:arm64 freebsd:386 freebsd:amd64 linux:386 linux:amd64 linux:arm linux:arm64 windows:386 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64 +os-archs=darwin:amd64 darwin:arm64 freebsd:amd64 linux:amd64 linux:arm linux:arm64 windows:amd64 windows:arm64 linux:mips64 linux:mips64le linux:mips:softfloat linux:mipsle:softfloat linux:riscv64 all: build @@ -19,8 +19,6 @@ app: env CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} GOMIPS=$${gomips} go build -trimpath -ldflags "$(LDFLAGS)" -o ./release/frps_$${target_suffix} ./cmd/frps;\ echo "Build $${os}-$${arch} done";\ ) - @mv ./release/frpc_windows_386 ./release/frpc_windows_386.exe - @mv ./release/frps_windows_386 ./release/frps_windows_386.exe @mv ./release/frpc_windows_amd64 ./release/frpc_windows_amd64.exe @mv ./release/frps_windows_amd64 ./release/frps_windows_amd64.exe @mv ./release/frpc_windows_arm64 ./release/frpc_windows_arm64.exe diff --git a/README.md b/README.md index 0ae937496c8..7c7656bf522 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ frp also offers a P2P connect mode. * [Configuration Files](#configuration-files) * [Using Environment Variables](#using-environment-variables) * [Split Configures Into Different Files](#split-configures-into-different-files) - * [Dashboard](#dashboard) + * [Server Dashboard](#server-dashboard) * [Admin UI](#admin-ui) * [Monitor](#monitor) * [Prometheus](#prometheus) @@ -72,10 +72,8 @@ frp also offers a P2P connect mode. * [URL Routing](#url-routing) * [TCP Port Multiplexing](#tcp-port-multiplexing) * [Connecting to frps via HTTP PROXY](#connecting-to-frps-via-http-proxy) - * [Range ports mapping](#range-ports-mapping) * [Client Plugins](#client-plugins) * [Server Manage Plugins](#server-manage-plugins) -* [Development Plan](#development-plan) * [Contributing](#contributing) * [Donation](#donation) * [GitHub Sponsors](#github-sponsors) @@ -113,44 +111,45 @@ We sincerely appreciate your support for frp. To begin, download the latest program for your operating system and architecture from the [Release](https://github.com/fatedier/frp/releases) page. -Next, place the `frps` binary and `frps.ini` configuration file on Server A, which has a public IP address. +Next, place the `frps` binary and server configuration file on Server A, which has a public IP address. -Finally, place the `frpc` binary and `frpc.ini` configuration file on Server B, which is located on a LAN that cannot be directly accessed from the public internet. +Finally, place the `frpc` binary and client configuration file on Server B, which is located on a LAN that cannot be directly accessed from the public internet. + +Some antiviruses improperly mark frpc as malware and delete it. This is due to frp being a networking tool capable of creating reverse proxies. Antiviruses sometimes flag reverse proxies due to their ability to bypass firewall port restrictions. If you are using antivirus, then you may need to whitelist/exclude frpc in your antivirus settings to avoid accidental quarantine/deletion. See [issue 3637](https://github.com/fatedier/frp/issues/3637) for more details. ### Access your computer in a LAN network via SSH -1. Modify `frps.ini` on server A by setting the `bind_port` for frp clients to connect to: +1. Modify `frps.toml` on server A by setting the `bindPort` for frp clients to connect to: - ```ini - # frps.ini - [common] - bind_port = 7000 + ```toml + # frps.toml + bindPort = 7000 ``` 2. Start `frps` on server A: - `./frps -c ./frps.ini` + `./frps -c ./frps.toml` -3. Modify `frpc.ini` on server B and set the `server_addr` field to the public IP address of your frps server: +3. Modify `frpc.toml` on server B and set the `serverAddr` field to the public IP address of your frps server: - ```ini - # frpc.ini - [common] - server_addr = x.x.x.x - server_port = 7000 + ```toml + # frpc.toml + serverAddr = "x.x.x.x" + serverPort = 7000 - [ssh] - type = tcp - local_ip = 127.0.0.1 - local_port = 22 - remote_port = 6000 + [[proxies]] + name = "ssh" + type = "tcp" + localIP = "127.0.0.1" + localPort = 22 + remotePort = 6000 ``` -Note that the `local_port` (listened on the client) and `remote_port` (exposed on the server) are used for traffic going in and out of the frp system, while the `server_port` is used for communication between frps and frpc. +Note that the `localPort` (listened on the client) and `remotePort` (exposed on the server) are used for traffic going in and out of the frp system, while the `serverPort` is used for communication between frps and frpc. 4. Start `frpc` on server B: - `./frpc -c ./frpc.ini` + `./frpc -c ./frpc.toml` 5. To access server B from another machine through server A via SSH (assuming the username is `test`), use the following command: @@ -160,42 +159,41 @@ Note that the `local_port` (listened on the client) and `remote_port` (exposed o This example implements multiple SSH services exposed through the same port using a proxy of type tcpmux. Similarly, as long as the client supports the HTTP Connect proxy connection method, port reuse can be achieved in this way. -1. Deploy frps on a machine with a public IP and modify the frps.ini file. Here is a simplified configuration: +1. Deploy frps on a machine with a public IP and modify the frps.toml file. Here is a simplified configuration: - ```ini - [common] - bind_port = 7000 - tcpmux_httpconnect_port = 5002 + ```toml + bindPort = 7000 + tcpmuxHTTPConnectPort = 5002 ``` 2. Deploy frpc on the internal machine A with the following configuration: - ```ini - [common] - server_addr = x.x.x.x - server_port = 7000 - - [ssh1] - type = tcpmux - multiplexer = httpconnect - custom_domains = machine-a.example.com - local_ip = 127.0.0.1 - local_port = 22 + ```toml + serverAddr = "x.x.x.x" + serverPort = 7000 + + [[proxies]] + name = "ssh1" + type = "tcpmux" + multiplexer = "httpconnect" + customDomains = ["machine-a.example.com"] + localIP = "127.0.0.1" + localPort = 22 ``` 3. Deploy another frpc on the internal machine B with the following configuration: - ```ini - [common] - server_addr = x.x.x.x - server_port = 7000 - - [ssh2] - type = tcpmux - multiplexer = httpconnect - custom_domains = machine-b.example.com - local_ip = 127.0.0.1 - local_port = 22 + ```toml + serverAddr = "x.x.x.x" + serverPort = 7000 + + [[proxies]] + name = "ssh2" + type = "tcpmux" + multiplexer = "httpconnect" + customDomains = ["machine-b.example.com"] + localIP = "127.0.0.1" + localPort = 22 ``` 4. To access internal machine A using SSH ProxyCommand, assuming the username is "test": @@ -212,38 +210,37 @@ Sometimes we need to expose a local web service behind a NAT network to others f Unfortunately, we cannot resolve a domain name to a local IP. However, we can use frp to expose an HTTP(S) service. -1. Modify `frps.ini` and set the HTTP port for vhost to 8080: +1. Modify `frps.toml` and set the HTTP port for vhost to 8080: - ```ini - # frps.ini - [common] - bind_port = 7000 - vhost_http_port = 8080 + ```toml + # frps.toml + bindPort = 7000 + vhostHTTPPort = 8080 ``` If you want to configure an https proxy, you need to set up the `vhost_https_port`. 2. Start `frps`: - `./frps -c ./frps.ini` + `./frps -c ./frps.toml` -3. Modify `frpc.ini` and set `server_addr` to the IP address of the remote frps server. Specify the `local_port` of your web service: +3. Modify `frpc.toml` and set `serverAddr` to the IP address of the remote frps server. Specify the `localPort` of your web service: - ```ini - # frpc.ini - [common] - server_addr = x.x.x.x - server_port = 7000 + ```toml + # frpc.toml + serverAddr = "x.x.x.x" + serverPort = 7000 - [web] - type = http - local_port = 80 - custom_domains = www.example.com + [[proxies]] + name = "web" + type = "http" + localPort = 80 + customDomains = ["www.example.com"] ``` 4. Start `frpc`: - `./frpc -c ./frpc.ini` + `./frpc -c ./frpc.toml` 5. Map the A record of `www.example.com` to either the public IP of the remote frps server or a CNAME record pointing to your original domain. @@ -251,36 +248,35 @@ Unfortunately, we cannot resolve a domain name to a local IP. However, we can us ### Forward DNS query requests -1. Modify `frps.ini`: +1. Modify `frps.toml`: - ```ini - # frps.ini - [common] - bind_port = 7000 + ```toml + # frps.toml + bindPort = 7000 ``` 2. Start `frps`: - `./frps -c ./frps.ini` + `./frps -c ./frps.toml` -3. Modify `frpc.ini` and set `server_addr` to the IP address of the remote frps server. Forward DNS query requests to the Google Public DNS server `8.8.8.8:53`: +3. Modify `frpc.toml` and set `serverAddr` to the IP address of the remote frps server. Forward DNS query requests to the Google Public DNS server `8.8.8.8:53`: - ```ini - # frpc.ini - [common] - server_addr = x.x.x.x - server_port = 7000 + ```toml + # frpc.toml + serverAddr = "x.x.x.x" + serverPort = 7000 - [dns] - type = udp - local_ip = 8.8.8.8 - local_port = 53 - remote_port = 6000 + [[proxies]] + name = "dns" + type = "udp" + localIP = "8.8.8.8" + localPort = 53 + remotePort = 6000 ``` 4. Start frpc: - `./frpc -c ./frpc.ini` + `./frpc -c ./frpc.toml` 5. Test DNS resolution using the `dig` command: @@ -294,17 +290,18 @@ Configure `frps` as above. 1. Start `frpc` with the following configuration: - ```ini - # frpc.ini - [common] - server_addr = x.x.x.x - server_port = 7000 - - [unix_domain_socket] - type = tcp - remote_port = 6000 - plugin = unix_domain_socket - plugin_unix_path = /var/run/docker.sock + ```toml + # frpc.toml + serverAddr = "x.x.x.x" + serverPort = 7000 + + [[proxies]] + name = "unix_domain_socket" + type = "tcp" + remotePort = 6000 + [proxies.plugin] + type = "unix_domain_socket" + unixPath = "/var/run/docker.sock" ``` 2. Test the configuration by getting the docker version using `curl`: @@ -319,20 +316,21 @@ Configure `frps` as described above, then: 1. Start `frpc` with the following configuration: - ```ini - # frpc.ini - [common] - server_addr = x.x.x.x - server_port = 7000 - - [test_static_file] - type = tcp - remote_port = 6000 - plugin = static_file - plugin_local_path = /tmp/files - plugin_strip_prefix = static - plugin_http_user = abc - plugin_http_passwd = abc + ```toml + # frpc.toml + serverAddr = "x.x.x.x" + serverPort = 7000 + + [[proxies]] + name = "test_static_file" + type = "tcp" + remotePort = 6000 + [proxies.plugin] + type = "static_file" + localPath = "/tmp/files" + stripPrefix = "static" + httpUser = "abc" + httpPassword = "abc" ``` 2. Visit `http://x.x.x.x:6000/static/` from your browser and specify correct username and password to view files in `/tmp/files` on the `frpc` machine. @@ -343,23 +341,24 @@ You may substitute `https2https` for the plugin, and point the `plugin_local_add 1. Start `frpc` with the following configuration: - ```ini - # frpc.ini - [common] - server_addr = x.x.x.x - server_port = 7000 - vhost_https_port = 443 - - [test_https2http] - type = https - custom_domains = test.example.com - - plugin = https2http - plugin_local_addr = 127.0.0.1:80 - plugin_crt_path = ./server.crt - plugin_key_path = ./server.key - plugin_host_header_rewrite = 127.0.0.1 - plugin_header_X-From-Where = frp + ```toml + # frpc.toml + serverAddr = "x.x.x.x" + serverPort = 7000 + vhostHTTPSPort = 443 + + [[proxies]] + name = "test_https2http" + type = "https" + customDomains = ["test.example.com"] + + [proxies.plugin] + type = "https2http" + localAddr = "127.0.0.1:80" + crtPath = "./server.crt" + keyPath = "./server.key" + hostHeaderRewrite = "127.0.0.1" + requestHeaders.set.x-from-where = "frp" ``` 2. Visit `https://test.example.com`. @@ -372,34 +371,33 @@ Configure `frps` same as above. 1. Start `frpc` on machine B with the following config. This example is for exposing the SSH service (port 22), and note the `sk` field for the preshared key, and that the `remote_port` field is removed here: - ```ini - # frpc.ini - [common] - server_addr = x.x.x.x - server_port = 7000 - - [secret_ssh] - type = stcp - sk = abcdefg - local_ip = 127.0.0.1 - local_port = 22 + ```toml + # frpc.toml + serverAddr = "x.x.x.x" + serverPort = 7000 + + [[proxies]] + name = "secret_ssh" + type = "stcp" + secretKey = "abcdefg" + localIP = "127.0.0.1" + localPort = 22 ``` 2. Start another `frpc` (typically on another machine C) with the following config to access the SSH service with a security key (`sk` field): - ```ini - # frpc.ini - [common] - server_addr = x.x.x.x - server_port = 7000 - - [secret_ssh_visitor] - type = stcp - role = visitor - server_name = secret_ssh - sk = abcdefg - bind_addr = 127.0.0.1 - bind_port = 6000 + ```toml + # frpc.toml + serverAddr = "x.x.x.x" + serverPort = 7000 + + [[visitors]] + name = "secret_ssh_visitor" + type = "stcp" + serverName = "secret_ssh" + secretKey = "abcdefg" + bindAddr = "127.0.0.1" + bindPort = 6000 ``` 3. On machine C, connect to SSH on machine B, using this command: @@ -412,42 +410,41 @@ Configure `frps` same as above. Note that it may not work with all types of NAT devices. You might want to fallback to stcp if xtcp doesn't work. -1. Start `frpc` on machine B, and expose the SSH port. Note that the `remote_port` field is removed: +1. Start `frpc` on machine B, and expose the SSH port. Note that the `remotePort` field is removed: - ```ini - # frpc.ini - [common] - server_addr = x.x.x.x - server_port = 7000 + ```toml + # frpc.toml + serverAddr = "x.x.x.x" + serverPort = 7000 # set up a new stun server if the default one is not available. - # nat_hole_stun_server = xxx - - [p2p_ssh] - type = xtcp - sk = abcdefg - local_ip = 127.0.0.1 - local_port = 22 + # natHoleStunServer = "xxx" + + [[proxies]] + name = "p2p_ssh" + type = "xtcp" + secretKey = "abcdefg" + localIP = "127.0.0.1" + localPort = 22 ``` 2. Start another `frpc` (typically on another machine C) with the configuration to connect to SSH using P2P mode: - ```ini - # frpc.ini - [common] - server_addr = x.x.x.x - server_port = 7000 + ```toml + # frpc.toml + serverAddr = "x.x.x.x" + serverPort = 7000 # set up a new stun server if the default one is not available. - # nat_hole_stun_server = xxx - - [p2p_ssh_visitor] - type = xtcp - role = visitor - server_name = p2p_ssh - sk = abcdefg - bind_addr = 127.0.0.1 - bind_port = 6000 + # natHoleStunServer = "xxx" + + [[visitors]] + name = "p2p_ssh_visitor" + type = "xtcp" + serverName = "p2p_ssh" + secretKey = "abcdefg" + bindAddr = "127.0.0.1" + bindPort = 6000 # when automatic tunnel persistence is required, set it to true - keep_tunnel_open = false + keepTunnelOpen = false ``` 3. On machine C, connect to SSH on machine B, using this command: @@ -458,35 +455,39 @@ Note that it may not work with all types of NAT devices. You might want to fallb ### Configuration Files +Since v0.52.0, we support TOML, YAML, and JSON for configuration. Please note that INI is deprecated and will be removed in future releases. New features will only be available in TOML, YAML, or JSON. Users wanting these new features should switch their configuration format accordingly. + Read the full example configuration files to find out even more features not described here. -[Full configuration file for frps (Server)](./conf/frps_full.ini) +Examples use TOML format, but you can still use YAML or JSON. + +[Full configuration file for frps (Server)](./conf/frps.toml) -[Full configuration file for frpc (Client)](./conf/frpc_full.ini) +[Full configuration file for frpc (Client)](./conf/frpc.toml) ### Using Environment Variables Environment variables can be referenced in the configuration file, using Go's standard format: -```ini -# frpc.ini -[common] -server_addr = {{ .Envs.FRP_SERVER_ADDR }} -server_port = 7000 - -[ssh] -type = tcp -local_ip = 127.0.0.1 -local_port = 22 -remote_port = {{ .Envs.FRP_SSH_REMOTE_PORT }} +```toml +# frpc.toml +serverAddr = "{{ .Envs.FRP_SERVER_ADDR }}" +serverPort = 7000 + +[[proxies]] +name = "ssh" +type = "tcp" +localIP = "127.0.0.1" +localPort = 22 +remotePort = "{{ .Envs.FRP_SSH_REMOTE_PORT }}" ``` With the config above, variables can be passed into `frpc` program like this: ``` -export FRP_SERVER_ADDR="x.x.x.x" -export FRP_SSH_REMOTE_PORT="6000" -./frpc -c ./frpc.ini +export FRP_SERVER_ADDR=x.x.x.x +export FRP_SSH_REMOTE_PORT=6000 +./frpc -c ./frpc.toml ``` `frpc` will render configuration file template using OS environment variables. Remember to prefix your reference with `.Envs`. @@ -495,81 +496,77 @@ export FRP_SSH_REMOTE_PORT="6000" You can split multiple proxy configs into different files and include them in the main file. -```ini -# frpc.ini -[common] -server_addr = x.x.x.x -server_port = 7000 -includes=./confd/*.ini +```toml +# frpc.toml +serverAddr = "x.x.x.x" +serverPort = 7000 +includes = ["./confd/*.toml"] ``` -```ini -# ./confd/test.ini -[ssh] -type = tcp -local_ip = 127.0.0.1 -local_port = 22 -remote_port = 6000 +```toml +# ./confd/test.toml +[[proxies]] +name = "ssh" +type = "tcp" +localIP = "127.0.0.1" +localPort = 22 +remotePort = 6000 ``` -### Dashboard +### Server Dashboard Check frp's status and proxies' statistics information by Dashboard. Configure a port for dashboard to enable this feature: -```ini -[common] -dashboard_port = 7500 +```toml +webServer.port = 7500 # dashboard's username and password are both optional -dashboard_user = admin -dashboard_pwd = admin +webServer.user = "admin" +webServer.password = "admin" ``` Then visit `http://[server_addr]:7500` to see the dashboard, with username and password both being `admin`. Additionally, you can use HTTPS port by using your domains wildcard or normal SSL certificate: -```ini -[common] -dashboard_port = 7500 +```toml +webServer.port = 7500 # dashboard's username and password are both optional -dashboard_user = admin -dashboard_pwd = admin -dashboard_tls_mode = true -dashboard_tls_cert_file = server.crt -dashboard_tls_key_file = server.key +webServer.user = "admin" +webServer.password = "admin" +webServer.tls.certFile = "server.crt" +webServer.tls.keyFile = "server.key" ``` Then visit `https://[server_addr]:7500` to see the dashboard in secure HTTPS connection, with username and password both being `admin`. ![dashboard](/doc/pic/dashboard.png) -### Admin UI +### Client Admin UI -The Admin UI helps you check and manage frpc's configuration. +The Client Admin UI helps you check and manage frpc's configuration. Configure an address for admin UI to enable this feature: -```ini -[common] -admin_addr = 127.0.0.1 -admin_port = 7400 -admin_user = admin -admin_pwd = admin +```toml +webServer.addr = "127.0.0.1" +webServer.port = 7400 +webServer.user = "admin" +webServer.password = "admin" ``` Then visit `http://127.0.0.1:7400` to see admin UI, with username and password both being `admin`. ### Monitor -When dashboard is enabled, frps will save monitor data in cache. It will be cleared after process restart. +When web server is enabled, frps will save monitor data in cache for 7 days. It will be cleared after process restart. Prometheus is also supported. #### Prometheus -Enable dashboard first, then configure `enable_prometheus = true` in `frps.ini`. +Enable dashboard first, then configure `enablePrometheus = true` in `frps.toml`. `http://{dashboard_addr}/metrics` will provide prometheus monitor data. @@ -577,80 +574,81 @@ Enable dashboard first, then configure `enable_prometheus = true` in `frps.ini`. There are 2 authentication methods to authenticate frpc with frps. -You can decide which one to use by configuring `authentication_method` under `[common]` in `frpc.ini` and `frps.ini`. +You can decide which one to use by configuring `auth.method` in `frpc.toml` and `frps.toml`, the default one is token. -Configuring `authenticate_heartbeats = true` under `[common]` will use the configured authentication method to add and validate authentication on every heartbeat between frpc and frps. +Configuring `auth.additionalScopes = ["HeartBeats"]` will use the configured authentication method to add and validate authentication on every heartbeat between frpc and frps. -Configuring `authenticate_new_work_conns = true` under `[common]` will do the same for every new work connection between frpc and frps. +Configuring `auth.additionalScopes = ["NewWorkConns"]` will do the same for every new work connection between frpc and frps. #### Token Authentication -When specifying `authentication_method = token` under `[common]` in `frpc.ini` and `frps.ini` - token based authentication will be used. +When specifying `auth.method = "token"` in `frpc.toml` and `frps.toml` - token based authentication will be used. -Make sure to specify the same `token` in the `[common]` section in `frps.ini` and `frpc.ini` for frpc to pass frps validation +Make sure to specify the same `auth.token` in `frps.toml` and `frpc.toml` for frpc to pass frps validation #### OIDC Authentication -When specifying `authentication_method = oidc` under `[common]` in `frpc.ini` and `frps.ini` - OIDC based authentication will be used. +When specifying `auth.method = "oidc"` in `frpc.toml` and `frps.toml` - OIDC based authentication will be used. OIDC stands for OpenID Connect, and the flow used is called [Client Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.4). -To use this authentication type - configure `frpc.ini` and `frps.ini` as follows: +To use this authentication type - configure `frpc.toml` and `frps.toml` as follows: -```ini -# frps.ini -[common] -authentication_method = oidc -oidc_issuer = https://example-oidc-issuer.com/ -oidc_audience = https://oidc-audience.com/.default +```toml +# frps.toml +auth.method = "oidc" +auth.oidc.issuer = "https://example-oidc-issuer.com/" +auth.oidc.audience = "https://oidc-audience.com/.default" ``` -```ini -# frpc.ini -[common] -authentication_method = oidc -oidc_client_id = 98692467-37de-409a-9fac-bb2585826f18 # Replace with OIDC client ID -oidc_client_secret = oidc_secret -oidc_audience = https://oidc-audience.com/.default -oidc_token_endpoint_url = https://example-oidc-endpoint.com/oauth2/v2.0/token +```toml +# frpc.toml +auth.method = "oidc" +auth.oidc.clientID = "98692467-37de-409a-9fac-bb2585826f18" # Replace with OIDC client ID +auth.oidc.clientSecret = "oidc_secret" +auth.oidc.audience = "https://oidc-audience.com/.default" +auth.oidc.tokenEndpointURL = "https://example-oidc-endpoint.com/oauth2/v2.0/token" ``` ### Encryption and Compression The features are off by default. You can turn on encryption and/or compression: -```ini -# frpc.ini -[ssh] -type = tcp -local_port = 22 -remote_port = 6000 -use_encryption = true -use_compression = true +```toml +# frpc.toml +[[proxies]] +name = "ssh" +type = "tcp" +localPort = 22 +remotePort = 6000 +transport.useEncryption = true +transport.useCompression = true ``` #### TLS -Since v0.50.0, the default value of `tls_enable` and `disable_custom_tls_first_byte` has been changed to true, and tls is enabled by default. +Since v0.50.0, the default value of `transport.tls.enable` and `transport.tls.disableCustomTLSFirstByte` has been changed to true, and tls is enabled by default. -For port multiplexing, frp sends a first byte `0x17` to dial a TLS connection. This only takes effect when you set `disable_custom_tls_first_byte` to false. +For port multiplexing, frp sends a first byte `0x17` to dial a TLS connection. This only takes effect when you set `transport.tls.disableCustomTLSFirstByte` to false. -To **enforce** `frps` to only accept TLS connections - configure `tls_only = true` in the `[common]` section in `frps.ini`. **This is optional.** +To **enforce** `frps` to only accept TLS connections - configure `transport.tls.force = true` in `frps.toml`. **This is optional.** -**`frpc` TLS settings (under the `[common]` section):** -```ini -tls_enable = true -tls_cert_file = certificate.crt -tls_key_file = certificate.key -tls_trusted_ca_file = ca.crt +**`frpc` TLS settings:** + +```toml +transport.tls.enable = true +transport.tls.certFile = "certificate.crt" +transport.tls.keyFile = "certificate.key" +transport.tls.trustedCaFile = "ca.crt" ``` -**`frps` TLS settings (under the `[common]` section):** -```ini -tls_only = true -tls_cert_file = certificate.crt -tls_key_file = certificate.key -tls_trusted_ca_file = ca.crt +**`frps` TLS settings:** + +```toml +transport.tls.force = true +transport.tls.certFile = "certificate.crt" +transport.tls.keyFile = "certificate.key" +transport.tls.trustedCaFile = "ca.crt" ``` You will need **a root CA cert** and **at least one SSL/TLS certificate**. It **can** be self-signed or regular (such as Let's Encrypt or another SSL/TLS certificate provider). @@ -727,40 +725,41 @@ openssl x509 -req -days 365 -sha256 \ ### Hot-Reloading frpc configuration -The `admin_addr` and `admin_port` fields are required for enabling HTTP API: +The `webServer` fields are required for enabling HTTP API: -```ini -# frpc.ini -[common] -admin_addr = 127.0.0.1 -admin_port = 7400 +```toml +# frpc.toml +webServer.addr = "127.0.0.1" +webServer.port = 7400 ``` -Then run command `frpc reload -c ./frpc.ini` and wait for about 10 seconds to let `frpc` create or update or remove proxies. +Then run command `frpc reload -c ./frpc.toml` and wait for about 10 seconds to let `frpc` create or update or remove proxies. -**Note that parameters in [common] section won't be modified except 'start'.** +**Note that global client parameters won't be modified except 'start'.** -You can run command `frpc verify -c ./frpc.ini` before reloading to check if there are config errors. +You can run command `frpc verify -c ./frpc.toml` before reloading to check if there are config errors. ### Get proxy status from client -Use `frpc status -c ./frpc.ini` to get status of all proxies. The `admin_addr` and `admin_port` fields are required for enabling HTTP API. +Use `frpc status -c ./frpc.toml` to get status of all proxies. The `webServer` fields are required for enabling HTTP API. ### Only allowing certain ports on the server -`allow_ports` in `frps.ini` is used to avoid abuse of ports: +`allowPorts` in `frps.toml` is used to avoid abuse of ports: -```ini -# frps.ini -[common] -allow_ports = 2000-3000,3001,3003,4000-50000 +```toml +# frps.toml +allowPorts = [ + { start = 2000, end = 3000 }, + { single = 3001 }, + { single = 3003 }, + { start = 4000, end = 50000 } +] ``` -`allow_ports` consists of specific ports or port ranges (lowest port number, dash `-`, highest port number), separated by comma `,`. - ### Port Reuse -`vhost_http_port` and `vhost_https_port` in frps can use same port with `bind_port`. frps will detect the connection's protocol and handle it correspondingly. +`vhostHTTPPort` and `vhostHTTPSPort` in frps can use same port with `bindPort`. frps will detect the connection's protocol and handle it correspondingly. We would like to try to allow multiple proxies bind a same remote port with different protocols in the future. @@ -768,29 +767,29 @@ We would like to try to allow multiple proxies bind a same remote port with diff #### For Each Proxy -```ini -# frpc.ini -[ssh] -type = tcp -local_port = 22 -remote_port = 6000 -bandwidth_limit = 1MB +```toml +# frpc.toml +[[proxies]] +name = "ssh" +type = "tcp" +localPort = 22 +remotePort = 6000 +transport.bandwidthLimit = "1MB" ``` -Set `bandwidth_limit` in each proxy's configure to enable this feature. Supported units are `MB` and `KB`. +Set `transport.bandwidthLimit` in each proxy's configure to enable this feature. Supported units are `MB` and `KB`. -Set `bandwidth_limit_mode` to `client` or `server` to limit bandwidth on the client or server side. Default is `client`. +Set `transport.bandwidthLimitMode` to `client` or `server` to limit bandwidth on the client or server side. Default is `client`. ### TCP Stream Multiplexing frp supports tcp stream multiplexing since v0.10.0 like HTTP2 Multiplexing, in which case all logic connections to the same frpc are multiplexed into the same TCP connection. -You can disable this feature by modify `frps.ini` and `frpc.ini`: +You can disable this feature by modify `frps.toml` and `frpc.toml`: -```ini -# frps.ini and frpc.ini, must be same -[common] -tcp_mux = false +```toml +# frps.toml and frpc.toml, must be same +tcpMux = false ``` ### Support KCP Protocol @@ -801,25 +800,23 @@ KCP mode uses UDP as the underlying transport. Using KCP in frp: 1. Enable KCP in frps: - ```ini - # frps.ini - [common] - bind_port = 7000 + ```toml + # frps.toml + bindPort = 7000 # Specify a UDP port for KCP. - kcp_bind_port = 7000 + kcpBindPort = 7000 ``` - The `kcp_bind_port` number can be the same number as `bind_port`, since `bind_port` field specifies a TCP port. + The `kcpBindPort` number can be the same number as `bindPort`, since `bindPort` field specifies a TCP port. -2. Configure `frpc.ini` to use KCP to connect to frps: +2. Configure `frpc.toml` to use KCP to connect to frps: - ```ini - # frpc.ini - [common] - server_addr = x.x.x.x - # Same as the 'kcp_bind_port' in frps.ini - server_port = 7000 - protocol = kcp + ```toml + # frpc.toml + serverAddr = "x.x.x.x" + # Same as the 'kcpBindPort' in frps.toml + serverPort = 7000 + transport.protocol = "kcp" ``` ### Support QUIC Protocol @@ -830,25 +827,23 @@ Using QUIC in frp: 1. Enable QUIC in frps: - ```ini - # frps.ini - [common] - bind_port = 7000 + ```toml + # frps.toml + bindPort = 7000 # Specify a UDP port for QUIC. - quic_bind_port = 7000 + quicBindPort = 7000 ``` - The `quic_bind_port` number can be the same number as `bind_port`, since `bind_port` field specifies a TCP port. + The `quicBindPort` number can be the same number as `bind_port`, since `bind_port` field specifies a TCP port. -2. Configure `frpc.ini` to use QUIC to connect to frps: +2. Configure `frpc.toml` to use QUIC to connect to frps: - ```ini - # frpc.ini - [common] - server_addr = x.x.x.x - # Same as the 'quic_bind_port' in frps.ini - server_port = 7000 - protocol = quic + ```toml + # frpc.toml + serverAddr = "x.x.x.x" + # Same as the 'quicBindPort' in frps.toml + serverPort = 7000 + transport.protocol = "quic" ``` ### Connection Pooling @@ -857,20 +852,18 @@ By default, frps creates a new frpc connection to the backend service upon a use This feature is suitable for a large number of short connections. -1. Configure the limit of pool count each proxy can use in `frps.ini`: +1. Configure the limit of pool count each proxy can use in `frps.toml`: - ```ini - # frps.ini - [common] - max_pool_count = 5 + ```toml + # frps.toml + transport.maxPoolCount = 5 ``` 2. Enable and specify the number of connection pool: - ```ini - # frpc.ini - [common] - pool_count = 1 + ```toml + # frpc.toml + transport.poolCount = 1 ``` ### Load balancing @@ -879,87 +872,92 @@ Load balancing is supported by `group`. This feature is only available for types `tcp`, `http`, `tcpmux` now. -```ini -# frpc.ini -[test1] -type = tcp -local_port = 8080 -remote_port = 80 -group = web -group_key = 123 - -[test2] -type = tcp -local_port = 8081 -remote_port = 80 -group = web -group_key = 123 +```toml +# frpc.toml +[[proxies]] +name = "test1" +type = "tcp" +localPort = 8080 +remotePort = 80 +loadBalancer.group = "web" +loadBalancer.groupKey = "123" + +[[proxies]] +name = "test2" +type = "tcp" +localPort = 8081 +remotePort = 80 +loadBalancer.group = "web" +loadBalancer.groupKey = "123" ``` -`group_key` is used for authentication. +`loadBalancer.groupKey` is used for authentication. Connections to port 80 will be dispatched to proxies in the same group randomly. -For type `tcp`, `remote_port` in the same group should be the same. +For type `tcp`, `remotePort` in the same group should be the same. -For type `http`, `custom_domains`, `subdomain`, `locations` should be the same. +For type `http`, `customDomains`, `subdomain`, `locations` should be the same. ### Service Health Check Health check feature can help you achieve high availability with load balancing. -Add `health_check_type = tcp` or `health_check_type = http` to enable health check. +Add `healthCheck.type = "tcp"` or `healthCheck.type = "http"` to enable health check. With health check type **tcp**, the service port will be pinged (TCPing): -```ini -# frpc.ini -[test1] -type = tcp -local_port = 22 -remote_port = 6000 +```toml +# frpc.toml +[[proxies]] +name = "test1" +type = "tcp" +localPort = 22 +remotePort = 6000 # Enable TCP health check -health_check_type = tcp +healthCheck.type = "tcp" # TCPing timeout seconds -health_check_timeout_s = 3 +healthCheck.timeoutSeconds = 3 # If health check failed 3 times in a row, the proxy will be removed from frps -health_check_max_failed = 3 +healthCheck.maxFailed = 3 # A health check every 10 seconds -health_check_interval_s = 10 +healthCheck.intervalSeconds = 10 ``` With health check type **http**, an HTTP request will be sent to the service and an HTTP 2xx OK response is expected: -```ini -# frpc.ini -[web] -type = http -local_ip = 127.0.0.1 -local_port = 80 -custom_domains = test.example.com +```toml +# frpc.toml +[[proxies]] +name = "web" +type = "http" +localIP = "127.0.0.1" +localPort = 80 +customDomains = ["test.example.com"] # Enable HTTP health check -health_check_type = http +healthCheck.type = "http" # frpc will send a GET request to '/status' # and expect an HTTP 2xx OK response -health_check_url = /status -health_check_timeout_s = 3 -health_check_max_failed = 3 -health_check_interval_s = 10 +healthCheck.path = "/status" +healthCheck.timeoutSeconds = 3 +healthCheck.maxFailed = 3 +healthCheck.intervalSeconds = 10 ``` ### Rewriting the HTTP Host Header By default frp does not modify the tunneled HTTP requests at all as it's a byte-for-byte copy. -However, speaking of web servers and HTTP requests, your web server might rely on the `Host` HTTP header to determine the website to be accessed. frp can rewrite the `Host` header when forwarding the HTTP requests, with the `host_header_rewrite` field: +However, speaking of web servers and HTTP requests, your web server might rely on the `Host` HTTP header to determine the website to be accessed. frp can rewrite the `Host` header when forwarding the HTTP requests, with the `hostHeaderRewrite` field: -```ini -# frpc.ini -[web] -type = http -local_port = 80 -custom_domains = test.example.com -host_header_rewrite = dev.example.com +```toml +# frpc.toml +[[proxies]] +name = "web" +type = "http" +localPort = 80 +customDomains = ["test.example.com"] +hostHeaderRewrite = "dev.example.com" ``` The HTTP request will have the `Host` header rewritten to `Host: dev.example.com` when it reaches the actual web server, although the request from the browser probably has `Host: test.example.com`. @@ -968,19 +966,18 @@ The HTTP request will have the `Host` header rewritten to `Host: dev.example.com Similar to `Host`, You can override other HTTP request headers with proxy type `http`. -```ini -# frpc.ini -[web] -type = http -local_port = 80 -custom_domains = test.example.com -host_header_rewrite = dev.example.com -header_X-From-Where = frp +```toml +# frpc.toml +[[proxies]] +name = "web" +type = "http" +localPort = 80 +customDomains = ["test.example.com"] +hostHeaderRewrite = "dev.example.com" +requestHeaders.set.x-from-where = "frp" ``` -Note that parameter(s) prefixed with `header_` will be added to HTTP request headers. - -In this example, it will set header `X-From-Where: frp` in the HTTP request. +In this example, it will set header `x-from-where: frp` in the HTTP request. ### Get Real IP @@ -996,15 +993,16 @@ frp supports Proxy Protocol to send user's real IP to local services. It support Here is an example for https service: -```ini -# frpc.ini -[web] -type = https -local_port = 443 -custom_domains = test.example.com +```toml +# frpc.toml +[[proxies]] +name = "web" +type = "https" +localPort = 443 +customDomains = ["test.example.com"] # now v1 and v2 are supported -proxy_protocol_version = v2 +transport.proxyProtocolVersion = "v2" ``` You can enable Proxy Protocol support in nginx to expose user's real IP in HTTP header `X-Real-IP`, and then read `X-Real-IP` header in your web service for the real IP. @@ -1017,14 +1015,15 @@ This enforces HTTP Basic Auth on all requests with the username and password spe It can only be enabled when proxy type is http. -```ini -# frpc.ini -[web] -type = http -local_port = 80 -custom_domains = test.example.com -http_user = abc -http_pwd = abc +```toml +# frpc.toml +[[proxies]] +name = "web" +type = "http" +localPort = 80 +customDomains = ["test.example.com"] +httpUser = "abc" +httpPassword = "abc" ``` Visit `http://test.example.com` in the browser and now you are prompted to enter the username and password. @@ -1033,24 +1032,25 @@ Visit `http://test.example.com` in the browser and now you are prompted to enter It is convenient to use `subdomain` configure for http and https types when many people share one frps server. -```ini -# frps.ini -subdomain_host = frps.com +```toml +# frps.toml +subDomainHost = "frps.com" ``` Resolve `*.frps.com` to the frps server's IP. This is usually called a Wildcard DNS record. -```ini -# frpc.ini -[web] -type = http -local_port = 80 -subdomain = test +```toml +# frpc.toml +[[proxies]] +name = "web" +type = "http" +localPort = 80 +subdomain = "test" ``` Now you can visit your web service on `test.frps.com`. -Note that if `subdomain_host` is not empty, `custom_domains` should not be the subdomain of `subdomain_host`. +Note that if `subdomainHost` is not empty, `customDomains` should not be the subdomain of `subdomainHost`. ### URL Routing @@ -1058,59 +1058,61 @@ frp supports forwarding HTTP requests to different backend web services by url r `locations` specifies the prefix of URL used for routing. frps first searches for the most specific prefix location given by literal strings regardless of the listed order. -```ini -# frpc.ini -[web01] -type = http -local_port = 80 -custom_domains = web.example.com -locations = / - -[web02] -type = http -local_port = 81 -custom_domains = web.example.com -locations = /news,/about +```toml +# frpc.toml +[[proxies]] +name = "web01" +type = "http" +localPort = 80 +customDomains = ["web.example.com"] +locations = ["/"] + +[[proxies]] +name = "web02" +type = "http" +localPort = 81 +customDomains = ["web.example.com"] +locations = ["/news", "/about"] ``` HTTP requests with URL prefix `/news` or `/about` will be forwarded to **web02** and other requests to **web01**. ### TCP Port Multiplexing -frp supports receiving TCP sockets directed to different proxies on a single port on frps, similar to `vhost_http_port` and `vhost_https_port`. +frp supports receiving TCP sockets directed to different proxies on a single port on frps, similar to `vhostHTTPPort` and `vhostHTTPSPort`. The only supported TCP port multiplexing method available at the moment is `httpconnect` - HTTP CONNECT tunnel. -When setting `tcpmux_httpconnect_port` to anything other than 0 in frps under `[common]`, frps will listen on this port for HTTP CONNECT requests. +When setting `tcpmuxHTTPConnectPort` to anything other than 0 in frps, frps will listen on this port for HTTP CONNECT requests. -The host of the HTTP CONNECT request will be used to match the proxy in frps. Proxy hosts can be configured in frpc by configuring `custom_domain` and / or `subdomain` under `type = tcpmux` proxies, when `multiplexer = httpconnect`. +The host of the HTTP CONNECT request will be used to match the proxy in frps. Proxy hosts can be configured in frpc by configuring `customDomains` and / or `subdomain` under `tcpmux` proxies, when `multiplexer = "httpconnect"`. For example: -```ini -# frps.ini -[common] -bind_port = 7000 -tcpmux_httpconnect_port = 1337 +```toml +# frps.toml +bindPort = 7000 +tcpmuxHTTPConnectPort = 1337 ``` -```ini -# frpc.ini -[common] -server_addr = x.x.x.x -server_port = 7000 - -[proxy1] -type = tcpmux -multiplexer = httpconnect -custom_domains = test1 -local_port = 80 - -[proxy2] -type = tcpmux -multiplexer = httpconnect -custom_domains = test2 -local_port = 8080 +```toml +# frpc.toml +serverAddr = "x.x.x.x" +serverPort = 7000 + +[[proxies]] +name = "proxy1" +type = "tcpmux" +multiplexer = "httpconnect" +customDomains = ["test1"] +localPort = 80 + +[[proxies]] +name = "proxy2" +type = "tcpmux" +multiplexer = "httpconnect" +customDomains = ["test2"] +localPort = 8080 ``` In the above configuration - frps can be contacted on port 1337 with a HTTP CONNECT header such as: @@ -1120,56 +1122,40 @@ CONNECT test1 HTTP/1.1\r\n\r\n ``` and the connection will be routed to `proxy1`. -### Connecting to frps via HTTP PROXY +### Connecting to frps via PROXY -frpc can connect to frps using HTTP proxy if you set OS environment variable `HTTP_PROXY`, or if `http_proxy` is set in frpc.ini file. +frpc can connect to frps through proxy if you set OS environment variable `HTTP_PROXY`, or if `transport.proxyURL` is set in frpc.toml file. It only works when protocol is tcp. -```ini -# frpc.ini -[common] -server_addr = x.x.x.x -server_port = 7000 -http_proxy = http://user:pwd@192.168.1.128:8080 +```toml +# frpc.toml +serverAddr = "x.x.x.x" +serverPort = 7000 +transport.proxyURL = "http://user:pwd@192.168.1.128:8080" ``` -### Range ports mapping - -Proxy with names that start with `range:` will support mapping range ports. - -```ini -# frpc.ini -[range:test_tcp] -type = tcp -local_ip = 127.0.0.1 -local_port = 6000-6006,6007 -remote_port = 6000-6006,6007 -``` - -frpc will generate 8 proxies like `test_tcp_0`, `test_tcp_1`, ..., `test_tcp_7`. - ### Client Plugins frpc only forwards requests to local TCP or UDP ports by default. Plugins are used for providing rich features. There are built-in plugins such as `unix_domain_socket`, `http_proxy`, `socks5`, `static_file`, `http2https`, `https2http`, `https2https` and you can see [example usage](#example-usage). -Specify which plugin to use with the `plugin` parameter. Configuration parameters of plugin should be started with `plugin_`. `local_ip` and `local_port` are not used for plugin. - Using plugin **http_proxy**: -```ini -# frpc.ini -[http_proxy] -type = tcp -remote_port = 6000 -plugin = http_proxy -plugin_http_user = abc -plugin_http_passwd = abc +```toml +# frpc.toml +[[proxies]] +name = "http_proxy" +type = "tcp" +remotePort = 6000 +[proxies.plugin] +type = "http_proxy" +httpUser = "abc" +httpPassword = "abc" ``` -`plugin_http_user` and `plugin_http_passwd` are configuration parameters used in `http_proxy` plugin. +`httpUser` and `httpPassword` are configuration parameters used in `http_proxy` plugin. ### Server Manage Plugins @@ -1177,10 +1163,6 @@ Read the [document](/doc/server_plugin.md). Find more plugins in [gofrp/plugin](https://github.com/gofrp/plugin). -## Development Plan - -* Log HTTP request information in frps. - ## Contributing Interested in getting involved? We would like to help you! diff --git a/README_zh.md b/README_zh.md index 951a793880b..d6256a1bcb3 100644 --- a/README_zh.md +++ b/README_zh.md @@ -90,10 +90,6 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进 ![zsxq](/doc/pic/zsxq.jpg) -### 支付宝扫码捐赠 - -![donate-alipay](/doc/pic/donate-alipay.png) - ### 微信支付捐赠 ![donate-wechatpay](/doc/pic/donate-wechatpay.png) diff --git a/Release.md b/Release.md index e9d6a662b74..95f1ba3648b 100644 --- a/Release.md +++ b/Release.md @@ -1,3 +1,9 @@ ### Features -* Support Go 1.21. +* Configuration: We now support TOML, YAML, and JSON for configuration. Please note that INI is deprecated and will be removed in future releases. New features will only be available in TOML, YAML, or JSON. Users wanting these new features should switch their configuration format accordingly. #2521 + +### Breaking Changes + +* Change the way to start the visitor through the command line from `frpc stcp --role=visitor xxx` to `frpc stcp visitor xxx`. +* Modified the semantics of the `server_addr` in the command line, no longer including the port. Added the `server_port` parameter to configure the port. +* No longer support range ports mapping in TOML/YAML/JSON. diff --git a/client/admin.go b/client/admin.go index 29e86a6e121..da8bab1bd5c 100644 --- a/client/admin.go +++ b/client/admin.go @@ -38,7 +38,7 @@ func (svr *Service) RunAdminServer(address string) (err error) { router.HandleFunc("/healthz", svr.healthz) // debug - if svr.cfg.PprofEnable { + if svr.cfg.WebServer.PprofEnable { router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) router.HandleFunc("/debug/pprof/profile", pprof.Profile) router.HandleFunc("/debug/pprof/symbol", pprof.Symbol) @@ -47,7 +47,7 @@ func (svr *Service) RunAdminServer(address string) (err error) { } subRouter := router.NewRoute().Subrouter() - user, passwd := svr.cfg.AdminUser, svr.cfg.AdminPwd + user, passwd := svr.cfg.WebServer.User, svr.cfg.WebServer.Password subRouter.Use(utilnet.NewHTTPAuthMiddleware(user, passwd).SetAuthFailDelay(200 * time.Millisecond).Middleware) // api, see admin_api.go diff --git a/client/admin_api.go b/client/admin_api.go index b1d1885cbd3..a348e8dd332 100644 --- a/client/admin_api.go +++ b/client/admin_api.go @@ -30,6 +30,7 @@ import ( "github.com/fatedier/frp/client/proxy" "github.com/fatedier/frp/pkg/config" + "github.com/fatedier/frp/pkg/config/v1/validation" "github.com/fatedier/frp/pkg/util/log" ) @@ -56,15 +57,21 @@ func (svr *Service) apiReload(w http.ResponseWriter, _ *http.Request) { } }() - _, pxyCfgs, visitorCfgs, err := config.ParseClientConfig(svr.cfgFile) + cliCfg, pxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(svr.cfgFile) if err != nil { res.Code = 400 res.Msg = err.Error() log.Warn("reload frpc proxy config error: %s", res.Msg) return } + if _, err := validation.ValidateAllClientConfig(cliCfg, pxyCfgs, visitorCfgs); err != nil { + res.Code = 400 + res.Msg = err.Error() + log.Warn("reload frpc proxy config error: %s", res.Msg) + return + } - if err = svr.ReloadConf(pxyCfgs, visitorCfgs); err != nil { + if err := svr.ReloadConf(pxyCfgs, visitorCfgs); err != nil { res.Code = 500 res.Msg = err.Error() log.Warn("reload frpc proxy config error: %s", res.Msg) @@ -112,7 +119,7 @@ func NewProxyStatusResp(status *proxy.WorkingStatus, serverAddr string) ProxySta if baseCfg.LocalPort != 0 { psr.LocalAddr = net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort)) } - psr.Plugin = baseCfg.Plugin + psr.Plugin = baseCfg.Plugin.Type if status.Err == "" { psr.RemoteAddr = status.RemoteAddr @@ -172,24 +179,14 @@ func (svr *Service) apiGetConfig(w http.ResponseWriter, _ *http.Request) { return } - content, err := config.GetRenderedConfFromFile(svr.cfgFile) + content, err := os.ReadFile(svr.cfgFile) if err != nil { res.Code = 400 res.Msg = err.Error() log.Warn("load frpc config file error: %s", res.Msg) return } - - rows := strings.Split(string(content), "\n") - newRows := make([]string, 0, len(rows)) - for _, row := range rows { - row = strings.TrimSpace(row) - if strings.HasPrefix(row, "token") { - continue - } - newRows = append(newRows, row) - } - res.Msg = strings.Join(newRows, "\n") + res.Msg = string(content) } // PUT /api/config @@ -221,49 +218,7 @@ func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) { return } - // get token from origin content - token := "" - b, err := os.ReadFile(svr.cfgFile) - if err != nil { - res.Code = 400 - res.Msg = err.Error() - log.Warn("load frpc config file error: %s", res.Msg) - return - } - content := string(b) - - for _, row := range strings.Split(content, "\n") { - row = strings.TrimSpace(row) - if strings.HasPrefix(row, "token") { - token = row - break - } - } - - tmpRows := make([]string, 0) - for _, row := range strings.Split(string(body), "\n") { - row = strings.TrimSpace(row) - if strings.HasPrefix(row, "token") { - continue - } - tmpRows = append(tmpRows, row) - } - - newRows := make([]string, 0) - if token != "" { - for _, row := range tmpRows { - newRows = append(newRows, row) - if strings.HasPrefix(row, "[common]") { - newRows = append(newRows, token) - } - } - } else { - newRows = tmpRows - } - content = strings.Join(newRows, "\n") - - err = os.WriteFile(svr.cfgFile, []byte(content), 0o644) - if err != nil { + if err := os.WriteFile(svr.cfgFile, body, 0o644); err != nil { res.Code = 500 res.Msg = fmt.Sprintf("write content to frpc config file error: %v", err) log.Warn("%s", res.Msg) diff --git a/client/control.go b/client/control.go index e344046c439..63c6c331fc9 100644 --- a/client/control.go +++ b/client/control.go @@ -23,11 +23,12 @@ import ( "github.com/fatedier/golib/control/shutdown" "github.com/fatedier/golib/crypto" + "github.com/samber/lo" "github.com/fatedier/frp/client/proxy" "github.com/fatedier/frp/client/visitor" "github.com/fatedier/frp/pkg/auth" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/util/xlog" @@ -43,7 +44,7 @@ type Control struct { runID string // manage all proxies - pxyCfgs map[string]config.ProxyConf + pxyCfgs []v1.ProxyConfigurer pm *proxy.Manager // manage all visitors @@ -69,7 +70,7 @@ type Control struct { lastPong time.Time // The client configuration - clientCfg config.ClientCommonConf + clientCfg *v1.ClientCommonConfig readerShutdown *shutdown.Shutdown writerShutdown *shutdown.Shutdown @@ -83,9 +84,9 @@ type Control struct { func NewControl( ctx context.Context, runID string, conn net.Conn, cm *ConnectionManager, - clientCfg config.ClientCommonConf, - pxyCfgs map[string]config.ProxyConf, - visitorCfgs map[string]config.VisitorConf, + clientCfg *v1.ClientCommonConfig, + pxyCfgs []v1.ProxyConfigurer, + visitorCfgs []v1.VisitorConfigurer, authSetter auth.Setter, ) *Control { // new xlog instance @@ -220,7 +221,7 @@ func (ctl *Control) reader() { defer ctl.readerShutdown.Done() defer close(ctl.closedCh) - encReader := crypto.NewReader(ctl.conn, []byte(ctl.clientCfg.Token)) + encReader := crypto.NewReader(ctl.conn, []byte(ctl.clientCfg.Auth.Token)) for { m, err := msg.ReadMsg(encReader) if err != nil { @@ -240,7 +241,7 @@ func (ctl *Control) reader() { func (ctl *Control) writer() { xl := ctl.xl defer ctl.writerShutdown.Done() - encWriter, err := crypto.NewWriter(ctl.conn, []byte(ctl.clientCfg.Token)) + encWriter, err := crypto.NewWriter(ctl.conn, []byte(ctl.clientCfg.Auth.Token)) if err != nil { xl.Error("crypto new writer error: %v", err) ctl.conn.Close() @@ -274,15 +275,16 @@ func (ctl *Control) msgHandler() { var hbSendCh <-chan time.Time // TODO(fatedier): disable heartbeat if TCPMux is enabled. // Just keep it here to keep compatible with old version frps. - if ctl.clientCfg.HeartbeatInterval > 0 { - hbSend := time.NewTicker(time.Duration(ctl.clientCfg.HeartbeatInterval) * time.Second) + if ctl.clientCfg.Transport.HeartbeatInterval > 0 { + hbSend := time.NewTicker(time.Duration(ctl.clientCfg.Transport.HeartbeatInterval) * time.Second) defer hbSend.Stop() hbSendCh = hbSend.C } var hbCheckCh <-chan time.Time // Check heartbeat timeout only if TCPMux is not enabled and users don't disable heartbeat feature. - if ctl.clientCfg.HeartbeatInterval > 0 && ctl.clientCfg.HeartbeatTimeout > 0 && !ctl.clientCfg.TCPMux { + if ctl.clientCfg.Transport.HeartbeatInterval > 0 && ctl.clientCfg.Transport.HeartbeatTimeout > 0 && + !lo.FromPtr(ctl.clientCfg.Transport.TCPMux) { hbCheck := time.NewTicker(time.Second) defer hbCheck.Stop() hbCheckCh = hbCheck.C @@ -301,7 +303,7 @@ func (ctl *Control) msgHandler() { } ctl.sendCh <- pingMsg case <-hbCheckCh: - if time.Since(ctl.lastPong) > time.Duration(ctl.clientCfg.HeartbeatTimeout)*time.Second { + if time.Since(ctl.lastPong) > time.Duration(ctl.clientCfg.Transport.HeartbeatTimeout)*time.Second { xl.Warn("heartbeat timeout") // let reader() stop ctl.conn.Close() @@ -354,7 +356,7 @@ func (ctl *Control) worker() { ctl.cm.Close() } -func (ctl *Control) ReloadConf(pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]config.VisitorConf) error { +func (ctl *Control) ReloadConf(pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error { ctl.vm.Reload(visitorCfgs) ctl.pm.Reload(pxyCfgs) return nil diff --git a/client/health/health.go b/client/health/health.go index 0ee3d04ac9c..63eec721811 100644 --- a/client/health/health.go +++ b/client/health/health.go @@ -21,8 +21,10 @@ import ( "io" "net" "net/http" + "strings" "time" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/util/xlog" ) @@ -49,26 +51,33 @@ type Monitor struct { cancel context.CancelFunc } -func NewMonitor(ctx context.Context, checkType string, - intervalS int, timeoutS int, maxFailedTimes int, - addr string, url string, +func NewMonitor(ctx context.Context, cfg v1.HealthCheckConfig, addr string, statusNormalFn func(), statusFailedFn func(), ) *Monitor { - if intervalS <= 0 { - intervalS = 10 + if cfg.IntervalSeconds <= 0 { + cfg.IntervalSeconds = 10 } - if timeoutS <= 0 { - timeoutS = 3 + if cfg.TimeoutSeconds <= 0 { + cfg.TimeoutSeconds = 3 } - if maxFailedTimes <= 0 { - maxFailedTimes = 1 + if cfg.MaxFailed <= 0 { + cfg.MaxFailed = 1 } newctx, cancel := context.WithCancel(ctx) + + var url string + if cfg.Type == "http" && cfg.Path != "" { + s := "http://" + addr + if !strings.HasPrefix(cfg.Path, "/") { + s += "/" + } + url = s + cfg.Path + } return &Monitor{ - checkType: checkType, - interval: time.Duration(intervalS) * time.Second, - timeout: time.Duration(timeoutS) * time.Second, - maxFailedTimes: maxFailedTimes, + checkType: cfg.Type, + interval: time.Duration(cfg.IntervalSeconds) * time.Second, + timeout: time.Duration(cfg.TimeoutSeconds) * time.Second, + maxFailedTimes: cfg.MaxFailed, addr: addr, url: url, statusOK: false, diff --git a/client/proxy/general_tcp.go b/client/proxy/general_tcp.go index cea9ef5b32c..c3923085503 100644 --- a/client/proxy/general_tcp.go +++ b/client/proxy/general_tcp.go @@ -17,16 +17,16 @@ package proxy import ( "reflect" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" ) func init() { - pxyConfs := []config.ProxyConf{ - &config.TCPProxyConf{}, - &config.HTTPProxyConf{}, - &config.HTTPSProxyConf{}, - &config.STCPProxyConf{}, - &config.TCPMuxProxyConf{}, + pxyConfs := []v1.ProxyConfigurer{ + &v1.TCPProxyConfig{}, + &v1.HTTPProxyConfig{}, + &v1.HTTPSProxyConfig{}, + &v1.STCPProxyConfig{}, + &v1.TCPMuxProxyConfig{}, } for _, cfg := range pxyConfs { RegisterProxyFactory(reflect.TypeOf(cfg), NewGeneralTCPProxy) @@ -40,7 +40,7 @@ type GeneralTCPProxy struct { *BaseProxy } -func NewGeneralTCPProxy(baseProxy *BaseProxy, _ config.ProxyConf) Proxy { +func NewGeneralTCPProxy(baseProxy *BaseProxy, _ v1.ProxyConfigurer) Proxy { return &GeneralTCPProxy{ BaseProxy: baseProxy, } diff --git a/client/proxy/proxy.go b/client/proxy/proxy.go index 62b3cbba51f..5ba63f94cce 100644 --- a/client/proxy/proxy.go +++ b/client/proxy/proxy.go @@ -15,7 +15,6 @@ package proxy import ( - "bytes" "context" "io" "net" @@ -30,7 +29,8 @@ import ( pp "github.com/pires/go-proxyproto" "golang.org/x/time/rate" - "github.com/fatedier/frp/pkg/config" + "github.com/fatedier/frp/pkg/config/types" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" plugin "github.com/fatedier/frp/pkg/plugin/client" "github.com/fatedier/frp/pkg/transport" @@ -38,9 +38,9 @@ import ( "github.com/fatedier/frp/pkg/util/xlog" ) -var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy, config.ProxyConf) Proxy{} +var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy, v1.ProxyConfigurer) Proxy{} -func RegisterProxyFactory(proxyConfType reflect.Type, factory func(*BaseProxy, config.ProxyConf) Proxy) { +func RegisterProxyFactory(proxyConfType reflect.Type, factory func(*BaseProxy, v1.ProxyConfigurer) Proxy) { proxyFactoryRegistry[proxyConfType] = factory } @@ -56,23 +56,23 @@ type Proxy interface { func NewProxy( ctx context.Context, - pxyConf config.ProxyConf, - clientCfg config.ClientCommonConf, + pxyConf v1.ProxyConfigurer, + clientCfg *v1.ClientCommonConfig, msgTransporter transport.MessageTransporter, ) (pxy Proxy) { var limiter *rate.Limiter - limitBytes := pxyConf.GetBaseConfig().BandwidthLimit.Bytes() - if limitBytes > 0 && pxyConf.GetBaseConfig().BandwidthLimitMode == config.BandwidthLimitModeClient { + limitBytes := pxyConf.GetBaseConfig().Transport.BandwidthLimit.Bytes() + if limitBytes > 0 && pxyConf.GetBaseConfig().Transport.BandwidthLimitMode == types.BandwidthLimitModeClient { limiter = rate.NewLimiter(rate.Limit(float64(limitBytes)), int(limitBytes)) } baseProxy := BaseProxy{ - baseProxyConfig: pxyConf.GetBaseConfig(), - clientCfg: clientCfg, - limiter: limiter, - msgTransporter: msgTransporter, - xl: xlog.FromContextSafe(ctx), - ctx: ctx, + baseCfg: pxyConf.GetBaseConfig(), + clientCfg: clientCfg, + limiter: limiter, + msgTransporter: msgTransporter, + xl: xlog.FromContextSafe(ctx), + ctx: ctx, } factory := proxyFactoryRegistry[reflect.TypeOf(pxyConf)] @@ -83,10 +83,10 @@ func NewProxy( } type BaseProxy struct { - baseProxyConfig *config.BaseProxyConf - clientCfg config.ClientCommonConf - msgTransporter transport.MessageTransporter - limiter *rate.Limiter + baseCfg *v1.ProxyBaseConfig + clientCfg *v1.ClientCommonConfig + msgTransporter transport.MessageTransporter + limiter *rate.Limiter // proxyPlugin is used to handle connections instead of dialing to local service. // It's only validate for TCP protocol now. proxyPlugin plugin.Plugin @@ -97,8 +97,8 @@ type BaseProxy struct { } func (pxy *BaseProxy) Run() error { - if pxy.baseProxyConfig.Plugin != "" { - p, err := plugin.Create(pxy.baseProxyConfig.Plugin, pxy.baseProxyConfig.PluginParams) + if pxy.baseCfg.Plugin.Type != "" { + p, err := plugin.Create(pxy.baseCfg.Plugin.Type, pxy.baseCfg.Plugin.ClientPluginOptions) if err != nil { return err } @@ -114,13 +114,13 @@ func (pxy *BaseProxy) Close() { } func (pxy *BaseProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) { - pxy.HandleTCPWorkConnection(conn, m, []byte(pxy.clientCfg.Token)) + pxy.HandleTCPWorkConnection(conn, m, []byte(pxy.clientCfg.Auth.Token)) } // Common handler for tcp work connections. func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWorkConn, encKey []byte) { xl := pxy.xl - baseConfig := pxy.baseProxyConfig + baseCfg := pxy.baseCfg var ( remote io.ReadWriteCloser err error @@ -133,8 +133,8 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor } xl.Trace("handle tcp work connection, use_encryption: %t, use_compression: %t", - baseConfig.UseEncryption, baseConfig.UseCompression) - if baseConfig.UseEncryption { + baseCfg.Transport.UseEncryption, baseCfg.Transport.UseCompression) + if baseCfg.Transport.UseEncryption { remote, err = libio.WithEncryption(remote, encKey) if err != nil { workConn.Close() @@ -143,13 +143,13 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor } } var compressionResourceRecycleFn func() - if baseConfig.UseCompression { + if baseCfg.Transport.UseCompression { remote, compressionResourceRecycleFn = libio.WithCompressionFromPool(remote) } // check if we need to send proxy protocol info - var extraInfo []byte - if baseConfig.ProxyProtocolVersion != "" { + var extraInfo plugin.ExtraInfo + if baseCfg.Transport.ProxyProtocolVersion != "" { if m.SrcAddr != "" && m.SrcPort != 0 { if m.DstAddr == "" { m.DstAddr = "127.0.0.1" @@ -168,43 +168,41 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor h.TransportProtocol = pp.TCPv6 } - if baseConfig.ProxyProtocolVersion == "v1" { + if baseCfg.Transport.ProxyProtocolVersion == "v1" { h.Version = 1 - } else if baseConfig.ProxyProtocolVersion == "v2" { + } else if baseCfg.Transport.ProxyProtocolVersion == "v2" { h.Version = 2 } - buf := bytes.NewBuffer(nil) - _, _ = h.WriteTo(buf) - extraInfo = buf.Bytes() + extraInfo.ProxyProtocolHeader = h } } if pxy.proxyPlugin != nil { // if plugin is set, let plugin handle connection first xl.Debug("handle by plugin: %s", pxy.proxyPlugin.Name()) - pxy.proxyPlugin.Handle(remote, workConn, extraInfo) + pxy.proxyPlugin.Handle(remote, workConn, &extraInfo) xl.Debug("handle by plugin finished") return } localConn, err := libdial.Dial( - net.JoinHostPort(baseConfig.LocalIP, strconv.Itoa(baseConfig.LocalPort)), + net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort)), libdial.WithTimeout(10*time.Second), ) if err != nil { workConn.Close() - xl.Error("connect to local service [%s:%d] error: %v", baseConfig.LocalIP, baseConfig.LocalPort, err) + xl.Error("connect to local service [%s:%d] error: %v", baseCfg.LocalIP, baseCfg.LocalPort, err) return } xl.Debug("join connections, localConn(l[%s] r[%s]) workConn(l[%s] r[%s])", localConn.LocalAddr().String(), localConn.RemoteAddr().String(), workConn.LocalAddr().String(), workConn.RemoteAddr().String()) - if len(extraInfo) > 0 { - if _, err := localConn.Write(extraInfo); err != nil { + if extraInfo.ProxyProtocolHeader != nil { + if _, err := extraInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil { workConn.Close() - xl.Error("write extraInfo to local conn error: %v", err) + xl.Error("write proxy protocol header to local conn error: %v", err) return } } diff --git a/client/proxy/proxy_manager.go b/client/proxy/proxy_manager.go index 9e551ced700..db66cb26397 100644 --- a/client/proxy/proxy_manager.go +++ b/client/proxy/proxy_manager.go @@ -21,8 +21,10 @@ import ( "reflect" "sync" + "github.com/samber/lo" + "github.com/fatedier/frp/client/event" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/util/xlog" @@ -35,14 +37,14 @@ type Manager struct { closed bool mu sync.RWMutex - clientCfg config.ClientCommonConf + clientCfg *v1.ClientCommonConfig ctx context.Context } func NewManager( ctx context.Context, - clientCfg config.ClientCommonConf, + clientCfg *v1.ClientCommonConfig, msgTransporter transport.MessageTransporter, ) *Manager { return &Manager{ @@ -113,15 +115,18 @@ func (pm *Manager) GetAllProxyStatus() []*WorkingStatus { return ps } -func (pm *Manager) Reload(pxyCfgs map[string]config.ProxyConf) { +func (pm *Manager) Reload(pxyCfgs []v1.ProxyConfigurer) { xl := xlog.FromContextSafe(pm.ctx) + pxyCfgsMap := lo.KeyBy(pxyCfgs, func(c v1.ProxyConfigurer) string { + return c.GetBaseConfig().Name + }) pm.mu.Lock() defer pm.mu.Unlock() delPxyNames := make([]string, 0) for name, pxy := range pm.proxies { del := false - cfg, ok := pxyCfgs[name] + cfg, ok := pxyCfgsMap[name] if !ok || !reflect.DeepEqual(pxy.Cfg, cfg) { del = true } @@ -137,7 +142,8 @@ func (pm *Manager) Reload(pxyCfgs map[string]config.ProxyConf) { } addPxyNames := make([]string, 0) - for name, cfg := range pxyCfgs { + for _, cfg := range pxyCfgs { + name := cfg.GetBaseConfig().Name if _, ok := pm.proxies[name]; !ok { pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.HandleEvent, pm.msgTransporter) pm.proxies[name] = pxy diff --git a/client/proxy/proxy_wrapper.go b/client/proxy/proxy_wrapper.go index 913094272bf..346c6d076a8 100644 --- a/client/proxy/proxy_wrapper.go +++ b/client/proxy/proxy_wrapper.go @@ -18,6 +18,7 @@ import ( "context" "fmt" "net" + "strconv" "sync" "sync/atomic" "time" @@ -26,7 +27,7 @@ import ( "github.com/fatedier/frp/client/event" "github.com/fatedier/frp/client/health" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/util/xlog" @@ -48,11 +49,11 @@ var ( ) type WorkingStatus struct { - Name string `json:"name"` - Type string `json:"type"` - Phase string `json:"status"` - Err string `json:"err"` - Cfg config.ProxyConf `json:"cfg"` + Name string `json:"name"` + Type string `json:"type"` + Phase string `json:"status"` + Err string `json:"err"` + Cfg v1.ProxyConfigurer `json:"cfg"` // Got from server. RemoteAddr string `json:"remote_addr"` @@ -86,17 +87,17 @@ type Wrapper struct { func NewWrapper( ctx context.Context, - cfg config.ProxyConf, - clientCfg config.ClientCommonConf, + cfg v1.ProxyConfigurer, + clientCfg *v1.ClientCommonConfig, eventHandler event.Handler, msgTransporter transport.MessageTransporter, ) *Wrapper { baseInfo := cfg.GetBaseConfig() - xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(baseInfo.ProxyName) + xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(baseInfo.Name) pw := &Wrapper{ WorkingStatus: WorkingStatus{ - Name: baseInfo.ProxyName, - Type: baseInfo.ProxyType, + Name: baseInfo.Name, + Type: baseInfo.Type, Phase: ProxyPhaseNew, Cfg: cfg, }, @@ -108,11 +109,11 @@ func NewWrapper( ctx: xlog.NewContext(ctx, xl), } - if baseInfo.HealthCheckType != "" { + if baseInfo.HealthCheck.Type != "" && baseInfo.LocalPort > 0 { pw.health = 1 // means failed - pw.monitor = health.NewMonitor(pw.ctx, baseInfo.HealthCheckType, baseInfo.HealthCheckIntervalS, - baseInfo.HealthCheckTimeoutS, baseInfo.HealthCheckMaxFailed, baseInfo.HealthCheckAddr, - baseInfo.HealthCheckURL, pw.statusNormalCallback, pw.statusFailedCallback) + addr := net.JoinHostPort(baseInfo.LocalIP, strconv.Itoa(baseInfo.LocalPort)) + pw.monitor = health.NewMonitor(pw.ctx, baseInfo.HealthCheck, addr, + pw.statusNormalCallback, pw.statusFailedCallback) xl.Trace("enable health check monitor") } diff --git a/client/proxy/sudp.go b/client/proxy/sudp.go index 347b86a6227..e67a33974f0 100644 --- a/client/proxy/sudp.go +++ b/client/proxy/sudp.go @@ -25,7 +25,7 @@ import ( "github.com/fatedier/golib/errors" libio "github.com/fatedier/golib/io" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/proto/udp" "github.com/fatedier/frp/pkg/util/limit" @@ -33,21 +33,21 @@ import ( ) func init() { - RegisterProxyFactory(reflect.TypeOf(&config.SUDPProxyConf{}), NewSUDPProxy) + RegisterProxyFactory(reflect.TypeOf(&v1.SUDPProxyConfig{}), NewSUDPProxy) } type SUDPProxy struct { *BaseProxy - cfg *config.SUDPProxyConf + cfg *v1.SUDPProxyConfig localAddr *net.UDPAddr closeCh chan struct{} } -func NewSUDPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy { - unwrapped, ok := cfg.(*config.SUDPProxyConf) +func NewSUDPProxy(baseProxy *BaseProxy, cfg v1.ProxyConfigurer) Proxy { + unwrapped, ok := cfg.(*v1.SUDPProxyConfig) if !ok { return nil } @@ -88,15 +88,15 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) { return conn.Close() }) } - if pxy.cfg.UseEncryption { - rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Token)) + if pxy.cfg.Transport.UseEncryption { + rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Auth.Token)) if err != nil { conn.Close() xl.Error("create encryption stream error: %v", err) return } } - if pxy.cfg.UseCompression { + if pxy.cfg.Transport.UseCompression { rwc = libio.WithCompression(rwc) } conn = utilnet.WrapReadWriteCloserToConn(rwc, conn) diff --git a/client/proxy/udp.go b/client/proxy/udp.go index c0edc31256b..0a5cefcc291 100644 --- a/client/proxy/udp.go +++ b/client/proxy/udp.go @@ -24,7 +24,7 @@ import ( "github.com/fatedier/golib/errors" libio "github.com/fatedier/golib/io" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/proto/udp" "github.com/fatedier/frp/pkg/util/limit" @@ -32,13 +32,13 @@ import ( ) func init() { - RegisterProxyFactory(reflect.TypeOf(&config.UDPProxyConf{}), NewUDPProxy) + RegisterProxyFactory(reflect.TypeOf(&v1.UDPProxyConfig{}), NewUDPProxy) } type UDPProxy struct { *BaseProxy - cfg *config.UDPProxyConf + cfg *v1.UDPProxyConfig localAddr *net.UDPAddr readCh chan *msg.UDPPacket @@ -49,8 +49,8 @@ type UDPProxy struct { closed bool } -func NewUDPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy { - unwrapped, ok := cfg.(*config.UDPProxyConf) +func NewUDPProxy(baseProxy *BaseProxy, cfg v1.ProxyConfigurer) Proxy { + unwrapped, ok := cfg.(*v1.UDPProxyConfig) if !ok { return nil } @@ -99,15 +99,15 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) { return conn.Close() }) } - if pxy.cfg.UseEncryption { - rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Token)) + if pxy.cfg.Transport.UseEncryption { + rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Auth.Token)) if err != nil { conn.Close() xl.Error("create encryption stream error: %v", err) return } } - if pxy.cfg.UseCompression { + if pxy.cfg.Transport.UseCompression { rwc = libio.WithCompression(rwc) } conn = utilnet.WrapReadWriteCloserToConn(rwc, conn) diff --git a/client/proxy/xtcp.go b/client/proxy/xtcp.go index ccf390791aa..8271099bb50 100644 --- a/client/proxy/xtcp.go +++ b/client/proxy/xtcp.go @@ -23,7 +23,7 @@ import ( fmux "github.com/hashicorp/yamux" "github.com/quic-go/quic-go" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/nathole" "github.com/fatedier/frp/pkg/transport" @@ -31,17 +31,17 @@ import ( ) func init() { - RegisterProxyFactory(reflect.TypeOf(&config.XTCPProxyConf{}), NewXTCPProxy) + RegisterProxyFactory(reflect.TypeOf(&v1.XTCPProxyConfig{}), NewXTCPProxy) } type XTCPProxy struct { *BaseProxy - cfg *config.XTCPProxyConf + cfg *v1.XTCPProxyConfig } -func NewXTCPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy { - unwrapped, ok := cfg.(*config.XTCPProxyConf) +func NewXTCPProxy(baseProxy *BaseProxy, cfg v1.ProxyConfigurer) Proxy { + unwrapped, ok := cfg.(*v1.XTCPProxyConfig) if !ok { return nil } @@ -75,7 +75,7 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC transactionID := nathole.NewTransactionID() natHoleClientMsg := &msg.NatHoleClient{ TransactionID: transactionID, - ProxyName: pxy.cfg.ProxyName, + ProxyName: pxy.cfg.Name, Sid: natHoleSidMsg.Sid, MappedAddrs: prepareResult.Addrs, AssistedAddrs: prepareResult.AssistedAddrs, @@ -93,7 +93,7 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC natHoleRespMsg.AssistedAddrs, natHoleRespMsg.DetectBehavior) listenConn := prepareResult.ListenConn - newListenConn, raddr, err := nathole.MakeHole(pxy.ctx, listenConn, natHoleRespMsg, []byte(pxy.cfg.Sk)) + newListenConn, raddr, err := nathole.MakeHole(pxy.ctx, listenConn, natHoleRespMsg, []byte(pxy.cfg.Secretkey)) if err != nil { listenConn.Close() xl.Warn("make hole error: %v", err) @@ -154,7 +154,7 @@ func (pxy *XTCPProxy) listenByKCP(listenConn *net.UDPConn, raddr *net.UDPAddr, s xl.Error("accept connection error: %v", err) return } - go pxy.HandleTCPWorkConnection(muxConn, startWorkConnMsg, []byte(pxy.cfg.Sk)) + go pxy.HandleTCPWorkConnection(muxConn, startWorkConnMsg, []byte(pxy.cfg.Secretkey)) } } @@ -170,9 +170,9 @@ func (pxy *XTCPProxy) listenByQUIC(listenConn *net.UDPConn, _ *net.UDPAddr, star tlsConfig.NextProtos = []string{"frp"} quicListener, err := quic.Listen(listenConn, tlsConfig, &quic.Config{ - MaxIdleTimeout: time.Duration(pxy.clientCfg.QUICMaxIdleTimeout) * time.Second, - MaxIncomingStreams: int64(pxy.clientCfg.QUICMaxIncomingStreams), - KeepAlivePeriod: time.Duration(pxy.clientCfg.QUICKeepalivePeriod) * time.Second, + MaxIdleTimeout: time.Duration(pxy.clientCfg.Transport.QUIC.MaxIdleTimeout) * time.Second, + MaxIncomingStreams: int64(pxy.clientCfg.Transport.QUIC.MaxIncomingStreams), + KeepAlivePeriod: time.Duration(pxy.clientCfg.Transport.QUIC.KeepalivePeriod) * time.Second, }, ) if err != nil { @@ -192,6 +192,6 @@ func (pxy *XTCPProxy) listenByQUIC(listenConn *net.UDPConn, _ *net.UDPAddr, star _ = c.CloseWithError(0, "") return } - go pxy.HandleTCPWorkConnection(utilnet.QuicStreamToNetConn(stream, c), startWorkConnMsg, []byte(pxy.cfg.Sk)) + go pxy.HandleTCPWorkConnection(utilnet.QuicStreamToNetConn(stream, c), startWorkConnMsg, []byte(pxy.cfg.Secretkey)) } } diff --git a/client/service.go b/client/service.go index 954045a366f..5ddc23c3f29 100644 --- a/client/service.go +++ b/client/service.go @@ -19,7 +19,6 @@ import ( "crypto/tls" "fmt" "io" - "math/rand" "net" "runtime" "strconv" @@ -32,10 +31,11 @@ import ( libdial "github.com/fatedier/golib/net/dial" fmux "github.com/hashicorp/yamux" quic "github.com/quic-go/quic-go" + "github.com/samber/lo" "github.com/fatedier/frp/assets" "github.com/fatedier/frp/pkg/auth" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/util/log" @@ -47,8 +47,6 @@ import ( func init() { crypto.DefaultSalt = "frp" - // TODO: remove this when we drop support for go1.19 - rand.Seed(time.Now().UnixNano()) } // Service is a client service. @@ -63,9 +61,9 @@ type Service struct { // Sets authentication based on selected method authSetter auth.Setter - cfg config.ClientCommonConf - pxyCfgs map[string]config.ProxyConf - visitorCfgs map[string]config.VisitorConf + cfg *v1.ClientCommonConfig + pxyCfgs []v1.ProxyConfigurer + visitorCfgs []v1.VisitorConfigurer cfgMu sync.RWMutex // The configuration file used to initialize this client, or an empty @@ -81,13 +79,13 @@ type Service struct { } func NewService( - cfg config.ClientCommonConf, - pxyCfgs map[string]config.ProxyConf, - visitorCfgs map[string]config.VisitorConf, + cfg *v1.ClientCommonConfig, + pxyCfgs []v1.ProxyConfigurer, + visitorCfgs []v1.VisitorConfigurer, cfgFile string, ) (svr *Service, err error) { svr = &Service{ - authSetter: auth.NewAuthSetter(cfg.ClientConfig), + authSetter: auth.NewAuthSetter(cfg.Auth), cfg: cfg, cfgFile: cfgFile, pxyCfgs: pxyCfgs, @@ -134,7 +132,7 @@ func (svr *Service) Run(ctx context.Context) error { // if login_fail_exit is true, just exit this program // otherwise sleep a while and try again to connect to server - if svr.cfg.LoginFailExit { + if lo.FromPtr(svr.cfg.LoginFailExit) { return err } util.RandomSleep(5*time.Second, 0.9, 1.1) @@ -151,16 +149,16 @@ func (svr *Service) Run(ctx context.Context) error { go svr.keepControllerWorking() - if svr.cfg.AdminPort != 0 { + if svr.cfg.WebServer.Port != 0 { // Init admin server assets - assets.Load(svr.cfg.AssetsDir) + assets.Load(svr.cfg.WebServer.AssetsDir) - address := net.JoinHostPort(svr.cfg.AdminAddr, strconv.Itoa(svr.cfg.AdminPort)) + address := net.JoinHostPort(svr.cfg.WebServer.Addr, strconv.Itoa(svr.cfg.WebServer.Port)) err := svr.RunAdminServer(address) if err != nil { log.Warn("run admin server error: %v", err) } - log.Info("admin server listen on %s:%d", svr.cfg.AdminAddr, svr.cfg.AdminPort) + log.Info("admin server listen on %s:%d", svr.cfg.WebServer.Addr, svr.cfg.WebServer.Port) } <-svr.ctx.Done() // service context may not be canceled by svr.Close(), we should call it here to release resources @@ -244,7 +242,7 @@ func (svr *Service) keepControllerWorking() { // session: if it's not nil, using tcp mux func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) { xl := xlog.FromContextSafe(svr.ctx) - cm = NewConnectionManager(svr.ctx, &svr.cfg) + cm = NewConnectionManager(svr.ctx, svr.cfg) if err = cm.OpenConnection(); err != nil { return nil, nil, err @@ -264,12 +262,12 @@ func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) { loginMsg := &msg.Login{ Arch: runtime.GOARCH, Os: runtime.GOOS, - PoolCount: svr.cfg.PoolCount, + PoolCount: svr.cfg.Transport.PoolCount, User: svr.cfg.User, Version: version.Full(), Timestamp: time.Now().Unix(), RunID: svr.runID, - Metas: svr.cfg.Metas, + Metas: svr.cfg.Metadatas, } // Add auth @@ -302,7 +300,7 @@ func (svr *Service) login() (conn net.Conn, cm *ConnectionManager, err error) { return } -func (svr *Service) ReloadConf(pxyCfgs map[string]config.ProxyConf, visitorCfgs map[string]config.VisitorConf) error { +func (svr *Service) ReloadConf(pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error { svr.cfgMu.Lock() svr.pxyCfgs = pxyCfgs svr.visitorCfgs = visitorCfgs @@ -339,13 +337,13 @@ func (svr *Service) GracefulClose(d time.Duration) { type ConnectionManager struct { ctx context.Context - cfg *config.ClientCommonConf + cfg *v1.ClientCommonConfig muxSession *fmux.Session quicConn quic.Connection } -func NewConnectionManager(ctx context.Context, cfg *config.ClientCommonConf) *ConnectionManager { +func NewConnectionManager(ctx context.Context, cfg *v1.ClientCommonConfig) *ConnectionManager { return &ConnectionManager{ ctx: ctx, cfg: cfg, @@ -356,18 +354,18 @@ func (cm *ConnectionManager) OpenConnection() error { xl := xlog.FromContextSafe(cm.ctx) // special for quic - if strings.EqualFold(cm.cfg.Protocol, "quic") { + if strings.EqualFold(cm.cfg.Transport.Protocol, "quic") { var tlsConfig *tls.Config var err error - sn := cm.cfg.TLSServerName + sn := cm.cfg.Transport.TLS.ServerName if sn == "" { sn = cm.cfg.ServerAddr } - if cm.cfg.TLSEnable { + if lo.FromPtr(cm.cfg.Transport.TLS.Enable) { tlsConfig, err = transport.NewClientTLSConfig( - cm.cfg.TLSCertFile, - cm.cfg.TLSKeyFile, - cm.cfg.TLSTrustedCaFile, + cm.cfg.Transport.TLS.CertFile, + cm.cfg.Transport.TLS.KeyFile, + cm.cfg.Transport.TLS.TrustedCaFile, sn) } else { tlsConfig, err = transport.NewClientTLSConfig("", "", "", sn) @@ -382,9 +380,9 @@ func (cm *ConnectionManager) OpenConnection() error { cm.ctx, net.JoinHostPort(cm.cfg.ServerAddr, strconv.Itoa(cm.cfg.ServerPort)), tlsConfig, &quic.Config{ - MaxIdleTimeout: time.Duration(cm.cfg.QUICMaxIdleTimeout) * time.Second, - MaxIncomingStreams: int64(cm.cfg.QUICMaxIncomingStreams), - KeepAlivePeriod: time.Duration(cm.cfg.QUICKeepalivePeriod) * time.Second, + MaxIdleTimeout: time.Duration(cm.cfg.Transport.QUIC.MaxIdleTimeout) * time.Second, + MaxIncomingStreams: int64(cm.cfg.Transport.QUIC.MaxIncomingStreams), + KeepAlivePeriod: time.Duration(cm.cfg.Transport.QUIC.KeepalivePeriod) * time.Second, }) if err != nil { return err @@ -393,7 +391,7 @@ func (cm *ConnectionManager) OpenConnection() error { return nil } - if !cm.cfg.TCPMux { + if !lo.FromPtr(cm.cfg.Transport.TCPMux) { return nil } @@ -403,7 +401,7 @@ func (cm *ConnectionManager) OpenConnection() error { } fmuxCfg := fmux.DefaultConfig() - fmuxCfg.KeepAliveInterval = time.Duration(cm.cfg.TCPMuxKeepaliveInterval) * time.Second + fmuxCfg.KeepAliveInterval = time.Duration(cm.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second fmuxCfg.LogOutput = io.Discard fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024 session, err := fmux.Client(conn, fmuxCfg) @@ -436,20 +434,20 @@ func (cm *ConnectionManager) realConnect() (net.Conn, error) { xl := xlog.FromContextSafe(cm.ctx) var tlsConfig *tls.Config var err error - tlsEnable := cm.cfg.TLSEnable - if cm.cfg.Protocol == "wss" { + tlsEnable := lo.FromPtr(cm.cfg.Transport.TLS.Enable) + if cm.cfg.Transport.Protocol == "wss" { tlsEnable = true } if tlsEnable { - sn := cm.cfg.TLSServerName + sn := cm.cfg.Transport.TLS.ServerName if sn == "" { sn = cm.cfg.ServerAddr } tlsConfig, err = transport.NewClientTLSConfig( - cm.cfg.TLSCertFile, - cm.cfg.TLSKeyFile, - cm.cfg.TLSTrustedCaFile, + cm.cfg.Transport.TLS.CertFile, + cm.cfg.Transport.TLS.KeyFile, + cm.cfg.Transport.TLS.TrustedCaFile, sn) if err != nil { xl.Warn("fail to build tls configuration, err: %v", err) @@ -457,19 +455,19 @@ func (cm *ConnectionManager) realConnect() (net.Conn, error) { } } - proxyType, addr, auth, err := libdial.ParseProxyURL(cm.cfg.HTTPProxy) + proxyType, addr, auth, err := libdial.ParseProxyURL(cm.cfg.Transport.ProxyURL) if err != nil { xl.Error("fail to parse proxy url") return nil, err } dialOptions := []libdial.DialOption{} - protocol := cm.cfg.Protocol + protocol := cm.cfg.Transport.Protocol switch protocol { case "websocket": protocol = "tcp" dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{Hook: utilnet.DialHookWebsocket(protocol, "")})) dialOptions = append(dialOptions, libdial.WithAfterHook(libdial.AfterHook{ - Hook: utilnet.DialHookCustomTLSHeadByte(tlsConfig != nil, cm.cfg.DisableCustomTLSFirstByte), + Hook: utilnet.DialHookCustomTLSHeadByte(tlsConfig != nil, lo.FromPtr(cm.cfg.Transport.TLS.DisableCustomTLSFirstByte)), })) dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig)) case "wss": @@ -481,13 +479,13 @@ func (cm *ConnectionManager) realConnect() (net.Conn, error) { dialOptions = append(dialOptions, libdial.WithTLSConfig(tlsConfig)) } - if cm.cfg.ConnectServerLocalIP != "" { - dialOptions = append(dialOptions, libdial.WithLocalAddr(cm.cfg.ConnectServerLocalIP)) + if cm.cfg.Transport.ConnectServerLocalIP != "" { + dialOptions = append(dialOptions, libdial.WithLocalAddr(cm.cfg.Transport.ConnectServerLocalIP)) } dialOptions = append(dialOptions, libdial.WithProtocol(protocol), - libdial.WithTimeout(time.Duration(cm.cfg.DialServerTimeout)*time.Second), - libdial.WithKeepAlive(time.Duration(cm.cfg.DialServerKeepAlive)*time.Second), + libdial.WithTimeout(time.Duration(cm.cfg.Transport.DialServerTimeout)*time.Second), + libdial.WithKeepAlive(time.Duration(cm.cfg.Transport.DialServerKeepAlive)*time.Second), libdial.WithProxy(proxyType, addr), libdial.WithProxyAuth(auth), ) diff --git a/client/visitor/stcp.go b/client/visitor/stcp.go index 56d55055bf7..58433879492 100644 --- a/client/visitor/stcp.go +++ b/client/visitor/stcp.go @@ -22,7 +22,7 @@ import ( libio "github.com/fatedier/golib/io" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/xlog" @@ -31,7 +31,7 @@ import ( type STCPVisitor struct { *BaseVisitor - cfg *config.STCPVisitorConf + cfg *v1.STCPVisitorConfig } func (sv *STCPVisitor) Run() (err error) { @@ -90,10 +90,10 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) { newVisitorConnMsg := &msg.NewVisitorConn{ RunID: sv.helper.RunID(), ProxyName: sv.cfg.ServerName, - SignKey: util.GetAuthKey(sv.cfg.Sk, now), + SignKey: util.GetAuthKey(sv.cfg.SecretKey, now), Timestamp: now, - UseEncryption: sv.cfg.UseEncryption, - UseCompression: sv.cfg.UseCompression, + UseEncryption: sv.cfg.Transport.UseEncryption, + UseCompression: sv.cfg.Transport.UseCompression, } err = msg.WriteMsg(visitorConn, newVisitorConnMsg) if err != nil { @@ -117,15 +117,15 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) { var remote io.ReadWriteCloser remote = visitorConn - if sv.cfg.UseEncryption { - remote, err = libio.WithEncryption(remote, []byte(sv.cfg.Sk)) + if sv.cfg.Transport.UseEncryption { + remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey)) if err != nil { xl.Error("create encryption stream error: %v", err) return } } - if sv.cfg.UseCompression { + if sv.cfg.Transport.UseCompression { var recycleFn func() remote, recycleFn = libio.WithCompressionFromPool(remote) defer recycleFn() diff --git a/client/visitor/sudp.go b/client/visitor/sudp.go index 5b2d5177937..159f46ee074 100644 --- a/client/visitor/sudp.go +++ b/client/visitor/sudp.go @@ -25,7 +25,7 @@ import ( "github.com/fatedier/golib/errors" libio "github.com/fatedier/golib/io" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/proto/udp" utilnet "github.com/fatedier/frp/pkg/util/net" @@ -42,7 +42,7 @@ type SUDPVisitor struct { readCh chan *msg.UDPPacket sendCh chan *msg.UDPPacket - cfg *config.SUDPVisitorConf + cfg *v1.SUDPVisitorConfig } // SUDP Run start listen a udp port @@ -208,10 +208,10 @@ func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) { newVisitorConnMsg := &msg.NewVisitorConn{ RunID: sv.helper.RunID(), ProxyName: sv.cfg.ServerName, - SignKey: util.GetAuthKey(sv.cfg.Sk, now), + SignKey: util.GetAuthKey(sv.cfg.SecretKey, now), Timestamp: now, - UseEncryption: sv.cfg.UseEncryption, - UseCompression: sv.cfg.UseCompression, + UseEncryption: sv.cfg.Transport.UseEncryption, + UseCompression: sv.cfg.Transport.UseCompression, } err = msg.WriteMsg(visitorConn, newVisitorConnMsg) if err != nil { @@ -232,14 +232,14 @@ func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) { var remote io.ReadWriteCloser remote = visitorConn - if sv.cfg.UseEncryption { - remote, err = libio.WithEncryption(remote, []byte(sv.cfg.Sk)) + if sv.cfg.Transport.UseEncryption { + remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey)) if err != nil { xl.Error("create encryption stream error: %v", err) return nil, err } } - if sv.cfg.UseCompression { + if sv.cfg.Transport.UseCompression { remote = libio.WithCompression(remote) } return utilnet.WrapReadWriteCloserToConn(remote, visitorConn), nil diff --git a/client/visitor/visitor.go b/client/visitor/visitor.go index 7020df63997..dcd1f7b3047 100644 --- a/client/visitor/visitor.go +++ b/client/visitor/visitor.go @@ -19,7 +19,7 @@ import ( "net" "sync" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/transport" utilnet "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/xlog" @@ -47,11 +47,11 @@ type Visitor interface { func NewVisitor( ctx context.Context, - cfg config.VisitorConf, - clientCfg config.ClientCommonConf, + cfg v1.VisitorConfigurer, + clientCfg *v1.ClientCommonConfig, helper Helper, ) (visitor Visitor) { - xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(cfg.GetBaseConfig().ProxyName) + xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(cfg.GetBaseConfig().Name) baseVisitor := BaseVisitor{ clientCfg: clientCfg, helper: helper, @@ -59,18 +59,18 @@ func NewVisitor( internalLn: utilnet.NewInternalListener(), } switch cfg := cfg.(type) { - case *config.STCPVisitorConf: + case *v1.STCPVisitorConfig: visitor = &STCPVisitor{ BaseVisitor: &baseVisitor, cfg: cfg, } - case *config.XTCPVisitorConf: + case *v1.XTCPVisitorConfig: visitor = &XTCPVisitor{ BaseVisitor: &baseVisitor, cfg: cfg, startTunnelCh: make(chan struct{}), } - case *config.SUDPVisitorConf: + case *v1.SUDPVisitorConfig: visitor = &SUDPVisitor{ BaseVisitor: &baseVisitor, cfg: cfg, @@ -81,7 +81,7 @@ func NewVisitor( } type BaseVisitor struct { - clientCfg config.ClientCommonConf + clientCfg *v1.ClientCommonConfig helper Helper l net.Listener internalLn *utilnet.InternalListener diff --git a/client/visitor/visitor_manager.go b/client/visitor/visitor_manager.go index 344101aeb64..4b235cdb113 100644 --- a/client/visitor/visitor_manager.go +++ b/client/visitor/visitor_manager.go @@ -22,14 +22,16 @@ import ( "sync" "time" - "github.com/fatedier/frp/pkg/config" + "github.com/samber/lo" + + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/pkg/util/xlog" ) type Manager struct { - clientCfg config.ClientCommonConf - cfgs map[string]config.VisitorConf + clientCfg *v1.ClientCommonConfig + cfgs map[string]v1.VisitorConfigurer visitors map[string]Visitor helper Helper @@ -44,13 +46,13 @@ type Manager struct { func NewManager( ctx context.Context, runID string, - clientCfg config.ClientCommonConf, + clientCfg *v1.ClientCommonConfig, connectServer func() (net.Conn, error), msgTransporter transport.MessageTransporter, ) *Manager { m := &Manager{ clientCfg: clientCfg, - cfgs: make(map[string]config.VisitorConf), + cfgs: make(map[string]v1.VisitorConfigurer), visitors: make(map[string]Visitor), checkInterval: 10 * time.Second, ctx: ctx, @@ -79,7 +81,7 @@ func (vm *Manager) Run() { case <-ticker.C: vm.mu.Lock() for _, cfg := range vm.cfgs { - name := cfg.GetBaseConfig().ProxyName + name := cfg.GetBaseConfig().Name if _, exist := vm.visitors[name]; !exist { xl.Info("try to start visitor [%s]", name) _ = vm.startVisitor(cfg) @@ -104,9 +106,9 @@ func (vm *Manager) Close() { } // Hold lock before calling this function. -func (vm *Manager) startVisitor(cfg config.VisitorConf) (err error) { +func (vm *Manager) startVisitor(cfg v1.VisitorConfigurer) (err error) { xl := xlog.FromContextSafe(vm.ctx) - name := cfg.GetBaseConfig().ProxyName + name := cfg.GetBaseConfig().Name visitor := NewVisitor(vm.ctx, cfg, vm.clientCfg, vm.helper) err = visitor.Run() if err != nil { @@ -118,15 +120,18 @@ func (vm *Manager) startVisitor(cfg config.VisitorConf) (err error) { return } -func (vm *Manager) Reload(cfgs map[string]config.VisitorConf) { +func (vm *Manager) Reload(cfgs []v1.VisitorConfigurer) { xl := xlog.FromContextSafe(vm.ctx) + cfgsMap := lo.KeyBy(cfgs, func(c v1.VisitorConfigurer) string { + return c.GetBaseConfig().Name + }) vm.mu.Lock() defer vm.mu.Unlock() delNames := make([]string, 0) for name, oldCfg := range vm.cfgs { del := false - cfg, ok := cfgs[name] + cfg, ok := cfgsMap[name] if !ok || !reflect.DeepEqual(oldCfg, cfg) { del = true } @@ -145,7 +150,8 @@ func (vm *Manager) Reload(cfgs map[string]config.VisitorConf) { } addNames := make([]string, 0) - for name, cfg := range cfgs { + for _, cfg := range cfgs { + name := cfg.GetBaseConfig().Name if _, ok := vm.cfgs[name]; !ok { vm.cfgs[name] = cfg addNames = append(addNames, name) diff --git a/client/visitor/xtcp.go b/client/visitor/xtcp.go index dc5b807f768..c180621c29e 100644 --- a/client/visitor/xtcp.go +++ b/client/visitor/xtcp.go @@ -29,7 +29,7 @@ import ( quic "github.com/quic-go/quic-go" "golang.org/x/time/rate" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/nathole" "github.com/fatedier/frp/pkg/transport" @@ -47,7 +47,7 @@ type XTCPVisitor struct { retryLimiter *rate.Limiter cancel context.CancelFunc - cfg *config.XTCPVisitorConf + cfg *v1.XTCPVisitorConfig } func (sv *XTCPVisitor) Run() (err error) { @@ -56,7 +56,7 @@ func (sv *XTCPVisitor) Run() (err error) { if sv.cfg.Protocol == "kcp" { sv.session = NewKCPTunnelSession() } else { - sv.session = NewQUICTunnelSession(&sv.clientCfg) + sv.session = NewQUICTunnelSession(sv.clientCfg) } if sv.cfg.BindPort > 0 { @@ -192,14 +192,14 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) { } var muxConnRWCloser io.ReadWriteCloser = tunnelConn - if sv.cfg.UseEncryption { - muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.Sk)) + if sv.cfg.Transport.UseEncryption { + muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey)) if err != nil { xl.Error("create encryption stream error: %v", err) return } } - if sv.cfg.UseCompression { + if sv.cfg.Transport.UseCompression { var recycleFn func() muxConnRWCloser, recycleFn = libio.WithCompressionFromPool(muxConnRWCloser) defer recycleFn() @@ -292,7 +292,7 @@ func (sv *XTCPVisitor) makeNatHole() { TransactionID: transactionID, ProxyName: sv.cfg.ServerName, Protocol: sv.cfg.Protocol, - SignKey: util.GetAuthKey(sv.cfg.Sk, now), + SignKey: util.GetAuthKey(sv.cfg.SecretKey, now), Timestamp: now, MappedAddrs: prepareResult.Addrs, AssistedAddrs: prepareResult.AssistedAddrs, @@ -310,7 +310,7 @@ func (sv *XTCPVisitor) makeNatHole() { natHoleRespMsg.Sid, natHoleRespMsg.Protocol, natHoleRespMsg.CandidateAddrs, natHoleRespMsg.AssistedAddrs, natHoleRespMsg.DetectBehavior) - newListenConn, raddr, err := nathole.MakeHole(sv.ctx, listenConn, natHoleRespMsg, []byte(sv.cfg.Sk)) + newListenConn, raddr, err := nathole.MakeHole(sv.ctx, listenConn, natHoleRespMsg, []byte(sv.cfg.SecretKey)) if err != nil { listenConn.Close() xl.Warn("make hole error: %v", err) @@ -398,10 +398,10 @@ type QUICTunnelSession struct { listenConn *net.UDPConn mu sync.RWMutex - clientCfg *config.ClientCommonConf + clientCfg *v1.ClientCommonConfig } -func NewQUICTunnelSession(clientCfg *config.ClientCommonConf) TunnelSession { +func NewQUICTunnelSession(clientCfg *v1.ClientCommonConfig) TunnelSession { return &QUICTunnelSession{ clientCfg: clientCfg, } @@ -415,9 +415,9 @@ func (qs *QUICTunnelSession) Init(listenConn *net.UDPConn, raddr *net.UDPAddr) e tlsConfig.NextProtos = []string{"frp"} quicConn, err := quic.Dial(context.Background(), listenConn, raddr, tlsConfig, &quic.Config{ - MaxIdleTimeout: time.Duration(qs.clientCfg.QUICMaxIdleTimeout) * time.Second, - MaxIncomingStreams: int64(qs.clientCfg.QUICMaxIncomingStreams), - KeepAlivePeriod: time.Duration(qs.clientCfg.QUICKeepalivePeriod) * time.Second, + MaxIdleTimeout: time.Duration(qs.clientCfg.Transport.QUIC.MaxIdleTimeout) * time.Second, + MaxIncomingStreams: int64(qs.clientCfg.Transport.QUIC.MaxIncomingStreams), + KeepAlivePeriod: time.Duration(qs.clientCfg.Transport.QUIC.KeepalivePeriod) * time.Second, }) if err != nil { return fmt.Errorf("dial quic error: %v", err) diff --git a/cmd/frpc/sub/admin.go b/cmd/frpc/sub/admin.go new file mode 100644 index 00000000000..2a5f2830a15 --- /dev/null +++ b/cmd/frpc/sub/admin.go @@ -0,0 +1,117 @@ +// Copyright 2023 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 sub + +import ( + "fmt" + "os" + "strings" + + "github.com/rodaine/table" + "github.com/spf13/cobra" + + "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" + clientsdk "github.com/fatedier/frp/pkg/sdk/client" +) + +func init() { + rootCmd.AddCommand(NewAdminCommand( + "reload", + "Hot-Reload frpc configuration", + ReloadHandler, + )) + + rootCmd.AddCommand(NewAdminCommand( + "status", + "Overview of all proxies status", + StatusHandler, + )) + + rootCmd.AddCommand(NewAdminCommand( + "stop", + "Stop the running frpc", + StopHandler, + )) +} + +func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) error) *cobra.Command { + return &cobra.Command{ + Use: name, + Short: short, + Run: func(cmd *cobra.Command, args []string) { + cfg, _, _, _, err := config.LoadClientConfig(cfgFile) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + if cfg.WebServer.Port <= 0 { + fmt.Println("web server port should be set if you want to use this feature") + os.Exit(1) + } + + if err := handler(cfg); err != nil { + fmt.Println(err) + os.Exit(1) + } + }, + } +} + +func ReloadHandler(clientCfg *v1.ClientCommonConfig) error { + client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port) + client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password) + if err := client.Reload(); err != nil { + return err + } + fmt.Println("reload success") + return nil +} + +func StatusHandler(clientCfg *v1.ClientCommonConfig) error { + client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port) + client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password) + res, err := client.GetAllProxyStatus() + if err != nil { + return err + } + + fmt.Printf("Proxy Status...\n\n") + for _, typ := range proxyTypes { + arrs := res[string(typ)] + if len(arrs) == 0 { + continue + } + + fmt.Println(strings.ToUpper(string(typ))) + tbl := table.New("Name", "Status", "LocalAddr", "Plugin", "RemoteAddr", "Error") + for _, ps := range arrs { + tbl.AddRow(ps.Name, ps.Status, ps.LocalAddr, ps.Plugin, ps.RemoteAddr, ps.Err) + } + tbl.Print() + fmt.Println("") + } + return nil +} + +func StopHandler(clientCfg *v1.ClientCommonConfig) error { + client := clientsdk.New(clientCfg.WebServer.Addr, clientCfg.WebServer.Port) + client.SetAuth(clientCfg.WebServer.User, clientCfg.WebServer.Password) + if err := client.Stop(); err != nil { + return err + } + fmt.Println("stop success") + return nil +} diff --git a/cmd/frpc/sub/flags.go b/cmd/frpc/sub/flags.go new file mode 100644 index 00000000000..eb3cc010629 --- /dev/null +++ b/cmd/frpc/sub/flags.go @@ -0,0 +1,125 @@ +// Copyright 2023 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 sub + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/fatedier/frp/pkg/config/types" + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/config/v1/validation" +) + +type BandwidthQuantityFlag struct { + V *types.BandwidthQuantity +} + +func (f *BandwidthQuantityFlag) Set(s string) error { + return f.V.UnmarshalString(s) +} + +func (f *BandwidthQuantityFlag) String() string { + return f.V.String() +} + +func (f *BandwidthQuantityFlag) Type() string { + return "string" +} + +func RegisterProxyFlags(cmd *cobra.Command, c v1.ProxyConfigurer) { + registerProxyBaseConfigFlags(cmd, c.GetBaseConfig()) + switch cc := c.(type) { + case *v1.TCPProxyConfig: + cmd.Flags().IntVarP(&cc.RemotePort, "remote_port", "r", 0, "remote port") + case *v1.UDPProxyConfig: + cmd.Flags().IntVarP(&cc.RemotePort, "remote_port", "r", 0, "remote port") + case *v1.HTTPProxyConfig: + registerProxyDomainConfigFlags(cmd, &cc.DomainConfig) + cmd.Flags().StringSliceVarP(&cc.Locations, "locations", "", []string{}, "locations") + cmd.Flags().StringVarP(&cc.HTTPUser, "http_user", "", "", "http auth user") + cmd.Flags().StringVarP(&cc.HTTPPassword, "http_pwd", "", "", "http auth password") + cmd.Flags().StringVarP(&cc.HostHeaderRewrite, "host_header_rewrite", "", "", "host header rewrite") + case *v1.HTTPSProxyConfig: + registerProxyDomainConfigFlags(cmd, &cc.DomainConfig) + case *v1.TCPMuxProxyConfig: + registerProxyDomainConfigFlags(cmd, &cc.DomainConfig) + cmd.Flags().StringVarP(&cc.Multiplexer, "mux", "", "", "multiplexer") + case *v1.STCPProxyConfig: + cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key") + case *v1.SUDPProxyConfig: + cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key") + case *v1.XTCPProxyConfig: + cmd.Flags().StringVarP(&cc.Secretkey, "sk", "", "", "secret key") + } +} + +func registerProxyBaseConfigFlags(cmd *cobra.Command, c *v1.ProxyBaseConfig) { + if c == nil { + return + } + cmd.Flags().StringVarP(&c.Name, "proxy_name", "n", "", "proxy name") + cmd.Flags().StringVarP(&c.LocalIP, "local_ip", "i", "127.0.0.1", "local ip") + cmd.Flags().IntVarP(&c.LocalPort, "local_port", "l", 0, "local port") + cmd.Flags().BoolVarP(&c.Transport.UseEncryption, "ue", "", false, "use encryption") + cmd.Flags().BoolVarP(&c.Transport.UseCompression, "uc", "", false, "use compression") + cmd.Flags().StringVarP(&c.Transport.BandwidthLimitMode, "bandwidth_limit_mode", "", types.BandwidthLimitModeClient, "bandwidth limit mode") + cmd.Flags().VarP(&BandwidthQuantityFlag{V: &c.Transport.BandwidthLimit}, "bandwidth_limit", "", "bandwidth limit (e.g. 100KB or 1MB)") +} + +func registerProxyDomainConfigFlags(cmd *cobra.Command, c *v1.DomainConfig) { + if c == nil { + return + } + cmd.Flags().StringSliceVarP(&c.CustomDomains, "custom_domain", "d", []string{}, "custom domains") + cmd.Flags().StringVarP(&c.SubDomain, "sd", "", "", "sub domain") +} + +func RegisterVisitorFlags(cmd *cobra.Command, c v1.VisitorConfigurer) { + registerVisitorBaseConfigFlags(cmd, c.GetBaseConfig()) + + // add visitor flags if exist +} + +func registerVisitorBaseConfigFlags(cmd *cobra.Command, c *v1.VisitorBaseConfig) { + if c == nil { + return + } + cmd.Flags().StringVarP(&c.Name, "visitor_name", "n", "", "visitor name") + cmd.Flags().BoolVarP(&c.Transport.UseEncryption, "ue", "", false, "use encryption") + cmd.Flags().BoolVarP(&c.Transport.UseCompression, "uc", "", false, "use compression") + cmd.Flags().StringVarP(&c.SecretKey, "sk", "", "", "secret key") + cmd.Flags().StringVarP(&c.ServerName, "server_name", "", "", "server name") + cmd.Flags().StringVarP(&c.BindAddr, "bind_addr", "", "", "bind addr") + cmd.Flags().IntVarP(&c.BindPort, "bind_port", "", 0, "bind port") +} + +func RegisterClientCommonConfigFlags(cmd *cobra.Command, c *v1.ClientCommonConfig) { + cmd.PersistentFlags().StringVarP(&c.ServerAddr, "server_addr", "s", "127.0.0.1", "frp server's address") + cmd.PersistentFlags().IntVarP(&c.ServerPort, "server_port", "P", 7000, "frp server's port") + cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user") + cmd.PersistentFlags().StringVarP(&c.Transport.Protocol, "protocol", "p", "tcp", + fmt.Sprintf("optional values are %v", validation.SupportedTransportProtocols)) + cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token") + cmd.PersistentFlags().StringVarP(&c.Log.Level, "log_level", "", "info", "log level") + cmd.PersistentFlags().StringVarP(&c.Log.To, "log_file", "", "console", "console or file path") + cmd.PersistentFlags().Int64VarP(&c.Log.MaxDays, "log_max_days", "", 3, "log file reversed days") + cmd.PersistentFlags().BoolVarP(&c.Log.DisablePrintColor, "disable_log_color", "", false, "disable log color in console") + cmd.PersistentFlags().StringVarP(&c.Transport.TLS.ServerName, "tls_server_name", "", "", "specify the custom server name of tls certificate") + cmd.PersistentFlags().StringVarP(&c.DNSServer, "dns_server", "", "", "specify dns server instead of using system default one") + + c.Transport.TLS.Enable = cmd.PersistentFlags().BoolP("tls_enable", "", true, "enable frpc tls") +} diff --git a/cmd/frpc/sub/http.go b/cmd/frpc/sub/http.go deleted file mode 100644 index 1d152585360..00000000000 --- a/cmd/frpc/sub/http.go +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2018 fatedier, fatedier@gmail.com -// -// 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 sub - -import ( - "fmt" - "os" - "strings" - - "github.com/spf13/cobra" - - "github.com/fatedier/frp/pkg/config" - "github.com/fatedier/frp/pkg/consts" -) - -func init() { - RegisterCommonFlags(httpCmd) - - httpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name") - httpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip") - httpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port") - httpCmd.PersistentFlags().StringVarP(&customDomains, "custom_domain", "d", "", "custom domain") - httpCmd.PersistentFlags().StringVarP(&subDomain, "sd", "", "", "sub domain") - httpCmd.PersistentFlags().StringVarP(&locations, "locations", "", "", "locations") - httpCmd.PersistentFlags().StringVarP(&httpUser, "http_user", "", "", "http auth user") - httpCmd.PersistentFlags().StringVarP(&httpPwd, "http_pwd", "", "", "http auth password") - httpCmd.PersistentFlags().StringVarP(&hostHeaderRewrite, "host_header_rewrite", "", "", "host header rewrite") - httpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") - httpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") - httpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit") - httpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode") - - rootCmd.AddCommand(httpCmd) -} - -var httpCmd = &cobra.Command{ - Use: "http", - Short: "Run frpc with a single http proxy", - RunE: func(cmd *cobra.Command, args []string) error { - clientCfg, err := parseClientCommonCfgFromCmd() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - cfg := &config.HTTPProxyConf{} - var prefix string - if user != "" { - prefix = user + "." - } - cfg.ProxyName = prefix + proxyName - cfg.ProxyType = consts.HTTPProxy - cfg.LocalIP = localIP - cfg.LocalPort = localPort - cfg.CustomDomains = strings.Split(customDomains, ",") - cfg.SubDomain = subDomain - cfg.Locations = strings.Split(locations, ",") - cfg.HTTPUser = httpUser - cfg.HTTPPwd = httpPwd - cfg.HostHeaderRewrite = hostHeaderRewrite - cfg.UseEncryption = useEncryption - cfg.UseCompression = useCompression - cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - cfg.BandwidthLimitMode = bandwidthLimitMode - - err = cfg.ValidateForClient() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - proxyConfs := map[string]config.ProxyConf{ - cfg.ProxyName: cfg, - } - err = startService(clientCfg, proxyConfs, nil, "") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - return nil - }, -} diff --git a/cmd/frpc/sub/https.go b/cmd/frpc/sub/https.go deleted file mode 100644 index 2eb6ed6b40a..00000000000 --- a/cmd/frpc/sub/https.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2018 fatedier, fatedier@gmail.com -// -// 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 sub - -import ( - "fmt" - "os" - "strings" - - "github.com/spf13/cobra" - - "github.com/fatedier/frp/pkg/config" - "github.com/fatedier/frp/pkg/consts" -) - -func init() { - RegisterCommonFlags(httpsCmd) - - httpsCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name") - httpsCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip") - httpsCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port") - httpsCmd.PersistentFlags().StringVarP(&customDomains, "custom_domain", "d", "", "custom domain") - httpsCmd.PersistentFlags().StringVarP(&subDomain, "sd", "", "", "sub domain") - httpsCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") - httpsCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") - httpsCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit") - httpsCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode") - - rootCmd.AddCommand(httpsCmd) -} - -var httpsCmd = &cobra.Command{ - Use: "https", - Short: "Run frpc with a single https proxy", - RunE: func(cmd *cobra.Command, args []string) error { - clientCfg, err := parseClientCommonCfgFromCmd() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - cfg := &config.HTTPSProxyConf{} - var prefix string - if user != "" { - prefix = user + "." - } - cfg.ProxyName = prefix + proxyName - cfg.ProxyType = consts.HTTPSProxy - cfg.LocalIP = localIP - cfg.LocalPort = localPort - cfg.CustomDomains = strings.Split(customDomains, ",") - cfg.SubDomain = subDomain - cfg.UseEncryption = useEncryption - cfg.UseCompression = useCompression - cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - cfg.BandwidthLimitMode = bandwidthLimitMode - - err = cfg.ValidateForClient() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - proxyConfs := map[string]config.ProxyConf{ - cfg.ProxyName: cfg, - } - err = startService(clientCfg, proxyConfs, nil, "") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - return nil - }, -} diff --git a/cmd/frpc/sub/nathole.go b/cmd/frpc/sub/nathole.go index 459f8d63339..72b635f1bf1 100644 --- a/cmd/frpc/sub/nathole.go +++ b/cmd/frpc/sub/nathole.go @@ -21,6 +21,7 @@ import ( "github.com/spf13/cobra" "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/nathole" ) @@ -30,8 +31,6 @@ var ( ) func init() { - RegisterCommonFlags(natholeCmd) - rootCmd.AddCommand(natholeCmd) natholeCmd.AddCommand(natholeDiscoveryCmd) @@ -49,9 +48,9 @@ var natholeDiscoveryCmd = &cobra.Command{ Short: "Discover nathole information from stun server", RunE: func(cmd *cobra.Command, args []string) error { // ignore error here, because we can use command line pameters - cfg, _, _, err := config.ParseClientConfig(cfgFile) + cfg, _, _, _, err := config.LoadClientConfig(cfgFile) if err != nil { - cfg = config.GetDefaultClientConf() + cfg = &v1.ClientCommonConfig{} } if natHoleSTUNServer != "" { cfg.NatHoleSTUNServer = natHoleSTUNServer @@ -89,7 +88,7 @@ var natholeDiscoveryCmd = &cobra.Command{ }, } -func validateForNatHoleDiscovery(cfg config.ClientCommonConf) error { +func validateForNatHoleDiscovery(cfg *v1.ClientCommonConfig) error { if cfg.NatHoleSTUNServer == "" { return fmt.Errorf("nat_hole_stun_server can not be empty") } diff --git a/cmd/frpc/sub/proxy.go b/cmd/frpc/sub/proxy.go new file mode 100644 index 00000000000..7ae8d353b39 --- /dev/null +++ b/cmd/frpc/sub/proxy.go @@ -0,0 +1,120 @@ +// Copyright 2023 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 sub + +import ( + "fmt" + "os" + + "github.com/samber/lo" + "github.com/spf13/cobra" + + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/config/v1/validation" +) + +var proxyTypes = []v1.ProxyType{ + v1.ProxyTypeTCP, + v1.ProxyTypeUDP, + v1.ProxyTypeTCPMUX, + v1.ProxyTypeHTTP, + v1.ProxyTypeHTTPS, + v1.ProxyTypeSTCP, + v1.ProxyTypeSUDP, + v1.ProxyTypeXTCP, +} + +var visitorTypes = []v1.VisitorType{ + v1.VisitorTypeSTCP, + v1.VisitorTypeSUDP, + v1.VisitorTypeXTCP, +} + +func init() { + for _, typ := range proxyTypes { + c := v1.NewProxyConfigurerByType(typ) + if c == nil { + panic("proxy type: " + typ + " not support") + } + clientCfg := v1.ClientCommonConfig{} + cmd := NewProxyCommand(string(typ), c, &clientCfg) + RegisterClientCommonConfigFlags(cmd, &clientCfg) + RegisterProxyFlags(cmd, c) + + // add sub command for visitor + if lo.Contains(visitorTypes, v1.VisitorType(typ)) { + vc := v1.NewVisitorConfigurerByType(v1.VisitorType(typ)) + if vc == nil { + panic("visitor type: " + typ + " not support") + } + visitorCmd := NewVisitorCommand(string(typ), vc, &clientCfg) + RegisterVisitorFlags(visitorCmd, vc) + cmd.AddCommand(visitorCmd) + } + rootCmd.AddCommand(cmd) + } +} + +func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientCommonConfig) *cobra.Command { + return &cobra.Command{ + Use: name, + Short: fmt.Sprintf("Run frpc with a single %s proxy", name), + Run: func(cmd *cobra.Command, args []string) { + clientCfg.Complete() + if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil { + fmt.Println(err) + os.Exit(1) + } + + c.Complete(clientCfg.User) + c.GetBaseConfig().Type = name + if err := validation.ValidateProxyConfigurerForClient(c); err != nil { + fmt.Println(err) + os.Exit(1) + } + err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, "") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + }, + } +} + +func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.ClientCommonConfig) *cobra.Command { + return &cobra.Command{ + Use: "visitor", + Short: fmt.Sprintf("Run frpc with a single %s visitor", name), + Run: func(cmd *cobra.Command, args []string) { + clientCfg.Complete() + if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil { + fmt.Println(err) + os.Exit(1) + } + + c.Complete(clientCfg) + c.GetBaseConfig().Type = name + if err := validation.ValidateVisitorConfigurer(c); err != nil { + fmt.Println(err) + os.Exit(1) + } + err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, "") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + }, + } +} diff --git a/cmd/frpc/sub/reload.go b/cmd/frpc/sub/reload.go deleted file mode 100644 index 258317e7d6a..00000000000 --- a/cmd/frpc/sub/reload.go +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2018 fatedier, fatedier@gmail.com -// -// 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 sub - -import ( - "encoding/base64" - "fmt" - "io" - "net/http" - "os" - "strings" - - "github.com/spf13/cobra" - - "github.com/fatedier/frp/pkg/config" -) - -func init() { - rootCmd.AddCommand(reloadCmd) -} - -var reloadCmd = &cobra.Command{ - Use: "reload", - Short: "Hot-Reload frpc configuration", - RunE: func(cmd *cobra.Command, args []string) error { - cfg, _, _, err := config.ParseClientConfig(cfgFile) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - err = reload(cfg) - if err != nil { - fmt.Printf("frpc reload error: %v\n", err) - os.Exit(1) - } - fmt.Printf("reload success\n") - return nil - }, -} - -func reload(clientCfg config.ClientCommonConf) error { - if clientCfg.AdminPort == 0 { - return fmt.Errorf("admin_port shoud be set if you want to use reload feature") - } - - req, err := http.NewRequest("GET", "http://"+ - clientCfg.AdminAddr+":"+fmt.Sprintf("%d", clientCfg.AdminPort)+"/api/reload", nil) - if err != nil { - return err - } - - authStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(clientCfg.AdminUser+":"+ - clientCfg.AdminPwd)) - - req.Header.Add("Authorization", authStr) - resp, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode == 200 { - return nil - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - return fmt.Errorf("code [%d], %s", resp.StatusCode, strings.TrimSpace(string(body))) -} diff --git a/cmd/frpc/sub/root.go b/cmd/frpc/sub/root.go index 783514b99d0..915e6b35174 100644 --- a/cmd/frpc/sub/root.go +++ b/cmd/frpc/sub/root.go @@ -18,11 +18,9 @@ import ( "context" "fmt" "io/fs" - "net" "os" "os/signal" "path/filepath" - "strconv" "sync" "syscall" "time" @@ -30,55 +28,17 @@ import ( "github.com/spf13/cobra" "github.com/fatedier/frp/client" - "github.com/fatedier/frp/pkg/auth" "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/config/v1/validation" "github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/pkg/util/version" ) -const ( - CfgFileTypeIni = iota - CfgFileTypeCmd -) - var ( cfgFile string cfgDir string showVersion bool - - serverAddr string - user string - protocol string - token string - logLevel string - logFile string - logMaxDays int - disableLogColor bool - dnsServer string - - proxyName string - localIP string - localPort int - remotePort int - useEncryption bool - useCompression bool - bandwidthLimit string - bandwidthLimitMode string - customDomains string - subDomain string - httpUser string - httpPwd string - locations string - hostHeaderRewrite string - role string - sk string - multiplexer string - serverName string - bindAddr string - bindPort int - - tlsEnable bool - tlsServerName string ) func init() { @@ -87,20 +47,6 @@ func init() { rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc") } -func RegisterCommonFlags(cmd *cobra.Command) { - cmd.PersistentFlags().StringVarP(&serverAddr, "server_addr", "s", "127.0.0.1:7000", "frp server's address") - cmd.PersistentFlags().StringVarP(&user, "user", "u", "", "user") - cmd.PersistentFlags().StringVarP(&protocol, "protocol", "p", "tcp", "tcp, kcp, quic, websocket, wss") - cmd.PersistentFlags().StringVarP(&token, "token", "t", "", "auth token") - cmd.PersistentFlags().StringVarP(&logLevel, "log_level", "", "info", "log level") - cmd.PersistentFlags().StringVarP(&logFile, "log_file", "", "console", "console or file path") - cmd.PersistentFlags().IntVarP(&logMaxDays, "log_max_days", "", 3, "log file reversed days") - cmd.PersistentFlags().BoolVarP(&disableLogColor, "disable_log_color", "", false, "disable log color in console") - cmd.PersistentFlags().BoolVarP(&tlsEnable, "tls_enable", "", true, "enable frpc tls") - cmd.PersistentFlags().StringVarP(&tlsServerName, "tls_server_name", "", "", "specify the custom server name of tls certificate") - cmd.PersistentFlags().StringVarP(&dnsServer, "dns_server", "", "", "specify dns server instead of using system default one") -} - var rootCmd = &cobra.Command{ Use: "frpc", Short: "frpc is the client of frp (https://github.com/fatedier/frp)", @@ -120,6 +66,7 @@ var rootCmd = &cobra.Command{ // Do not show command usage here. err := runClient(cfgFile) if err != nil { + fmt.Println(err) os.Exit(1) } return nil @@ -160,78 +107,49 @@ func handleTermSignal(svr *client.Service) { svr.GracefulClose(500 * time.Millisecond) } -func parseClientCommonCfgFromCmd() (cfg config.ClientCommonConf, err error) { - cfg = config.GetDefaultClientConf() - - ipStr, portStr, err := net.SplitHostPort(serverAddr) +func runClient(cfgFilePath string) error { + cfg, pxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath) if err != nil { - err = fmt.Errorf("invalid server_addr: %v", err) - return + return err } - - cfg.ServerAddr = ipStr - cfg.ServerPort, err = strconv.Atoi(portStr) - if err != nil { - err = fmt.Errorf("invalid server_addr: %v", err) - return + if isLegacyFormat { + fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, " + + "please use yaml/json/toml format instead!\n") } - cfg.User = user - cfg.Protocol = protocol - cfg.LogLevel = logLevel - cfg.LogFile = logFile - cfg.LogMaxDays = int64(logMaxDays) - cfg.DisableLogColor = disableLogColor - cfg.DNSServer = dnsServer - - // Only token authentication is supported in cmd mode - cfg.ClientConfig = auth.GetDefaultClientConf() - cfg.Token = token - cfg.TLSEnable = tlsEnable - cfg.TLSServerName = tlsServerName - - cfg.Complete() - if err = cfg.Validate(); err != nil { - err = fmt.Errorf("parse config error: %v", err) - return + warning, err := validation.ValidateAllClientConfig(cfg, pxyCfgs, visitorCfgs) + if warning != nil { + fmt.Printf("WARNING: %v\n", warning) } - return -} - -func runClient(cfgFilePath string) error { - cfg, pxyCfgs, visitorCfgs, err := config.ParseClientConfig(cfgFilePath) if err != nil { - fmt.Println(err) return err } return startService(cfg, pxyCfgs, visitorCfgs, cfgFilePath) } func startService( - cfg config.ClientCommonConf, - pxyCfgs map[string]config.ProxyConf, - visitorCfgs map[string]config.VisitorConf, + cfg *v1.ClientCommonConfig, + pxyCfgs []v1.ProxyConfigurer, + visitorCfgs []v1.VisitorConfigurer, cfgFile string, -) (err error) { - log.InitLog(cfg.LogWay, cfg.LogFile, cfg.LogLevel, - cfg.LogMaxDays, cfg.DisableLogColor) +) error { + log.InitLog(cfg.Log.To, cfg.Log.Level, cfg.Log.MaxDays, cfg.Log.DisablePrintColor) if cfgFile != "" { log.Info("start frpc service for config file [%s]", cfgFile) defer log.Info("frpc service for config file [%s] stopped", cfgFile) } - svr, errRet := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile) - if errRet != nil { - err = errRet - return + svr, err := client.NewService(cfg, pxyCfgs, visitorCfgs, cfgFile) + if err != nil { + return err } - shouldGracefulClose := cfg.Protocol == "kcp" || cfg.Protocol == "quic" + shouldGracefulClose := cfg.Transport.Protocol == "kcp" || cfg.Transport.Protocol == "quic" // Capture the exit signal if we use kcp or quic. if shouldGracefulClose { go handleTermSignal(svr) } _ = svr.Run(context.Background()) - return + return nil } diff --git a/cmd/frpc/sub/status.go b/cmd/frpc/sub/status.go deleted file mode 100644 index db7471e380a..00000000000 --- a/cmd/frpc/sub/status.go +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2018 fatedier, fatedier@gmail.com -// -// 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 sub - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "io" - "net/http" - "os" - "strings" - - "github.com/rodaine/table" - "github.com/spf13/cobra" - - "github.com/fatedier/frp/client" - "github.com/fatedier/frp/pkg/config" -) - -func init() { - rootCmd.AddCommand(statusCmd) -} - -var statusCmd = &cobra.Command{ - Use: "status", - Short: "Overview of all proxies status", - RunE: func(cmd *cobra.Command, args []string) error { - cfg, _, _, err := config.ParseClientConfig(cfgFile) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - if err = status(cfg); err != nil { - fmt.Printf("frpc get status error: %v\n", err) - os.Exit(1) - } - return nil - }, -} - -func status(clientCfg config.ClientCommonConf) error { - if clientCfg.AdminPort == 0 { - return fmt.Errorf("admin_port shoud be set if you want to get proxy status") - } - - req, err := http.NewRequest("GET", "http://"+ - clientCfg.AdminAddr+":"+fmt.Sprintf("%d", clientCfg.AdminPort)+"/api/status", nil) - if err != nil { - return err - } - - authStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(clientCfg.AdminUser+":"+ - clientCfg.AdminPwd)) - - req.Header.Add("Authorization", authStr) - resp, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return fmt.Errorf("admin api status code [%d]", resp.StatusCode) - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - res := make(client.StatusResp) - err = json.Unmarshal(body, &res) - if err != nil { - return fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(string(body))) - } - - fmt.Println("Proxy Status...") - types := []string{"tcp", "udp", "tcpmux", "http", "https", "stcp", "sudp", "xtcp"} - for _, pxyType := range types { - arrs := res[pxyType] - if len(arrs) == 0 { - continue - } - - fmt.Println(strings.ToUpper(pxyType)) - tbl := table.New("Name", "Status", "LocalAddr", "Plugin", "RemoteAddr", "Error") - for _, ps := range arrs { - tbl.AddRow(ps.Name, ps.Status, ps.LocalAddr, ps.Plugin, ps.RemoteAddr, ps.Err) - } - tbl.Print() - fmt.Println("") - } - return nil -} diff --git a/cmd/frpc/sub/stcp.go b/cmd/frpc/sub/stcp.go deleted file mode 100644 index 24aa955a24c..00000000000 --- a/cmd/frpc/sub/stcp.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2018 fatedier, fatedier@gmail.com -// -// 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 sub - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" - - "github.com/fatedier/frp/pkg/config" - "github.com/fatedier/frp/pkg/consts" -) - -func init() { - RegisterCommonFlags(stcpCmd) - - stcpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name") - stcpCmd.PersistentFlags().StringVarP(&role, "role", "", "server", "role") - stcpCmd.PersistentFlags().StringVarP(&sk, "sk", "", "", "secret key") - stcpCmd.PersistentFlags().StringVarP(&serverName, "server_name", "", "", "server name") - stcpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip") - stcpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port") - stcpCmd.PersistentFlags().StringVarP(&bindAddr, "bind_addr", "", "", "bind addr") - stcpCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "", 0, "bind port") - stcpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") - stcpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") - stcpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit") - stcpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode") - - rootCmd.AddCommand(stcpCmd) -} - -var stcpCmd = &cobra.Command{ - Use: "stcp", - Short: "Run frpc with a single stcp proxy", - RunE: func(cmd *cobra.Command, args []string) error { - clientCfg, err := parseClientCommonCfgFromCmd() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - proxyConfs := make(map[string]config.ProxyConf) - visitorConfs := make(map[string]config.VisitorConf) - - var prefix string - if user != "" { - prefix = user + "." - } - - switch role { - case "server": - cfg := &config.STCPProxyConf{} - cfg.ProxyName = prefix + proxyName - cfg.ProxyType = consts.STCPProxy - cfg.UseEncryption = useEncryption - cfg.UseCompression = useCompression - cfg.Role = role - cfg.Sk = sk - cfg.LocalIP = localIP - cfg.LocalPort = localPort - cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - cfg.BandwidthLimitMode = bandwidthLimitMode - err = cfg.ValidateForClient() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - proxyConfs[cfg.ProxyName] = cfg - case "visitor": - cfg := &config.STCPVisitorConf{} - cfg.ProxyName = prefix + proxyName - cfg.ProxyType = consts.STCPProxy - cfg.UseEncryption = useEncryption - cfg.UseCompression = useCompression - cfg.Role = role - cfg.Sk = sk - cfg.ServerName = serverName - cfg.BindAddr = bindAddr - cfg.BindPort = bindPort - err = cfg.Validate() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - visitorConfs[cfg.ProxyName] = cfg - default: - fmt.Println("invalid role") - os.Exit(1) - } - - err = startService(clientCfg, proxyConfs, visitorConfs, "") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - return nil - }, -} diff --git a/cmd/frpc/sub/stop.go b/cmd/frpc/sub/stop.go deleted file mode 100644 index 6c8f5f0aea3..00000000000 --- a/cmd/frpc/sub/stop.go +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright 2023 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 sub - -import ( - "encoding/base64" - "fmt" - "io" - "net/http" - "os" - "strings" - - "github.com/spf13/cobra" - - "github.com/fatedier/frp/pkg/config" -) - -func init() { - rootCmd.AddCommand(stopCmd) -} - -var stopCmd = &cobra.Command{ - Use: "stop", - Short: "Stop the running frpc", - RunE: func(cmd *cobra.Command, args []string) error { - cfg, _, _, err := config.ParseClientConfig(cfgFile) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - err = stopClient(cfg) - if err != nil { - fmt.Printf("frpc stop error: %v\n", err) - os.Exit(1) - } - fmt.Printf("stop success\n") - return nil - }, -} - -func stopClient(clientCfg config.ClientCommonConf) error { - if clientCfg.AdminPort == 0 { - return fmt.Errorf("admin_port shoud be set if you want to use stop feature") - } - - req, err := http.NewRequest("POST", "http://"+ - clientCfg.AdminAddr+":"+fmt.Sprintf("%d", clientCfg.AdminPort)+"/api/stop", nil) - if err != nil { - return err - } - - authStr := "Basic " + base64.StdEncoding.EncodeToString([]byte(clientCfg.AdminUser+":"+ - clientCfg.AdminPwd)) - - req.Header.Add("Authorization", authStr) - resp, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - if resp.StatusCode == 200 { - return nil - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - return fmt.Errorf("code [%d], %s", resp.StatusCode, strings.TrimSpace(string(body))) -} diff --git a/cmd/frpc/sub/sudp.go b/cmd/frpc/sub/sudp.go deleted file mode 100644 index 553e42528dc..00000000000 --- a/cmd/frpc/sub/sudp.go +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright 2018 fatedier, fatedier@gmail.com -// -// 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 sub - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" - - "github.com/fatedier/frp/pkg/config" - "github.com/fatedier/frp/pkg/consts" -) - -func init() { - RegisterCommonFlags(sudpCmd) - - sudpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name") - sudpCmd.PersistentFlags().StringVarP(&role, "role", "", "server", "role") - sudpCmd.PersistentFlags().StringVarP(&sk, "sk", "", "", "secret key") - sudpCmd.PersistentFlags().StringVarP(&serverName, "server_name", "", "", "server name") - sudpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip") - sudpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port") - sudpCmd.PersistentFlags().StringVarP(&bindAddr, "bind_addr", "", "", "bind addr") - sudpCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "", 0, "bind port") - sudpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") - sudpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") - sudpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit") - sudpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode") - - rootCmd.AddCommand(sudpCmd) -} - -var sudpCmd = &cobra.Command{ - Use: "sudp", - Short: "Run frpc with a single sudp proxy", - RunE: func(cmd *cobra.Command, args []string) error { - clientCfg, err := parseClientCommonCfgFromCmd() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - proxyConfs := make(map[string]config.ProxyConf) - visitorConfs := make(map[string]config.VisitorConf) - - var prefix string - if user != "" { - prefix = user + "." - } - - switch role { - case "server": - cfg := &config.SUDPProxyConf{} - cfg.ProxyName = prefix + proxyName - cfg.ProxyType = consts.SUDPProxy - cfg.UseEncryption = useEncryption - cfg.UseCompression = useCompression - cfg.Role = role - cfg.Sk = sk - cfg.LocalIP = localIP - cfg.LocalPort = localPort - cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - cfg.BandwidthLimitMode = bandwidthLimitMode - err = cfg.ValidateForClient() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - proxyConfs[cfg.ProxyName] = cfg - case "visitor": - cfg := &config.SUDPVisitorConf{} - cfg.ProxyName = prefix + proxyName - cfg.ProxyType = consts.SUDPProxy - cfg.UseEncryption = useEncryption - cfg.UseCompression = useCompression - cfg.Role = role - cfg.Sk = sk - cfg.ServerName = serverName - cfg.BindAddr = bindAddr - cfg.BindPort = bindPort - err = cfg.Validate() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - visitorConfs[cfg.ProxyName] = cfg - default: - fmt.Println("invalid role") - os.Exit(1) - } - - err = startService(clientCfg, proxyConfs, visitorConfs, "") - if err != nil { - os.Exit(1) - } - return nil - }, -} diff --git a/cmd/frpc/sub/tcp.go b/cmd/frpc/sub/tcp.go deleted file mode 100644 index 2da9ad61be1..00000000000 --- a/cmd/frpc/sub/tcp.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2018 fatedier, fatedier@gmail.com -// -// 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 sub - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" - - "github.com/fatedier/frp/pkg/config" - "github.com/fatedier/frp/pkg/consts" -) - -func init() { - RegisterCommonFlags(tcpCmd) - - tcpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name") - tcpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip") - tcpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port") - tcpCmd.PersistentFlags().IntVarP(&remotePort, "remote_port", "r", 0, "remote port") - tcpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") - tcpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") - tcpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit") - tcpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode") - - rootCmd.AddCommand(tcpCmd) -} - -var tcpCmd = &cobra.Command{ - Use: "tcp", - Short: "Run frpc with a single tcp proxy", - RunE: func(cmd *cobra.Command, args []string) error { - clientCfg, err := parseClientCommonCfgFromCmd() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - cfg := &config.TCPProxyConf{} - var prefix string - if user != "" { - prefix = user + "." - } - cfg.ProxyName = prefix + proxyName - cfg.ProxyType = consts.TCPProxy - cfg.LocalIP = localIP - cfg.LocalPort = localPort - cfg.RemotePort = remotePort - cfg.UseEncryption = useEncryption - cfg.UseCompression = useCompression - cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - cfg.BandwidthLimitMode = bandwidthLimitMode - - err = cfg.ValidateForClient() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - proxyConfs := map[string]config.ProxyConf{ - cfg.ProxyName: cfg, - } - err = startService(clientCfg, proxyConfs, nil, "") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - return nil - }, -} diff --git a/cmd/frpc/sub/tcpmux.go b/cmd/frpc/sub/tcpmux.go deleted file mode 100644 index 4b993f9cec1..00000000000 --- a/cmd/frpc/sub/tcpmux.go +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright 2020 guylewin, guy@lewin.co.il -// -// 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 sub - -import ( - "fmt" - "os" - "strings" - - "github.com/spf13/cobra" - - "github.com/fatedier/frp/pkg/config" - "github.com/fatedier/frp/pkg/consts" -) - -func init() { - RegisterCommonFlags(tcpMuxCmd) - - tcpMuxCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name") - tcpMuxCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip") - tcpMuxCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port") - tcpMuxCmd.PersistentFlags().StringVarP(&customDomains, "custom_domain", "d", "", "custom domain") - tcpMuxCmd.PersistentFlags().StringVarP(&subDomain, "sd", "", "", "sub domain") - tcpMuxCmd.PersistentFlags().StringVarP(&multiplexer, "mux", "", "", "multiplexer") - tcpMuxCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") - tcpMuxCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") - tcpMuxCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit") - tcpMuxCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode") - - rootCmd.AddCommand(tcpMuxCmd) -} - -var tcpMuxCmd = &cobra.Command{ - Use: "tcpmux", - Short: "Run frpc with a single tcpmux proxy", - RunE: func(cmd *cobra.Command, args []string) error { - clientCfg, err := parseClientCommonCfgFromCmd() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - cfg := &config.TCPMuxProxyConf{} - var prefix string - if user != "" { - prefix = user + "." - } - cfg.ProxyName = prefix + proxyName - cfg.ProxyType = consts.TCPMuxProxy - cfg.LocalIP = localIP - cfg.LocalPort = localPort - cfg.CustomDomains = strings.Split(customDomains, ",") - cfg.SubDomain = subDomain - cfg.Multiplexer = multiplexer - cfg.UseEncryption = useEncryption - cfg.UseCompression = useCompression - cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - cfg.BandwidthLimitMode = bandwidthLimitMode - - err = cfg.ValidateForClient() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - proxyConfs := map[string]config.ProxyConf{ - cfg.ProxyName: cfg, - } - err = startService(clientCfg, proxyConfs, nil, "") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - return nil - }, -} diff --git a/cmd/frpc/sub/udp.go b/cmd/frpc/sub/udp.go deleted file mode 100644 index 9a4803dc94d..00000000000 --- a/cmd/frpc/sub/udp.go +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2018 fatedier, fatedier@gmail.com -// -// 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 sub - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" - - "github.com/fatedier/frp/pkg/config" - "github.com/fatedier/frp/pkg/consts" -) - -func init() { - RegisterCommonFlags(udpCmd) - - udpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name") - udpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip") - udpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port") - udpCmd.PersistentFlags().IntVarP(&remotePort, "remote_port", "r", 0, "remote port") - udpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") - udpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") - udpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit") - udpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode") - - rootCmd.AddCommand(udpCmd) -} - -var udpCmd = &cobra.Command{ - Use: "udp", - Short: "Run frpc with a single udp proxy", - RunE: func(cmd *cobra.Command, args []string) error { - clientCfg, err := parseClientCommonCfgFromCmd() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - cfg := &config.UDPProxyConf{} - var prefix string - if user != "" { - prefix = user + "." - } - cfg.ProxyName = prefix + proxyName - cfg.ProxyType = consts.UDPProxy - cfg.LocalIP = localIP - cfg.LocalPort = localPort - cfg.RemotePort = remotePort - cfg.UseEncryption = useEncryption - cfg.UseCompression = useCompression - cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - cfg.BandwidthLimitMode = bandwidthLimitMode - - err = cfg.ValidateForClient() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - proxyConfs := map[string]config.ProxyConf{ - cfg.ProxyName: cfg, - } - err = startService(clientCfg, proxyConfs, nil, "") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - return nil - }, -} diff --git a/cmd/frpc/sub/verify.go b/cmd/frpc/sub/verify.go index c86b1e82b9c..a84f54f2a8f 100644 --- a/cmd/frpc/sub/verify.go +++ b/cmd/frpc/sub/verify.go @@ -21,6 +21,7 @@ import ( "github.com/spf13/cobra" "github.com/fatedier/frp/pkg/config" + "github.com/fatedier/frp/pkg/config/v1/validation" ) func init() { @@ -31,7 +32,20 @@ var verifyCmd = &cobra.Command{ Use: "verify", Short: "Verify that the configures is valid", RunE: func(cmd *cobra.Command, args []string) error { - _, _, _, err := config.ParseClientConfig(cfgFile) + if cfgFile == "" { + fmt.Println("frpc: the configuration file is not specified") + return nil + } + + cliCfg, pxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + warning, err := validation.ValidateAllClientConfig(cliCfg, pxyCfgs, visitorCfgs) + if warning != nil { + fmt.Printf("WARNING: %v\n", warning) + } if err != nil { fmt.Println(err) os.Exit(1) diff --git a/cmd/frpc/sub/xtcp.go b/cmd/frpc/sub/xtcp.go deleted file mode 100644 index 60483afa709..00000000000 --- a/cmd/frpc/sub/xtcp.go +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2018 fatedier, fatedier@gmail.com -// -// 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 sub - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" - - "github.com/fatedier/frp/pkg/config" - "github.com/fatedier/frp/pkg/consts" -) - -func init() { - RegisterCommonFlags(xtcpCmd) - - xtcpCmd.PersistentFlags().StringVarP(&proxyName, "proxy_name", "n", "", "proxy name") - xtcpCmd.PersistentFlags().StringVarP(&role, "role", "", "server", "role") - xtcpCmd.PersistentFlags().StringVarP(&sk, "sk", "", "", "secret key") - xtcpCmd.PersistentFlags().StringVarP(&serverName, "server_name", "", "", "server name") - xtcpCmd.PersistentFlags().StringVarP(&localIP, "local_ip", "i", "127.0.0.1", "local ip") - xtcpCmd.PersistentFlags().IntVarP(&localPort, "local_port", "l", 0, "local port") - xtcpCmd.PersistentFlags().StringVarP(&bindAddr, "bind_addr", "", "", "bind addr") - xtcpCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "", 0, "bind port") - xtcpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption") - xtcpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression") - xtcpCmd.PersistentFlags().StringVarP(&bandwidthLimit, "bandwidth_limit", "", "", "bandwidth limit") - xtcpCmd.PersistentFlags().StringVarP(&bandwidthLimitMode, "bandwidth_limit_mode", "", config.BandwidthLimitModeClient, "bandwidth limit mode") - - rootCmd.AddCommand(xtcpCmd) -} - -var xtcpCmd = &cobra.Command{ - Use: "xtcp", - Short: "Run frpc with a single xtcp proxy", - RunE: func(cmd *cobra.Command, args []string) error { - clientCfg, err := parseClientCommonCfgFromCmd() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - proxyConfs := make(map[string]config.ProxyConf) - visitorConfs := make(map[string]config.VisitorConf) - - var prefix string - if user != "" { - prefix = user + "." - } - - switch role { - case "server": - cfg := &config.XTCPProxyConf{} - cfg.ProxyName = prefix + proxyName - cfg.ProxyType = consts.XTCPProxy - cfg.UseEncryption = useEncryption - cfg.UseCompression = useCompression - cfg.Role = role - cfg.Sk = sk - cfg.LocalIP = localIP - cfg.LocalPort = localPort - cfg.BandwidthLimit, err = config.NewBandwidthQuantity(bandwidthLimit) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - cfg.BandwidthLimitMode = bandwidthLimitMode - err = cfg.ValidateForClient() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - proxyConfs[cfg.ProxyName] = cfg - case "visitor": - cfg := &config.XTCPVisitorConf{} - cfg.ProxyName = prefix + proxyName - cfg.ProxyType = consts.XTCPProxy - cfg.UseEncryption = useEncryption - cfg.UseCompression = useCompression - cfg.Role = role - cfg.Sk = sk - cfg.ServerName = serverName - cfg.BindAddr = bindAddr - cfg.BindPort = bindPort - err = cfg.Validate() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - visitorConfs[cfg.ProxyName] = cfg - default: - fmt.Println("invalid role") - os.Exit(1) - } - - err = startService(clientCfg, proxyConfs, visitorConfs, "") - if err != nil { - fmt.Println(err) - os.Exit(1) - } - return nil - }, -} diff --git a/cmd/frps/flags.go b/cmd/frps/flags.go new file mode 100644 index 00000000000..50170684734 --- /dev/null +++ b/cmd/frps/flags.go @@ -0,0 +1,110 @@ +// Copyright 2023 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 main + +import ( + "strconv" + + "github.com/spf13/cobra" + + "github.com/fatedier/frp/pkg/config/types" + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +type PortsRangeSliceFlag struct { + V *[]types.PortsRange +} + +func (f *PortsRangeSliceFlag) String() string { + if f.V == nil { + return "" + } + return types.PortsRangeSlice(*f.V).String() +} + +func (f *PortsRangeSliceFlag) Set(s string) error { + slice, err := types.NewPortsRangeSliceFromString(s) + if err != nil { + return err + } + *f.V = slice + return nil +} + +func (f *PortsRangeSliceFlag) Type() string { + return "string" +} + +type BoolFuncFlag struct { + TrueFunc func() + FalseFunc func() + + v bool +} + +func (f *BoolFuncFlag) String() string { + return strconv.FormatBool(f.v) +} + +func (f *BoolFuncFlag) Set(s string) error { + f.v = strconv.FormatBool(f.v) == "true" + + if !f.v { + if f.FalseFunc != nil { + f.FalseFunc() + } + return nil + } + + if f.TrueFunc != nil { + f.TrueFunc() + } + return nil +} + +func (f *BoolFuncFlag) Type() string { + return "bool" +} + +func RegisterServerConfigFlags(cmd *cobra.Command, c *v1.ServerConfig) { + cmd.PersistentFlags().StringVarP(&c.BindAddr, "bind_addr", "", "0.0.0.0", "bind address") + cmd.PersistentFlags().IntVarP(&c.BindPort, "bind_port", "p", 7000, "bind port") + cmd.PersistentFlags().IntVarP(&c.KCPBindPort, "kcp_bind_port", "", 0, "kcp bind udp port") + cmd.PersistentFlags().StringVarP(&c.ProxyBindAddr, "proxy_bind_addr", "", "0.0.0.0", "proxy bind address") + cmd.PersistentFlags().IntVarP(&c.VhostHTTPPort, "vhost_http_port", "", 0, "vhost http port") + cmd.PersistentFlags().IntVarP(&c.VhostHTTPSPort, "vhost_https_port", "", 0, "vhost https port") + cmd.PersistentFlags().Int64VarP(&c.VhostHTTPTimeout, "vhost_http_timeout", "", 60, "vhost http response header timeout") + cmd.PersistentFlags().StringVarP(&c.WebServer.Addr, "dashboard_addr", "", "0.0.0.0", "dashboard address") + cmd.PersistentFlags().IntVarP(&c.WebServer.Port, "dashboard_port", "", 0, "dashboard port") + cmd.PersistentFlags().StringVarP(&c.WebServer.User, "dashboard_user", "", "admin", "dashboard user") + cmd.PersistentFlags().StringVarP(&c.WebServer.Password, "dashboard_pwd", "", "admin", "dashboard password") + cmd.PersistentFlags().BoolVarP(&c.EnablePrometheus, "enable_prometheus", "", false, "enable prometheus dashboard") + cmd.PersistentFlags().StringVarP(&c.Log.To, "log_file", "", "console", "log file") + cmd.PersistentFlags().StringVarP(&c.Log.Level, "log_level", "", "info", "log level") + cmd.PersistentFlags().Int64VarP(&c.Log.MaxDays, "log_max_days", "", 3, "log max days") + cmd.PersistentFlags().BoolVarP(&c.Log.DisablePrintColor, "disable_log_color", "", false, "disable log color in console") + cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token") + cmd.PersistentFlags().StringVarP(&c.SubDomainHost, "subdomain_host", "", "", "subdomain host") + cmd.PersistentFlags().VarP(&PortsRangeSliceFlag{V: &c.AllowPorts}, "allow_ports", "", "allow ports") + cmd.PersistentFlags().Int64VarP(&c.MaxPortsPerClient, "max_ports_per_client", "", 0, "max ports per client") + cmd.PersistentFlags().BoolVarP(&c.Transport.TLS.Force, "tls_only", "", false, "frps tls only") + + webServerTLS := v1.TLSConfig{} + cmd.PersistentFlags().StringVarP(&webServerTLS.CertFile, "dashboard_tls_cert_file", "", "", "dashboard tls cert file") + cmd.PersistentFlags().StringVarP(&webServerTLS.KeyFile, "dashboard_tls_key_file", "", "", "dashboard tls key file") + cmd.PersistentFlags().VarP(&BoolFuncFlag{ + TrueFunc: func() { c.WebServer.TLS = &webServerTLS }, + }, "dashboard_tls_mode", "", "if enable dashboard tls mode") +} diff --git a/cmd/frps/main.go b/cmd/frps/main.go index 6ae5378be48..34676a2ba6b 100644 --- a/cmd/frps/main.go +++ b/cmd/frps/main.go @@ -15,9 +15,6 @@ package main import ( - "math/rand" - "time" - "github.com/fatedier/golib/crypto" _ "github.com/fatedier/frp/assets/frps" @@ -26,8 +23,5 @@ import ( func main() { crypto.DefaultSalt = "frp" - // TODO: remove this when we drop support for go1.19 - rand.Seed(time.Now().UnixNano()) - Execute() } diff --git a/cmd/frps/root.go b/cmd/frps/root.go index e1859376a9a..4a6f0117620 100644 --- a/cmd/frps/root.go +++ b/cmd/frps/root.go @@ -21,78 +21,26 @@ import ( "github.com/spf13/cobra" - "github.com/fatedier/frp/pkg/auth" "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/config/v1/validation" "github.com/fatedier/frp/pkg/util/log" - "github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/version" "github.com/fatedier/frp/server" ) -const ( - CfgFileTypeIni = iota - CfgFileTypeCmd -) - var ( cfgFile string showVersion bool - bindAddr string - bindPort int - kcpBindPort int - proxyBindAddr string - vhostHTTPPort int - vhostHTTPSPort int - vhostHTTPTimeout int64 - dashboardAddr string - dashboardPort int - dashboardUser string - dashboardPwd string - enablePrometheus bool - logFile string - logLevel string - logMaxDays int64 - disableLogColor bool - token string - subDomainHost string - allowPorts string - maxPortsPerClient int64 - tlsOnly bool - dashboardTLSMode bool - dashboardTLSCertFile string - dashboardTLSKeyFile string + serverCfg v1.ServerConfig ) func init() { rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps") rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frps") - rootCmd.PersistentFlags().StringVarP(&bindAddr, "bind_addr", "", "0.0.0.0", "bind address") - rootCmd.PersistentFlags().IntVarP(&bindPort, "bind_port", "p", 7000, "bind port") - rootCmd.PersistentFlags().IntVarP(&kcpBindPort, "kcp_bind_port", "", 0, "kcp bind udp port") - rootCmd.PersistentFlags().StringVarP(&proxyBindAddr, "proxy_bind_addr", "", "0.0.0.0", "proxy bind address") - rootCmd.PersistentFlags().IntVarP(&vhostHTTPPort, "vhost_http_port", "", 0, "vhost http port") - rootCmd.PersistentFlags().IntVarP(&vhostHTTPSPort, "vhost_https_port", "", 0, "vhost https port") - rootCmd.PersistentFlags().Int64VarP(&vhostHTTPTimeout, "vhost_http_timeout", "", 60, "vhost http response header timeout") - rootCmd.PersistentFlags().StringVarP(&dashboardAddr, "dashboard_addr", "", "0.0.0.0", "dashboard address") - rootCmd.PersistentFlags().IntVarP(&dashboardPort, "dashboard_port", "", 0, "dashboard port") - rootCmd.PersistentFlags().StringVarP(&dashboardUser, "dashboard_user", "", "admin", "dashboard user") - rootCmd.PersistentFlags().StringVarP(&dashboardPwd, "dashboard_pwd", "", "admin", "dashboard password") - rootCmd.PersistentFlags().BoolVarP(&enablePrometheus, "enable_prometheus", "", false, "enable prometheus dashboard") - rootCmd.PersistentFlags().StringVarP(&logFile, "log_file", "", "console", "log file") - rootCmd.PersistentFlags().StringVarP(&logLevel, "log_level", "", "info", "log level") - rootCmd.PersistentFlags().Int64VarP(&logMaxDays, "log_max_days", "", 3, "log max days") - rootCmd.PersistentFlags().BoolVarP(&disableLogColor, "disable_log_color", "", false, "disable log color in console") - - rootCmd.PersistentFlags().StringVarP(&token, "token", "t", "", "auth token") - rootCmd.PersistentFlags().StringVarP(&subDomainHost, "subdomain_host", "", "", "subdomain host") - rootCmd.PersistentFlags().StringVarP(&allowPorts, "allow_ports", "", "", "allow ports") - rootCmd.PersistentFlags().Int64VarP(&maxPortsPerClient, "max_ports_per_client", "", 0, "max ports per client") - rootCmd.PersistentFlags().BoolVarP(&tlsOnly, "tls_only", "", false, "frps tls only") - rootCmd.PersistentFlags().BoolVarP(&dashboardTLSMode, "dashboard_tls_mode", "", false, "dashboard tls mode") - rootCmd.PersistentFlags().StringVarP(&dashboardTLSCertFile, "dashboard_tls_cert_file", "", "", "dashboard tls cert file") - rootCmd.PersistentFlags().StringVarP(&dashboardTLSKeyFile, "dashboard_tls_key_file", "", "", "dashboard tls key file") + RegisterServerConfigFlags(rootCmd, &serverCfg) } var rootCmd = &cobra.Command{ @@ -104,24 +52,36 @@ var rootCmd = &cobra.Command{ return nil } - var cfg config.ServerCommonConf - var err error + var ( + svrCfg *v1.ServerConfig + isLegacyFormat bool + err error + ) if cfgFile != "" { - var content []byte - content, err = config.GetRenderedConfFromFile(cfgFile) + svrCfg, isLegacyFormat, err = config.LoadServerConfig(cfgFile) if err != nil { - return err + fmt.Println(err) + os.Exit(1) + } + if isLegacyFormat { + fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, " + + "please use yaml/json/toml format instead!\n") } - cfg, err = parseServerCommonCfg(CfgFileTypeIni, content) } else { - cfg, err = parseServerCommonCfg(CfgFileTypeCmd, nil) + serverCfg.Complete() + svrCfg = &serverCfg + } + + warning, err := validation.ValidateServerConfig(svrCfg) + if warning != nil { + fmt.Printf("WARNING: %v\n", warning) } if err != nil { - return err + fmt.Println(err) + os.Exit(1) } - err = runServer(cfg) - if err != nil { + if err := runServer(svrCfg); err != nil { fmt.Println(err) os.Exit(1) } @@ -135,70 +95,8 @@ func Execute() { } } -func parseServerCommonCfg(fileType int, source []byte) (cfg config.ServerCommonConf, err error) { - if fileType == CfgFileTypeIni { - cfg, err = config.UnmarshalServerConfFromIni(source) - } else if fileType == CfgFileTypeCmd { - cfg, err = parseServerCommonCfgFromCmd() - } - if err != nil { - return - } - cfg.Complete() - err = cfg.Validate() - if err != nil { - err = fmt.Errorf("parse config error: %v", err) - return - } - return -} - -func parseServerCommonCfgFromCmd() (cfg config.ServerCommonConf, err error) { - cfg = config.GetDefaultServerConf() - - cfg.BindAddr = bindAddr - cfg.BindPort = bindPort - cfg.KCPBindPort = kcpBindPort - cfg.ProxyBindAddr = proxyBindAddr - cfg.VhostHTTPPort = vhostHTTPPort - cfg.VhostHTTPSPort = vhostHTTPSPort - cfg.VhostHTTPTimeout = vhostHTTPTimeout - cfg.DashboardAddr = dashboardAddr - cfg.DashboardPort = dashboardPort - cfg.DashboardUser = dashboardUser - cfg.DashboardPwd = dashboardPwd - cfg.EnablePrometheus = enablePrometheus - cfg.DashboardTLSCertFile = dashboardTLSCertFile - cfg.DashboardTLSKeyFile = dashboardTLSKeyFile - cfg.DashboardTLSMode = dashboardTLSMode - cfg.LogFile = logFile - cfg.LogLevel = logLevel - cfg.LogMaxDays = logMaxDays - cfg.SubDomainHost = subDomainHost - cfg.TLSOnly = tlsOnly - - // Only token authentication is supported in cmd mode - cfg.ServerConfig = auth.GetDefaultServerConf() - cfg.Token = token - if len(allowPorts) > 0 { - // e.g. 1000-2000,2001,2002,3000-4000 - ports, errRet := util.ParseRangeNumbers(allowPorts) - if errRet != nil { - err = fmt.Errorf("parse conf error: allow_ports: %v", errRet) - return - } - - for _, port := range ports { - cfg.AllowPorts[int(port)] = struct{}{} - } - } - cfg.MaxPortsPerClient = maxPortsPerClient - cfg.DisableLogColor = disableLogColor - return -} - -func runServer(cfg config.ServerCommonConf) (err error) { - log.InitLog(cfg.LogWay, cfg.LogFile, cfg.LogLevel, cfg.LogMaxDays, cfg.DisableLogColor) +func runServer(cfg *v1.ServerConfig) (err error) { + log.InitLog(cfg.Log.To, cfg.Log.Level, cfg.Log.MaxDays, cfg.Log.DisablePrintColor) if cfgFile != "" { log.Info("frps uses config file: %s", cfgFile) diff --git a/cmd/frps/verify.go b/cmd/frps/verify.go index 42f2ca74b08..4f0cefb18fb 100644 --- a/cmd/frps/verify.go +++ b/cmd/frps/verify.go @@ -21,6 +21,7 @@ import ( "github.com/spf13/cobra" "github.com/fatedier/frp/pkg/config" + "github.com/fatedier/frp/pkg/config/v1/validation" ) func init() { @@ -32,21 +33,23 @@ var verifyCmd = &cobra.Command{ Short: "Verify that the configures is valid", RunE: func(cmd *cobra.Command, args []string) error { if cfgFile == "" { - fmt.Println("no config file is specified") + fmt.Println("frps: the configuration file is not specified") return nil } - iniContent, err := config.GetRenderedConfFromFile(cfgFile) + svrCfg, _, err := config.LoadServerConfig(cfgFile) if err != nil { fmt.Println(err) os.Exit(1) } - _, err = parseServerCommonCfg(CfgFileTypeIni, iniContent) + warning, err := validation.ValidateServerConfig(svrCfg) + if warning != nil { + fmt.Printf("WARNING: %v\n", warning) + } if err != nil { fmt.Println(err) os.Exit(1) } - fmt.Printf("frps: the configuration file %s syntax is ok\n", cfgFile) return nil }, diff --git a/conf/frpc.ini b/conf/frpc.ini deleted file mode 100644 index 13a8e5f6fc9..00000000000 --- a/conf/frpc.ini +++ /dev/null @@ -1,9 +0,0 @@ -[common] -server_addr = 127.0.0.1 -server_port = 7000 - -[ssh] -type = tcp -local_ip = 127.0.0.1 -local_port = 22 -remote_port = 6000 diff --git a/conf/frpc.toml b/conf/frpc.toml new file mode 100644 index 00000000000..05d6cbe2a5b --- /dev/null +++ b/conf/frpc.toml @@ -0,0 +1,360 @@ +# your proxy name will be changed to {user}.{proxy} +user = "your_name" + +# A literal address or host name for IPv6 must be enclosed +# in square brackets, as in "[::1]:80", "[ipv6-host]:http" or "[ipv6-host%zone]:80" +# For single serverAddr field, no need square brackets, like serverAddr = "::". +serverAddr = "0.0.0.0" +serverPort = 7000 + +# STUN server to help penetrate NAT hole. +# natHoleStunServer = "stun.easyvoip.com:3478" + +# Decide if exit program when first login failed, otherwise continuous relogin to frps +# default is true +loginFailExit = true + +# console or real logFile path like ./frpc.log +log.to = "./frpc.log" +# trace, debug, info, warn, error +log.level = "info" +log.maxDays = 3 +# disable log colors when log.to is console, default is false +log.disablePrintColor = false + +auth.method = "token" +# auth.additionalScopes specifies additional scopes to include authentication information. +# Optional values are HeartBeats, NewWorkConns. +# auth.additionalScopes = ["HeartBeats", "NewWorkConns"] + +# auth token +auth.token = "12345678" + +# oidc.clientID specifies the client ID to use to get a token in OIDC authentication. +# auth.oidc.clientID = "" +# oidc.clientSecret specifies the client secret to use to get a token in OIDC authentication. +# auth.oidc.clientSecret = "" +# oidc.audience specifies the audience of the token in OIDC authentication. +# auth.oidc.audience = "" +# oidc_scope specifies the permisssions of the token in OIDC authentication if AuthenticationMethod == "oidc". By default, this value is "". +# auth.oidc.scope = "" +# oidc.tokenEndpointURL specifies the URL which implements OIDC Token Endpoint. +# It will be used to get an OIDC token. +# auth.oidc.tokenEndpointURL = "" + +# oidc.additionalEndpointParams specifies additional parameters to be sent to the OIDC Token Endpoint. +# For example, if you want to specify the "audience" parameter, you can set as follow. +# frp will add "audience=" "var1=" to the additional parameters. +# auth.oidc.additionalEndpointParams.audience = "https://dev.auth.com/api/v2/" +# auth.oidc.additionalEndpointParams.var1 = "foobar" + +# Set admin address for control frpc's action by http api such as reload +webServer.addr = "127.0.0.1" +webServer.port = 7400 +webServer.user = "admin" +webServer.password = "admin" +# Admin assets directory. By default, these assets are bundled with frpc. +# webServer.assetsDir = "./static" + +# Enable golang pprof handlers in admin listener. +webServer.pprofEnable = false + +# The maximum amount of time a dial to server will wait for a connect to complete. Default value is 10 seconds. +# transport.dialServerTimeout = 10 + +# dialServerKeepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps. +# If negative, keep-alive probes are disabled. +# transport.dialServerKeepalive = 7200 + +# connections will be established in advance, default value is zero +transport.poolCount = 5 + +# If tcp stream multiplexing is used, default is true, it must be same with frps +# transport.tcpMux = true + +# Specify keep alive interval for tcp mux. +# only valid if tcpMux is enabled. +# transport.tcpMuxKeepaliveInterval = 60 + +# Communication protocol used to connect to server +# supports tcp, kcp, quic, websocket and wss now, default is tcp +transport.protocol = "tcp" + +# set client binding ip when connect server, default is empty. +# only when protocol = tcp or websocket, the value will be used. +transport.connectServerLocalIP = "0.0.0.0" + +# if you want to connect frps by http proxy or socks5 proxy or ntlm proxy, you can set proxyURL here or in global environment variables +# it only works when protocol is tcp +# transport.proxyURL = "http://user:passwd@192.168.1.128:8080" +# transport.proxyURL = "socks5://user:passwd@192.168.1.128:1080" +# transport.proxyURL = "ntlm://user:passwd@192.168.1.128:2080" + +# quic protocol options +# transport.quic.keepalivePeriod = 10 +# transport.quic.maxIdleTimeout = 30 +# transport.quic.maxIncomingStreams = 100000 + +# If tls.enable is true, frpc will connect frps by tls. +# Since v0.50.0, the default value has been changed to true, and tls is enabled by default. +transport.tls.enable = true + +# transport.tls.certFile = "client.crt" +# transport.tls.keyFile = "client.key" +# transport.tls.trustedCaFile = "ca.crt" +# transport.tls.serverName = "example.com" + +# If the disableCustomTLSFirstByte is set to false, frpc will establish a connection with frps using the +# 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. +# transport.tls.disableCustomTLSFirstByte = true + +# Heartbeat configure, it's not recommended to modify the default value. +# The default value of heartbeat_interval is 10 and heartbeat_timeout is 90. Set negative value +# to disable it. +# transport.heartbeatInterval = 30 +# transport.heartbeatTimeout = 90 + +# Specify a dns server, so frpc will use this instead of default one +# dnsServer = "8.8.8.8" + +# Proxy names you want to start. +# Default is empty, means all proxies. +# start = ["ssh", "dns"] + +# Specify udp packet size, unit is byte. If not set, the default value is 1500. +# This parameter should be same between client and server. +# It affects the udp and sudp proxy. +udpPacketSize = 1500 + +# Additional metadatas for client. +metadatas.var1 = "abc" +metadatas.var2 = "123" + +# Include other config files for proxies. +# includes = ["./confd/*.ini"] + +[[proxies]] +# 'ssh' is the unique proxy name +# If global user is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh' +name = "ssh" +type = "tcp" +localIP = "127.0.0.1" +localPort = 22 +# Limit bandwidth for this proxy, unit is KB and MB +transport.bandwidthLimit = "1MB" +# Where to limit bandwidth, can be 'client' or 'server', default is 'client' +transport.bandwidthLimitMode = "client" +# If true, traffic of this proxy will be encrypted, default is false +transport.useEncryption = false +# If true, traffic will be compressed +transport.useCompression = false +# Remote port listen by frps +remotePort = 6001 +# frps will load balancing connections for proxies in same group +loadBalancer.group = "test_group" +# group should have same group key +loadBalancer.groupKey = "123456" +# Enable health check for the backend service, it supports 'tcp' and 'http' now. +# frpc will connect local service's port to detect it's healthy status +healthCheck.type = "tcp" +# Health check connection timeout +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 +healthCheck.intervalSeconds = 10 +# additional meta info for each proxy +metadatas.var1 = "abc" +metadatas.var2 = "123" + +[[proxies]] +name = "ssh_random" +type = "tcp" +localIP = "192.168.31.100" +localPort = 22 +# If remote_port is 0, frps will assign a random port for you +remotePort = 0 + +[[proxies]] +name = "dns" +type = "udp" +localIP = "114.114.114.114" +localPort = 53 +remotePort = 6002 + +# Resolve your domain names to [server_addr] so you can use http://web01.yourdomain.com to browse web01 and http://web02.yourdomain.com to browse web02 +[[proxies]] +name = "web01" +type = "http" +localIP = "127.0.0.1" +localPort = 80 +# http username and password are safety certification for http protocol +# if not set, you can access this custom_domains without certification +httpUser = "admin" +httpPassword = "admin" +# if domain for frps is frps.com, then you can access [web01] proxy by URL http://web01.frps.com +subdomain = "web01" +customDomains = ["web01.yourdomain.com"] +# locations is only available for http type +locations = ["/", "/pic"] +# route requests to this service if http basic auto user is abc +# route_by_http_user = abc +hostHeaderRewrite = "example.com" +# params with prefix "header_" will be used to update http request headers +requestHeaders.set.x-from-where = "frp" +healthCheck.type = "http" +# frpc will send a GET http request '/status' to local http service +# http service is alive when it return 2xx http response code +healthCheck.path = "/status" +healthCheck.intervalSeconds = 10 +healthCheck.maxFailed = 3 +healthCheck.timeoutSeconds = 3 + +[[proxies]] +name = "web02" +type = "https" +localIP = "127.0.0.1" +localPort = 8000 +subdomain = "web02" +customDomains = ["web02.yourdomain.com"] +# if not empty, frpc will use proxy protocol to transfer connection info to your local service +# v1 or v2 or empty +transport.proxyProtocolVersion = "v2" + +[[proxies]] +name = "tcpmuxhttpconnect" +type = "tcpmux" +multiplexer = "httpconnect" +localIP = "127.0.0.1" +localPort = 10701 +customDomains = ["tunnel1"] +# routeByHTTPUser = "user1" + +[[proxies]] +name = "plugin_unix_domain_socket" +type = "tcp" +remotePort = 6003 +# if plugin is defined, local_ip and local_port is useless +# plugin will handle connections got from frps +[proxies.plugin] +type = "unix_domain_socket" +unixPath = "/var/run/docker.sock" + +[[proxies]] +name = "plugin_http_proxy" +type = "tcp" +remotePort = 6004 +[proxies.plugin] +type = "http_proxy" +httpUser = "abc" +httpPassword = "abc" + +[[proxies]] +name = "plugin_socks5" +type = "tcp" +remotePort = 6005 +[proxies.plugin] +type = "socks5" +username = "abc" +password = "abc" + +[[proxies]] +name = "plugin_static_file" +type = "tcp" +remotePort = 6006 +[proxies.plugin] +type = "static_file" +localPath = "/var/www/blog" +stripPrefix = "static" +httpUser = "abc" +httpPassword = "abc" + +[[proxies]] +name = "plugin_https2http" +type = "https" +customDomains = ["test.yourdomain.com"] +[proxies.plugin] +type = "https2http" +localAddr = "127.0.0.1:80" +crtPath = "./server.crt" +keyPath = "./server.key" +hostHeaderRewrite = "127.0.0.1" +requestHeaders.set.x-from-where = "frp" + +[[proxies]] +name = "plugin_https2https" +type = "https" +customDomains = ["test.yourdomain.com"] +[proxies.plugin] +type = "https2https" +localAddr = "127.0.0.1:443" +crtPath = "./server.crt" +keyPath = "./server.key" +hostHeaderRewrite = "127.0.0.1" +requestHeaders.set.x-from-where = "frp" + +[[proxies]] +name = "plugin_http2https" +type = "http" +customDomains = ["test.yourdomain.com"] +[proxies.plugin] +type = "http2https" +localAddr = "127.0.0.1:443" +hostHeaderRewrite = "127.0.0.1" +requestHeaders.set.x-from-where = "frp" + +[[proxies]] +name = "secret_tcp" +# If the type is secret tcp, remote_port is useless +# Who want to connect local port should deploy another frpc with stcp proxy and role is visitor +type = "stcp" +# secretKey is used for authentication for visitors +secretKey = "abcdefg" +localIP = "127.0.0.1" +localPort = 22 +# If not empty, only visitors from specified users can connect. +# Otherwise, visitors from same user can connect. '*' means allow all users. +allowUsers = ["*"] + +[[proxies]] +name = "p2p_tcp" +type = "xtcp" +secretKey = "abcdefg" +localIP = "127.0.0.1" +localPort = 22 +# If not empty, only visitors from specified users can connect. +# Otherwise, visitors from same user can connect. '*' means allow all users. +allowUsers = ["user1", "user2"] + +# frpc role visitor -> frps -> frpc role server +[[visitors]] +name = "secret_tcp_visitor" +type = "stcp" +# the server name you want to visitor +serverName = "secret_tcp" +secretKey = "abcdefg" +# connect this address to visitor stcp server +bindAddr = "127.0.0.1" +# bindPort can be less than 0, it means don't bind to the port and only receive connections redirected from +# other visitors. (This is not supported for SUDP now) +bindPort = 9000 + +[[visitors]] +name = "p2p_tcp_visitor" +type = "xtcp" +# if the server user is not set, it defaults to the current user +serverUser = "user1" +serverName = "p2p_tcp" +secretKey = "abcdefg" +bindAddr = "127.0.0.1" +# bindPort can be less than 0, it means don't bind to the port and only receive connections redirected from +# other visitors. (This is not supported for SUDP now) +bindPort = 9001 +# when automatic tunnel persistence is required, set it to true +keepTunnelOpen = false +# effective when keep_tunnel_open is set to true, the number of attempts to punch through per hour +maxRetriesAnHour = 8 +minRetryInterval = 90 +# fallbackTo = "stcp_visitor" +# fallbackTimeoutMs = 500 diff --git a/conf/frps.ini b/conf/frps.ini deleted file mode 100644 index 229567a9d6b..00000000000 --- a/conf/frps.ini +++ /dev/null @@ -1,2 +0,0 @@ -[common] -bind_port = 7000 diff --git a/conf/frps.toml b/conf/frps.toml new file mode 100644 index 00000000000..59db6c9ea5c --- /dev/null +++ b/conf/frps.toml @@ -0,0 +1,154 @@ +# A literal address or host name for IPv6 must be enclosed +# in square brackets, as in "[::1]:80", "[ipv6-host]:http" or "[ipv6-host%zone]:80" +# For single "bind_addr" field, no need square brackets, like "bind_addr = ::". +bindAddr = "0.0.0.0" +bindPort = 7000 + +# udp port used for kcp protocol, it can be same with 'bind_port'. +# if not set, kcp is disabled in frps. +kcpBindPort = 7000 + +# udp port used for quic protocol. +# if not set, quic is disabled in frps. +# quicBindPort = 7002 + +# Specify which address proxy will listen for, default value is same with bind_addr +# proxy_bind_addr = "127.0.0.1" + +# quic protocol options +# transport.quic.keepalivePeriod = 10 +# transport.quic.maxIdleTimeout = 30 +# transport.quic.maxIncomingStreams = 100000 + +# Heartbeat configure, it's not recommended to modify the default value +# The default value of heartbeat_timeout is 90. Set negative value to disable it. +# transport.heartbeatTimeout = 90 + +# Pool count in each proxy will keep no more than maxPoolCount. +transport.maxPoolCount = 5 + +# If tcp stream multiplexing is used, default is true +# transport.tcpMux = true + +# Specify keep alive interval for tcp mux. +# only valid if tcpMux is true. +# transport.tcpMuxKeepaliveInterval = 60 + +# tcpKeepalive specifies the interval between keep-alive probes for an active network connection between frpc and frps. +# If negative, keep-alive probes are disabled. +# transport.tcpKeepalive = 7200 + +# transport.tls.force specifies whether to only accept TLS-encrypted connections. By default, the value is false. +tls.force = false + +# transport.tls.certFile = "server.crt" +# transport.tls.keyFile = "server.key" +# transport.tls.trustedCaFile = "ca.crt" + +# If you want to support virtual host, you must set the http port for listening (optional) +# Note: http port and https port can be same with bind_port +vhostHTTPPort = 80 +vhostHTTPSPort = 443 + +# Response header timeout(seconds) for vhost http server, default is 60s +# vhostHTTPTimeout = 60 + +# tcpmuxHTTPConnectPort specifies the port that the server listens for TCP +# HTTP CONNECT requests. If the value is 0, the server will not multiplex TCP +# requests on one single port. If it's not - it will listen on this value for +# HTTP CONNECT requests. By default, this value is 0. +# tcpmuxHTTPConnectPort = 1337 + +# If tcpmux_passthrough is true, frps won't do any update on traffic. +# tcpmuxPassthrough = false + +# Configure the web server to enable the dashboard for frps. +# dashboard is available only if webServer.port is set. +webServer.addr = "127.0.0.1" +webServer.port = 7500 +webServer.user = "admin" +webServer.password = "admin" +# webServer.tls.certFile = "server.crt" +# webServer.tls.keyFile = "server.key" +# dashboard assets directory(only for debug mode) +# webServer.assetsDir = "./static" + +# Enable golang pprof handlers in dashboard listener. +# Dashboard port must be set first +webServer.pprofEnable = false + +# enablePrometheus will export prometheus metrics on webServer in /metrics api. +enablePrometheus = true + +# console or real logFile path like ./frps.log +log.to = "./frps.log" +# trace, debug, info, warn, error +log.level = info +log.maxDays = 3 +# disable log colors when log.to is console, default is false +log.disablePrintColor = false + +# DetailedErrorsToClient defines whether to send the specific error (with debug info) to frpc. By default, this value is true. +detailedErrorsToClient = true + +# auth.method specifies what authentication method to use authenticate frpc with frps. +# If "token" is specified - token will be read into login message. +# If "oidc" is specified - OIDC (Open ID Connect) token will be issued using OIDC settings. By default, this value is "token". +auth.method = "token" + +# auth.additionalScopes specifies additional scopes to include authentication information. +# Optional values are HeartBeats, NewWorkConns. +# auth.additionalScopes = ["HeartBeats", "NewWorkConns"] + +# auth token +auth.token = "12345678" + +# oidc issuer specifies the issuer to verify OIDC tokens with. +auth.oidc.issuer = "" +# oidc audience specifies the audience OIDC tokens should contain when validated. +auth.oidc.audience = "" +# oidc skipExpiryCheck specifies whether to skip checking if the OIDC token is expired. +auth.oidc.skipExpiryCheck = false +# oidc skipIssuerCheck specifies whether to skip checking if the OIDC token's issuer claim matches the issuer specified in OidcIssuer. +auth.oidc.skipIssuerCheck = false + +# userConnTimeout specifies the maximum time to wait for a work connection. +# userConnTimeout = 10 + +# Only allow frpc to bind ports you list. By default, there won't be any limit. +allowPorts = [ + { start = 2000, end = 3000 }, + { single = 3001 }, + { single = 3003 }, + { start = 4000, end = 50000 } +] + +# Max ports can be used for each client, default value is 0 means no limit +maxPortsPerClient = 0 + +# If subDomainHost is not empty, you can set subdomain when type is http or https in frpc's configure file +# When subdomain is est, the host used by routing is test.frps.com +subDomainHost = "frps.com" + +# custom 404 page for HTTP requests +# custom404Page = "/path/to/404.html" + +# specify udp packet size, unit is byte. If not set, the default value is 1500. +# This parameter should be same between client and server. +# It affects the udp and sudp proxy. +udpPacketSize = 1500 + +# Retention time for NAT hole punching strategy data. +natholeAnalysisDataReserveHours = 168 + +[[httpPlugins]] +name = "user-manager" +addr = "127.0.0.1:9000" +path = "/handler" +ops = ["Login"] + +[[httpPlugins]] +name = "port-manager" +addr = "127.0.0.1:9001" +path = "/handler" +ops = ["NewProxy"] diff --git a/conf/frpc_full.ini b/conf/legacy/frpc_legacy_full.ini similarity index 100% rename from conf/frpc_full.ini rename to conf/legacy/frpc_legacy_full.ini diff --git a/conf/frps_full.ini b/conf/legacy/frps_legacy_full.ini similarity index 100% rename from conf/frps_full.ini rename to conf/legacy/frps_legacy_full.ini diff --git a/doc/server_plugin.md b/doc/server_plugin.md index f4e377f43c7..b8fa161907e 100644 --- a/doc/server_plugin.md +++ b/doc/server_plugin.md @@ -216,49 +216,50 @@ New user connection received from proxy (support `tcp`, `stcp`, `https` and `tcp ### Server Plugin Configuration -```ini -# frps.ini -[common] -bind_port = 7000 - -[plugin.user-manager] -addr = 127.0.0.1:9000 -path = /handler -ops = Login - -[plugin.port-manager] -addr = 127.0.0.1:9001 -path = /handler -ops = NewProxy +```toml +# frps.toml +bindPort = 7000 + +[[httpPlugins]] +name = "user-manager" +addr = "127.0.0.1:9000" +path = "/handler" +ops = ["Login"] + +[[httpPlugins]] +name = "port-manager" +addr = "127.0.0.1:9001" +path = "/handler" +ops = ["NewProxy"] ``` -- addr: the address where the external RPC service listens. Defaults to http. For https, specify the schema: `addr = https://127.0.0.1:9001`. +- addr: the address where the external RPC service listens. Defaults to http. For https, specify the schema: `addr = "https://127.0.0.1:9001"`. - path: http request url path for the POST request. - ops: operations plugin needs to handle (e.g. "Login", "NewProxy", ...). -- tls_verify: When the schema is https, we verify by default. Set this value to false if you want to skip verification. +- tlsVerify: When the schema is https, we verify by default. Set this value to false if you want to skip verification. ### Metadata Metadata will be sent to the server plugin in each RPC request. -There are 2 types of metadata entries - 1 under `[common]` and the other under each proxy configuration. -Metadata entries under `[common]` will be sent in `Login` under the key `metas`, and in any other RPC request under `user.metas`. +There are 2 types of metadata entries - global one and the other under each proxy configuration. +Global metadata entries will be sent in `Login` under the key `metas`, and in any other RPC request under `user.metas`. Metadata entries under each proxy configuration will be sent in `NewProxy` op only, under `metas`. -Metadata entries start with `meta_`. This is an example of metadata entries in `[common]` and under the proxy named `[ssh]`: - -``` -# frpc.ini -[common] -server_addr = 127.0.0.1 -server_port = 7000 -user = fake -meta_token = fake -meta_version = 1.0.0 - -[ssh] -type = tcp -local_port = 22 -remote_port = 6000 -meta_id = 123 +This is an example of metadata entries: + +```toml +# frpc.toml +serverAddr = "127.0.0.1" +serverPort = 7000 +user = "fake" +metadatas.token = "fake" +metadatas.version = "1.0.0" + +[[proxies]] +name = "ssh" +type = "tcp" +localPort = 22 +remotePort = 6000 +metadatas.id = "123" ``` diff --git a/go.mod b/go.mod index e9042ecf802..f7996399484 100644 --- a/go.mod +++ b/go.mod @@ -8,13 +8,13 @@ require ( github.com/fatedier/beego v0.0.0-20171024143340-6c6a4f5bd5eb github.com/fatedier/golib v0.1.1-0.20230725122706-dcbaee8eef40 github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible - github.com/go-playground/validator/v10 v10.14.1 github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 github.com/hashicorp/yamux v0.1.1 github.com/onsi/ginkgo/v2 v2.11.0 github.com/onsi/gomega v1.27.8 + github.com/pelletier/go-toml/v2 v2.1.0 github.com/pion/stun v0.6.1 github.com/pires/go-proxyproto v0.7.0 github.com/prometheus/client_golang v1.16.0 @@ -37,11 +37,8 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/go-logr/logr v1.2.4 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.3 // indirect @@ -52,7 +49,6 @@ require ( github.com/klauspost/cpuid/v2 v2.0.6 // indirect github.com/klauspost/reedsolomon v1.9.15 // indirect github.com/kr/text v0.2.0 // indirect - github.com/leodido/go-urn v1.2.4 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/pion/dtls/v2 v2.2.7 // indirect github.com/pion/logging v0.2.2 // indirect @@ -76,8 +72,11 @@ require ( golang.org/x/tools v0.9.3 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/yaml v1.3.0 // indirect ) // TODO(fatedier): Temporary use the modified version, update to the official version after merging into the official repository. diff --git a/go.sum b/go.sum index d1b794c9f1f..4cab567e29b 100644 --- a/go.sum +++ b/go.sum @@ -32,19 +32,10 @@ github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible h1: github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible/go.mod h1:YpCOaxj7vvMThhIQ9AfTOPW2sfztQR5WDfs7AflSy4s= github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d h1:ynk1ra0RUqDWQfvFi5KtMiSobkVQ3cNc0ODb8CfIETo= github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= -github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k= -github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -93,8 +84,6 @@ github.com/klauspost/reedsolomon v1.9.15/go.mod h1:eqPAcE7xar5CIzcdfwydOEdcmchAK github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= @@ -103,6 +92,8 @@ github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc= github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= @@ -147,7 +138,6 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -275,6 +265,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -286,3 +278,7 @@ k8s.io/client-go v0.27.4 h1:vj2YTtSJ6J4KxaC88P4pMPEQECWMY8gqPqsTgUKzvjk= k8s.io/client-go v0.27.4/go.mod h1:ragcly7lUlN0SRPk5/ZkGnDjPknzb37TICq07WhI6Xc= k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY= k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/hack/run-e2e.sh b/hack/run-e2e.sh index 953df0ee8ae..a7d4688a7be 100755 --- a/hack/run-e2e.sh +++ b/hack/run-e2e.sh @@ -6,7 +6,7 @@ ROOT=$(unset CDPATH && cd "$(dirname "$SCRIPT")/.." && pwd) ginkgo_command=$(which ginkgo 2>/dev/null) if [ -z "$ginkgo_command" ]; then echo "ginkgo not found, try to install..." - go install github.com/onsi/ginkgo/v2/ginkgo@v2.8.3 + go install github.com/onsi/ginkgo/v2/ginkgo@v2.11.0 fi debug=false diff --git a/package.sh b/package.sh index 1a63a4e8c30..f3b220b6a2d 100755 --- a/package.sh +++ b/package.sh @@ -47,6 +47,7 @@ for os in $os_all; do fi cp ../LICENSE ${frp_path} cp -rf ../conf/* ${frp_path} + rm -rf ${frp_path}/legacy # packages cd ./packages diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 894da2471c7..9d8db7b686b 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -17,76 +17,25 @@ package auth import ( "fmt" - "github.com/fatedier/frp/pkg/consts" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" ) -type BaseConfig struct { - // AuthenticationMethod specifies what authentication method to use to - // authenticate frpc with frps. If "token" is specified - token will be - // read into login message. If "oidc" is specified - OIDC (Open ID Connect) - // token will be issued using OIDC settings. By default, this value is "token". - AuthenticationMethod string `ini:"authentication_method" json:"authentication_method"` - // AuthenticateHeartBeats specifies whether to include authentication token in - // heartbeats sent to frps. By default, this value is false. - AuthenticateHeartBeats bool `ini:"authenticate_heartbeats" json:"authenticate_heartbeats"` - // AuthenticateNewWorkConns specifies whether to include authentication token in - // new work connections sent to frps. By default, this value is false. - AuthenticateNewWorkConns bool `ini:"authenticate_new_work_conns" json:"authenticate_new_work_conns"` -} - -func getDefaultBaseConf() BaseConfig { - return BaseConfig{ - AuthenticationMethod: "token", - AuthenticateHeartBeats: false, - AuthenticateNewWorkConns: false, - } -} - -type ClientConfig struct { - BaseConfig `ini:",extends"` - OidcClientConfig `ini:",extends"` - TokenConfig `ini:",extends"` -} - -func GetDefaultClientConf() ClientConfig { - return ClientConfig{ - BaseConfig: getDefaultBaseConf(), - OidcClientConfig: getDefaultOidcClientConf(), - TokenConfig: getDefaultTokenConf(), - } -} - -type ServerConfig struct { - BaseConfig `ini:",extends"` - OidcServerConfig `ini:",extends"` - TokenConfig `ini:",extends"` -} - -func GetDefaultServerConf() ServerConfig { - return ServerConfig{ - BaseConfig: getDefaultBaseConf(), - OidcServerConfig: getDefaultOidcServerConf(), - TokenConfig: getDefaultTokenConf(), - } -} - type Setter interface { SetLogin(*msg.Login) error SetPing(*msg.Ping) error SetNewWorkConn(*msg.NewWorkConn) error } -func NewAuthSetter(cfg ClientConfig) (authProvider Setter) { - switch cfg.AuthenticationMethod { - case consts.TokenAuthMethod: - authProvider = NewTokenAuth(cfg.BaseConfig, cfg.TokenConfig) - case consts.OidcAuthMethod: - authProvider = NewOidcAuthSetter(cfg.BaseConfig, cfg.OidcClientConfig) +func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter) { + switch cfg.Method { + case v1.AuthMethodToken: + authProvider = NewTokenAuth(cfg.AdditionalScopes, cfg.Token) + case v1.AuthMethodOIDC: + authProvider = NewOidcAuthSetter(cfg.AdditionalScopes, cfg.OIDC) default: - panic(fmt.Sprintf("wrong authentication method: '%s'", cfg.AuthenticationMethod)) + panic(fmt.Sprintf("wrong method: '%s'", cfg.Method)) } - return authProvider } @@ -96,13 +45,12 @@ type Verifier interface { VerifyNewWorkConn(*msg.NewWorkConn) error } -func NewAuthVerifier(cfg ServerConfig) (authVerifier Verifier) { - switch cfg.AuthenticationMethod { - case consts.TokenAuthMethod: - authVerifier = NewTokenAuth(cfg.BaseConfig, cfg.TokenConfig) - case consts.OidcAuthMethod: - authVerifier = NewOidcAuthVerifier(cfg.BaseConfig, cfg.OidcServerConfig) +func NewAuthVerifier(cfg v1.AuthServerConfig) (authVerifier Verifier) { + switch cfg.Method { + case v1.AuthMethodToken: + authVerifier = NewTokenAuth(cfg.AdditionalScopes, cfg.Token) + case v1.AuthMethodOIDC: + authVerifier = NewOidcAuthVerifier(cfg.AdditionalScopes, cfg.OIDC) } - return authVerifier } diff --git a/pkg/auth/legacy/legacy.go b/pkg/auth/legacy/legacy.go new file mode 100644 index 00000000000..c16d38f2c91 --- /dev/null +++ b/pkg/auth/legacy/legacy.go @@ -0,0 +1,145 @@ +// Copyright 2023 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 legacy + +type BaseConfig struct { + // AuthenticationMethod specifies what authentication method to use to + // authenticate frpc with frps. If "token" is specified - token will be + // read into login message. If "oidc" is specified - OIDC (Open ID Connect) + // token will be issued using OIDC settings. By default, this value is "token". + AuthenticationMethod string `ini:"authentication_method" json:"authentication_method"` + // AuthenticateHeartBeats specifies whether to include authentication token in + // heartbeats sent to frps. By default, this value is false. + AuthenticateHeartBeats bool `ini:"authenticate_heartbeats" json:"authenticate_heartbeats"` + // AuthenticateNewWorkConns specifies whether to include authentication token in + // new work connections sent to frps. By default, this value is false. + AuthenticateNewWorkConns bool `ini:"authenticate_new_work_conns" json:"authenticate_new_work_conns"` +} + +func getDefaultBaseConf() BaseConfig { + return BaseConfig{ + AuthenticationMethod: "token", + AuthenticateHeartBeats: false, + AuthenticateNewWorkConns: false, + } +} + +type ClientConfig struct { + BaseConfig `ini:",extends"` + OidcClientConfig `ini:",extends"` + TokenConfig `ini:",extends"` +} + +func GetDefaultClientConf() ClientConfig { + return ClientConfig{ + BaseConfig: getDefaultBaseConf(), + OidcClientConfig: getDefaultOidcClientConf(), + TokenConfig: getDefaultTokenConf(), + } +} + +type ServerConfig struct { + BaseConfig `ini:",extends"` + OidcServerConfig `ini:",extends"` + TokenConfig `ini:",extends"` +} + +func GetDefaultServerConf() ServerConfig { + return ServerConfig{ + BaseConfig: getDefaultBaseConf(), + OidcServerConfig: getDefaultOidcServerConf(), + TokenConfig: getDefaultTokenConf(), + } +} + +type OidcClientConfig struct { + // OidcClientID specifies the client ID to use to get a token in OIDC + // authentication if AuthenticationMethod == "oidc". By default, this value + // is "". + OidcClientID string `ini:"oidc_client_id" json:"oidc_client_id"` + // OidcClientSecret specifies the client secret to use to get a token in OIDC + // authentication if AuthenticationMethod == "oidc". By default, this value + // is "". + OidcClientSecret string `ini:"oidc_client_secret" json:"oidc_client_secret"` + // OidcAudience specifies the audience of the token in OIDC authentication + // if AuthenticationMethod == "oidc". By default, this value is "". + OidcAudience string `ini:"oidc_audience" json:"oidc_audience"` + // OidcScope specifies the scope of the token in OIDC authentication + // if AuthenticationMethod == "oidc". By default, this value is "". + OidcScope string `ini:"oidc_scope" json:"oidc_scope"` + // OidcTokenEndpointURL specifies the URL which implements OIDC Token Endpoint. + // It will be used to get an OIDC token if AuthenticationMethod == "oidc". + // By default, this value is "". + OidcTokenEndpointURL string `ini:"oidc_token_endpoint_url" json:"oidc_token_endpoint_url"` + + // OidcAdditionalEndpointParams specifies additional parameters to be sent + // this field will be transfer to map[string][]string in OIDC token generator + // The field will be set by prefix "oidc_additional_" + OidcAdditionalEndpointParams map[string]string `ini:"-" json:"oidc_additional_endpoint_params"` +} + +func getDefaultOidcClientConf() OidcClientConfig { + return OidcClientConfig{ + OidcClientID: "", + OidcClientSecret: "", + OidcAudience: "", + OidcScope: "", + OidcTokenEndpointURL: "", + OidcAdditionalEndpointParams: make(map[string]string), + } +} + +type OidcServerConfig struct { + // OidcIssuer specifies the issuer to verify OIDC tokens with. This issuer + // will be used to load public keys to verify signature and will be compared + // with the issuer claim in the OIDC token. It will be used if + // AuthenticationMethod == "oidc". By default, this value is "". + OidcIssuer string `ini:"oidc_issuer" json:"oidc_issuer"` + // OidcAudience specifies the audience OIDC tokens should contain when validated. + // If this value is empty, audience ("client ID") verification will be skipped. + // It will be used when AuthenticationMethod == "oidc". By default, this + // value is "". + OidcAudience string `ini:"oidc_audience" json:"oidc_audience"` + // OidcSkipExpiryCheck specifies whether to skip checking if the OIDC token is + // expired. It will be used when AuthenticationMethod == "oidc". By default, this + // value is false. + OidcSkipExpiryCheck bool `ini:"oidc_skip_expiry_check" json:"oidc_skip_expiry_check"` + // OidcSkipIssuerCheck specifies whether to skip checking if the OIDC token's + // issuer claim matches the issuer specified in OidcIssuer. It will be used when + // AuthenticationMethod == "oidc". By default, this value is false. + OidcSkipIssuerCheck bool `ini:"oidc_skip_issuer_check" json:"oidc_skip_issuer_check"` +} + +func getDefaultOidcServerConf() OidcServerConfig { + return OidcServerConfig{ + OidcIssuer: "", + OidcAudience: "", + OidcSkipExpiryCheck: false, + OidcSkipIssuerCheck: false, + } +} + +type TokenConfig struct { + // Token specifies the authorization token used to create keys to be sent + // to the server. The server must have a matching token for authorization + // to succeed. By default, this value is "". + Token string `ini:"token" json:"token"` +} + +func getDefaultTokenConf() TokenConfig { + return TokenConfig{ + Token: "", + } +} diff --git a/pkg/auth/oidc.go b/pkg/auth/oidc.go index c8ef923d3fc..f428a04dacf 100644 --- a/pkg/auth/oidc.go +++ b/pkg/auth/oidc.go @@ -19,105 +19,40 @@ import ( "fmt" "github.com/coreos/go-oidc/v3/oidc" + "github.com/samber/lo" "golang.org/x/oauth2/clientcredentials" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" ) -type OidcClientConfig struct { - // OidcClientID specifies the client ID to use to get a token in OIDC - // authentication if AuthenticationMethod == "oidc". By default, this value - // is "". - OidcClientID string `ini:"oidc_client_id" json:"oidc_client_id"` - // OidcClientSecret specifies the client secret to use to get a token in OIDC - // authentication if AuthenticationMethod == "oidc". By default, this value - // is "". - OidcClientSecret string `ini:"oidc_client_secret" json:"oidc_client_secret"` - // OidcAudience specifies the audience of the token in OIDC authentication - // if AuthenticationMethod == "oidc". By default, this value is "". - OidcAudience string `ini:"oidc_audience" json:"oidc_audience"` - // OidcScope specifies the scope of the token in OIDC authentication - // if AuthenticationMethod == "oidc". By default, this value is "". - OidcScope string `ini:"oidc_scope" json:"oidc_scope"` - // OidcTokenEndpointURL specifies the URL which implements OIDC Token Endpoint. - // It will be used to get an OIDC token if AuthenticationMethod == "oidc". - // By default, this value is "". - OidcTokenEndpointURL string `ini:"oidc_token_endpoint_url" json:"oidc_token_endpoint_url"` - - // OidcAdditionalEndpointParams specifies additional parameters to be sent - // this field will be transfer to map[string][]string in OIDC token generator - // The field will be set by prefix "oidc_additional_" - OidcAdditionalEndpointParams map[string]string `ini:"-" json:"oidc_additional_endpoint_params"` -} - -func getDefaultOidcClientConf() OidcClientConfig { - return OidcClientConfig{ - OidcClientID: "", - OidcClientSecret: "", - OidcAudience: "", - OidcScope: "", - OidcTokenEndpointURL: "", - OidcAdditionalEndpointParams: make(map[string]string), - } -} - -type OidcServerConfig struct { - // OidcIssuer specifies the issuer to verify OIDC tokens with. This issuer - // will be used to load public keys to verify signature and will be compared - // with the issuer claim in the OIDC token. It will be used if - // AuthenticationMethod == "oidc". By default, this value is "". - OidcIssuer string `ini:"oidc_issuer" json:"oidc_issuer"` - // OidcAudience specifies the audience OIDC tokens should contain when validated. - // If this value is empty, audience ("client ID") verification will be skipped. - // It will be used when AuthenticationMethod == "oidc". By default, this - // value is "". - OidcAudience string `ini:"oidc_audience" json:"oidc_audience"` - // OidcSkipExpiryCheck specifies whether to skip checking if the OIDC token is - // expired. It will be used when AuthenticationMethod == "oidc". By default, this - // value is false. - OidcSkipExpiryCheck bool `ini:"oidc_skip_expiry_check" json:"oidc_skip_expiry_check"` - // OidcSkipIssuerCheck specifies whether to skip checking if the OIDC token's - // issuer claim matches the issuer specified in OidcIssuer. It will be used when - // AuthenticationMethod == "oidc". By default, this value is false. - OidcSkipIssuerCheck bool `ini:"oidc_skip_issuer_check" json:"oidc_skip_issuer_check"` -} - -func getDefaultOidcServerConf() OidcServerConfig { - return OidcServerConfig{ - OidcIssuer: "", - OidcAudience: "", - OidcSkipExpiryCheck: false, - OidcSkipIssuerCheck: false, - } -} - type OidcAuthProvider struct { - BaseConfig + additionalAuthScopes []v1.AuthScope tokenGenerator *clientcredentials.Config } -func NewOidcAuthSetter(baseCfg BaseConfig, cfg OidcClientConfig) *OidcAuthProvider { +func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) *OidcAuthProvider { eps := make(map[string][]string) - for k, v := range cfg.OidcAdditionalEndpointParams { + for k, v := range cfg.AdditionalEndpointParams { eps[k] = []string{v} } - if cfg.OidcAudience != "" { - eps["audience"] = []string{cfg.OidcAudience} + if cfg.Audience != "" { + eps["audience"] = []string{cfg.Audience} } tokenGenerator := &clientcredentials.Config{ - ClientID: cfg.OidcClientID, - ClientSecret: cfg.OidcClientSecret, - Scopes: []string{cfg.OidcScope}, - TokenURL: cfg.OidcTokenEndpointURL, + ClientID: cfg.ClientID, + ClientSecret: cfg.ClientSecret, + Scopes: []string{cfg.Scope}, + TokenURL: cfg.TokenEndpointURL, EndpointParams: eps, } return &OidcAuthProvider{ - BaseConfig: baseCfg, - tokenGenerator: tokenGenerator, + additionalAuthScopes: additionalAuthScopes, + tokenGenerator: tokenGenerator, } } @@ -135,7 +70,7 @@ func (auth *OidcAuthProvider) SetLogin(loginMsg *msg.Login) (err error) { } func (auth *OidcAuthProvider) SetPing(pingMsg *msg.Ping) (err error) { - if !auth.AuthenticateHeartBeats { + if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) { return nil } @@ -144,7 +79,7 @@ func (auth *OidcAuthProvider) SetPing(pingMsg *msg.Ping) (err error) { } func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) { - if !auth.AuthenticateNewWorkConns { + if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) { return nil } @@ -153,26 +88,26 @@ func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (e } type OidcAuthConsumer struct { - BaseConfig + additionalAuthScopes []v1.AuthScope verifier *oidc.IDTokenVerifier subjectFromLogin string } -func NewOidcAuthVerifier(baseCfg BaseConfig, cfg OidcServerConfig) *OidcAuthConsumer { - provider, err := oidc.NewProvider(context.Background(), cfg.OidcIssuer) +func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCServerConfig) *OidcAuthConsumer { + provider, err := oidc.NewProvider(context.Background(), cfg.Issuer) if err != nil { panic(err) } verifierConf := oidc.Config{ - ClientID: cfg.OidcAudience, - SkipClientIDCheck: cfg.OidcAudience == "", - SkipExpiryCheck: cfg.OidcSkipExpiryCheck, - SkipIssuerCheck: cfg.OidcSkipIssuerCheck, + ClientID: cfg.Audience, + SkipClientIDCheck: cfg.Audience == "", + SkipExpiryCheck: cfg.SkipExpiryCheck, + SkipIssuerCheck: cfg.SkipIssuerCheck, } return &OidcAuthConsumer{ - BaseConfig: baseCfg, - verifier: provider.Verifier(&verifierConf), + additionalAuthScopes: additionalAuthScopes, + verifier: provider.Verifier(&verifierConf), } } @@ -200,7 +135,7 @@ func (auth *OidcAuthConsumer) verifyPostLoginToken(privilegeKey string) (err err } func (auth *OidcAuthConsumer) VerifyPing(pingMsg *msg.Ping) (err error) { - if !auth.AuthenticateHeartBeats { + if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) { return nil } @@ -208,7 +143,7 @@ func (auth *OidcAuthConsumer) VerifyPing(pingMsg *msg.Ping) (err error) { } func (auth *OidcAuthConsumer) VerifyNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) { - if !auth.AuthenticateNewWorkConns { + if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) { return nil } diff --git a/pkg/auth/token.go b/pkg/auth/token.go index 9b2e3f7ce02..0b34b05e417 100644 --- a/pkg/auth/token.go +++ b/pkg/auth/token.go @@ -18,43 +18,32 @@ import ( "fmt" "time" + "github.com/samber/lo" + + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/util/util" ) -type TokenConfig struct { - // Token specifies the authorization token used to create keys to be sent - // to the server. The server must have a matching token for authorization - // to succeed. By default, this value is "". - Token string `ini:"token" json:"token"` -} - -func getDefaultTokenConf() TokenConfig { - return TokenConfig{ - Token: "", - } -} - type TokenAuthSetterVerifier struct { - BaseConfig - - token string + additionalAuthScopes []v1.AuthScope + token string } -func NewTokenAuth(baseCfg BaseConfig, cfg TokenConfig) *TokenAuthSetterVerifier { +func NewTokenAuth(additionalAuthScopes []v1.AuthScope, token string) *TokenAuthSetterVerifier { return &TokenAuthSetterVerifier{ - BaseConfig: baseCfg, - token: cfg.Token, + additionalAuthScopes: additionalAuthScopes, + token: token, } } -func (auth *TokenAuthSetterVerifier) SetLogin(loginMsg *msg.Login) (err error) { +func (auth *TokenAuthSetterVerifier) SetLogin(loginMsg *msg.Login) error { loginMsg.PrivilegeKey = util.GetAuthKey(auth.token, loginMsg.Timestamp) return nil } func (auth *TokenAuthSetterVerifier) SetPing(pingMsg *msg.Ping) error { - if !auth.AuthenticateHeartBeats { + if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) { return nil } @@ -64,7 +53,7 @@ func (auth *TokenAuthSetterVerifier) SetPing(pingMsg *msg.Ping) error { } func (auth *TokenAuthSetterVerifier) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) error { - if !auth.AuthenticateNewWorkConns { + if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) { return nil } @@ -81,7 +70,7 @@ func (auth *TokenAuthSetterVerifier) VerifyLogin(m *msg.Login) error { } func (auth *TokenAuthSetterVerifier) VerifyPing(m *msg.Ping) error { - if !auth.AuthenticateHeartBeats { + if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) { return nil } @@ -92,7 +81,7 @@ func (auth *TokenAuthSetterVerifier) VerifyPing(m *msg.Ping) error { } func (auth *TokenAuthSetterVerifier) VerifyNewWorkConn(m *msg.NewWorkConn) error { - if !auth.AuthenticateNewWorkConns { + if !lo.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) { return nil } diff --git a/pkg/config/client_test.go b/pkg/config/client_test.go deleted file mode 100644 index 187324a11a6..00000000000 --- a/pkg/config/client_test.go +++ /dev/null @@ -1,680 +0,0 @@ -// Copyright 2020 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 config - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/fatedier/frp/pkg/auth" - "github.com/fatedier/frp/pkg/consts" -) - -const ( - testUser = "test" -) - -var testClientBytesWithFull = []byte(` - # [common] is integral section - [common] - server_addr = 0.0.0.9 - server_port = 7009 - http_proxy = http://user:passwd@192.168.1.128:8080 - log_file = ./frpc.log9 - log_way = file - log_level = info9 - log_max_days = 39 - disable_log_color = false - authenticate_heartbeats = false - authenticate_new_work_conns = false - token = 12345678 - oidc_client_id = client-id - oidc_client_secret = client-secret - oidc_audience = audience - oidc_token_endpoint_url = endpoint_url - admin_addr = 127.0.0.9 - admin_port = 7409 - admin_user = admin9 - admin_pwd = admin9 - assets_dir = ./static9 - pool_count = 59 - tcp_mux - user = your_name - login_fail_exit - protocol = tcp - tls_enable = true - tls_cert_file = client.crt - tls_key_file = client.key - tls_trusted_ca_file = ca.crt - tls_server_name = example.com - dns_server = 8.8.8.9 - start = ssh,dns - heartbeat_interval = 39 - heartbeat_timeout = 99 - meta_var1 = 123 - meta_var2 = 234 - udp_packet_size = 1509 - - # all proxy - [ssh] - type = tcp - local_ip = 127.0.0.9 - local_port = 29 - bandwidth_limit = 19MB - bandwidth_limit_mode = server - use_encryption - use_compression - remote_port = 6009 - group = test_group - group_key = 123456 - health_check_type = tcp - health_check_timeout_s = 3 - health_check_max_failed = 3 - health_check_interval_s = 19 - meta_var1 = 123 - meta_var2 = 234 - - [ssh_random] - type = tcp - local_ip = 127.0.0.9 - local_port = 29 - remote_port = 9 - - [range:tcp_port] - type = tcp - local_ip = 127.0.0.9 - local_port = 6010-6011,6019 - remote_port = 6010-6011,6019 - use_encryption = false - use_compression = false - - [dns] - type = udp - local_ip = 114.114.114.114 - local_port = 59 - remote_port = 6009 - use_encryption - use_compression - - [range:udp_port] - type = udp - local_ip = 114.114.114.114 - local_port = 6000,6010-6011 - remote_port = 6000,6010-6011 - use_encryption - use_compression - - [web01] - type = http - local_ip = 127.0.0.9 - local_port = 89 - use_encryption - use_compression - http_user = admin - http_pwd = admin - subdomain = web01 - custom_domains = web02.yourdomain.com - locations = /,/pic - host_header_rewrite = example.com - header_X-From-Where = frp - health_check_type = http - health_check_url = /status - health_check_interval_s = 19 - health_check_max_failed = 3 - health_check_timeout_s = 3 - - [web02] - type = https - local_ip = 127.0.0.9 - local_port = 8009 - use_encryption - use_compression - subdomain = web01 - custom_domains = web02.yourdomain.com - proxy_protocol_version = v2 - - [secret_tcp] - type = stcp - sk = abcdefg - local_ip = 127.0.0.1 - local_port = 22 - use_encryption = false - use_compression = false - - [p2p_tcp] - type = xtcp - sk = abcdefg - local_ip = 127.0.0.1 - local_port = 22 - use_encryption = false - use_compression = false - - [tcpmuxhttpconnect] - type = tcpmux - multiplexer = httpconnect - local_ip = 127.0.0.1 - local_port = 10701 - custom_domains = tunnel1 - - [plugin_unix_domain_socket] - type = tcp - remote_port = 6003 - plugin = unix_domain_socket - plugin_unix_path = /var/run/docker.sock - - [plugin_http_proxy] - type = tcp - remote_port = 6004 - plugin = http_proxy - plugin_http_user = abc - plugin_http_passwd = abc - - [plugin_socks5] - type = tcp - remote_port = 6005 - plugin = socks5 - plugin_user = abc - plugin_passwd = abc - - [plugin_static_file] - type = tcp - remote_port = 6006 - plugin = static_file - plugin_local_path = /var/www/blog - plugin_strip_prefix = static - plugin_http_user = abc - plugin_http_passwd = abc - - [plugin_https2http] - type = https - custom_domains = test.yourdomain.com - plugin = https2http - plugin_local_addr = 127.0.0.1:80 - plugin_crt_path = ./server.crt - plugin_key_path = ./server.key - plugin_host_header_rewrite = 127.0.0.1 - plugin_header_X-From-Where = frp - - [plugin_http2https] - type = http - custom_domains = test.yourdomain.com - plugin = http2https - plugin_local_addr = 127.0.0.1:443 - plugin_host_header_rewrite = 127.0.0.1 - plugin_header_X-From-Where = frp - - # visitor - [secret_tcp_visitor] - role = visitor - type = stcp - server_name = secret_tcp - sk = abcdefg - bind_addr = 127.0.0.1 - bind_port = 9000 - use_encryption = false - use_compression = false - - [p2p_tcp_visitor] - role = visitor - type = xtcp - server_name = p2p_tcp - sk = abcdefg - bind_addr = 127.0.0.1 - bind_port = 9001 - use_encryption = false - use_compression = false - `) - -func Test_LoadClientCommonConf(t *testing.T) { - assert := assert.New(t) - - expected := ClientCommonConf{ - ClientConfig: auth.ClientConfig{ - BaseConfig: auth.BaseConfig{ - AuthenticationMethod: "token", - AuthenticateHeartBeats: false, - AuthenticateNewWorkConns: false, - }, - TokenConfig: auth.TokenConfig{ - Token: "12345678", - }, - OidcClientConfig: auth.OidcClientConfig{ - OidcClientID: "client-id", - OidcClientSecret: "client-secret", - OidcAudience: "audience", - OidcTokenEndpointURL: "endpoint_url", - }, - }, - ServerAddr: "0.0.0.9", - ServerPort: 7009, - NatHoleSTUNServer: "stun.easyvoip.com:3478", - DialServerTimeout: 10, - DialServerKeepAlive: 7200, - HTTPProxy: "http://user:passwd@192.168.1.128:8080", - LogFile: "./frpc.log9", - LogWay: "file", - LogLevel: "info9", - LogMaxDays: 39, - DisableLogColor: false, - AdminAddr: "127.0.0.9", - AdminPort: 7409, - AdminUser: "admin9", - AdminPwd: "admin9", - AssetsDir: "./static9", - PoolCount: 59, - TCPMux: true, - TCPMuxKeepaliveInterval: 60, - User: "your_name", - LoginFailExit: true, - Protocol: "tcp", - QUICKeepalivePeriod: 10, - QUICMaxIdleTimeout: 30, - QUICMaxIncomingStreams: 100000, - TLSEnable: true, - TLSCertFile: "client.crt", - TLSKeyFile: "client.key", - TLSTrustedCaFile: "ca.crt", - TLSServerName: "example.com", - DisableCustomTLSFirstByte: true, - DNSServer: "8.8.8.9", - Start: []string{"ssh", "dns"}, - HeartbeatInterval: 39, - HeartbeatTimeout: 99, - Metas: map[string]string{ - "var1": "123", - "var2": "234", - }, - UDPPacketSize: 1509, - IncludeConfigFiles: []string{}, - } - - common, err := UnmarshalClientConfFromIni(testClientBytesWithFull) - assert.NoError(err) - assert.EqualValues(expected, common) -} - -func Test_LoadClientBasicConf(t *testing.T) { - assert := assert.New(t) - - proxyExpected := map[string]ProxyConf{ - testUser + ".ssh": &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".ssh", - ProxyType: consts.TCPProxy, - UseCompression: true, - UseEncryption: true, - Group: "test_group", - GroupKey: "123456", - BandwidthLimit: MustBandwidthQuantity("19MB"), - BandwidthLimitMode: BandwidthLimitModeServer, - Metas: map[string]string{ - "var1": "123", - "var2": "234", - }, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 29, - }, - HealthCheckConf: HealthCheckConf{ - HealthCheckType: consts.TCPProxy, - HealthCheckTimeoutS: 3, - HealthCheckMaxFailed: 3, - HealthCheckIntervalS: 19, - HealthCheckAddr: "127.0.0.9:29", - }, - }, - RemotePort: 6009, - }, - testUser + ".ssh_random": &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".ssh_random", - ProxyType: consts.TCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 29, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - RemotePort: 9, - }, - testUser + ".tcp_port_0": &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".tcp_port_0", - ProxyType: consts.TCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 6010, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - RemotePort: 6010, - }, - testUser + ".tcp_port_1": &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".tcp_port_1", - ProxyType: consts.TCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 6011, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - RemotePort: 6011, - }, - testUser + ".tcp_port_2": &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".tcp_port_2", - ProxyType: consts.TCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 6019, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - RemotePort: 6019, - }, - testUser + ".dns": &UDPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".dns", - ProxyType: consts.UDPProxy, - UseEncryption: true, - UseCompression: true, - LocalSvrConf: LocalSvrConf{ - LocalIP: "114.114.114.114", - LocalPort: 59, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - RemotePort: 6009, - }, - testUser + ".udp_port_0": &UDPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".udp_port_0", - ProxyType: consts.UDPProxy, - UseEncryption: true, - UseCompression: true, - LocalSvrConf: LocalSvrConf{ - LocalIP: "114.114.114.114", - LocalPort: 6000, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - RemotePort: 6000, - }, - testUser + ".udp_port_1": &UDPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".udp_port_1", - ProxyType: consts.UDPProxy, - UseEncryption: true, - UseCompression: true, - LocalSvrConf: LocalSvrConf{ - LocalIP: "114.114.114.114", - LocalPort: 6010, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - RemotePort: 6010, - }, - testUser + ".udp_port_2": &UDPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".udp_port_2", - ProxyType: consts.UDPProxy, - UseEncryption: true, - UseCompression: true, - LocalSvrConf: LocalSvrConf{ - LocalIP: "114.114.114.114", - LocalPort: 6011, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - RemotePort: 6011, - }, - testUser + ".web01": &HTTPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".web01", - ProxyType: consts.HTTPProxy, - UseCompression: true, - UseEncryption: true, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 89, - }, - HealthCheckConf: HealthCheckConf{ - HealthCheckType: consts.HTTPProxy, - HealthCheckTimeoutS: 3, - HealthCheckMaxFailed: 3, - HealthCheckIntervalS: 19, - HealthCheckURL: "http://127.0.0.9:89/status", - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - DomainConf: DomainConf{ - CustomDomains: []string{"web02.yourdomain.com"}, - SubDomain: "web01", - }, - Locations: []string{"/", "/pic"}, - HTTPUser: "admin", - HTTPPwd: "admin", - HostHeaderRewrite: "example.com", - Headers: map[string]string{ - "X-From-Where": "frp", - }, - }, - testUser + ".web02": &HTTPSProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".web02", - ProxyType: consts.HTTPSProxy, - UseCompression: true, - UseEncryption: true, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 8009, - }, - ProxyProtocolVersion: "v2", - BandwidthLimitMode: BandwidthLimitModeClient, - }, - DomainConf: DomainConf{ - CustomDomains: []string{"web02.yourdomain.com"}, - SubDomain: "web01", - }, - }, - testUser + ".secret_tcp": &STCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".secret_tcp", - ProxyType: consts.STCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.1", - LocalPort: 22, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - RoleServerCommonConf: RoleServerCommonConf{ - Role: "server", - Sk: "abcdefg", - }, - }, - testUser + ".p2p_tcp": &XTCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".p2p_tcp", - ProxyType: consts.XTCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.1", - LocalPort: 22, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - RoleServerCommonConf: RoleServerCommonConf{ - Role: "server", - Sk: "abcdefg", - }, - }, - testUser + ".tcpmuxhttpconnect": &TCPMuxProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".tcpmuxhttpconnect", - ProxyType: consts.TCPMuxProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.1", - LocalPort: 10701, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - DomainConf: DomainConf{ - CustomDomains: []string{"tunnel1"}, - SubDomain: "", - }, - Multiplexer: "httpconnect", - }, - testUser + ".plugin_unix_domain_socket": &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".plugin_unix_domain_socket", - ProxyType: consts.TCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.1", - Plugin: "unix_domain_socket", - PluginParams: map[string]string{ - "plugin_unix_path": "/var/run/docker.sock", - }, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - RemotePort: 6003, - }, - testUser + ".plugin_http_proxy": &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".plugin_http_proxy", - ProxyType: consts.TCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.1", - Plugin: "http_proxy", - PluginParams: map[string]string{ - "plugin_http_user": "abc", - "plugin_http_passwd": "abc", - }, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - RemotePort: 6004, - }, - testUser + ".plugin_socks5": &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".plugin_socks5", - ProxyType: consts.TCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.1", - Plugin: "socks5", - PluginParams: map[string]string{ - "plugin_user": "abc", - "plugin_passwd": "abc", - }, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - RemotePort: 6005, - }, - testUser + ".plugin_static_file": &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".plugin_static_file", - ProxyType: consts.TCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.1", - Plugin: "static_file", - PluginParams: map[string]string{ - "plugin_local_path": "/var/www/blog", - "plugin_strip_prefix": "static", - "plugin_http_user": "abc", - "plugin_http_passwd": "abc", - }, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - RemotePort: 6006, - }, - testUser + ".plugin_https2http": &HTTPSProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".plugin_https2http", - ProxyType: consts.HTTPSProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.1", - Plugin: "https2http", - PluginParams: map[string]string{ - "plugin_local_addr": "127.0.0.1:80", - "plugin_crt_path": "./server.crt", - "plugin_key_path": "./server.key", - "plugin_host_header_rewrite": "127.0.0.1", - "plugin_header_X-From-Where": "frp", - }, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - DomainConf: DomainConf{ - CustomDomains: []string{"test.yourdomain.com"}, - }, - }, - testUser + ".plugin_http2https": &HTTPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testUser + ".plugin_http2https", - ProxyType: consts.HTTPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.1", - Plugin: "http2https", - PluginParams: map[string]string{ - "plugin_local_addr": "127.0.0.1:443", - "plugin_host_header_rewrite": "127.0.0.1", - "plugin_header_X-From-Where": "frp", - }, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - DomainConf: DomainConf{ - CustomDomains: []string{"test.yourdomain.com"}, - }, - }, - } - - visitorExpected := map[string]VisitorConf{ - testUser + ".secret_tcp_visitor": &STCPVisitorConf{ - BaseVisitorConf: BaseVisitorConf{ - ProxyName: testUser + ".secret_tcp_visitor", - ProxyType: consts.STCPProxy, - Role: "visitor", - Sk: "abcdefg", - ServerName: testVisitorPrefix + "secret_tcp", - BindAddr: "127.0.0.1", - BindPort: 9000, - }, - }, - testUser + ".p2p_tcp_visitor": &XTCPVisitorConf{ - BaseVisitorConf: BaseVisitorConf{ - ProxyName: testUser + ".p2p_tcp_visitor", - ProxyType: consts.XTCPProxy, - Role: "visitor", - Sk: "abcdefg", - ServerName: testProxyPrefix + "p2p_tcp", - BindAddr: "127.0.0.1", - BindPort: 9001, - }, - Protocol: "quic", - MaxRetriesAnHour: 8, - MinRetryInterval: 90, - FallbackTimeoutMs: 1000, - }, - } - - proxyActual, visitorActual, err := LoadAllProxyConfsFromIni(testUser, testClientBytesWithFull, nil) - assert.NoError(err) - assert.Equal(proxyExpected, proxyActual) - assert.Equal(visitorExpected, visitorActual) -} diff --git a/pkg/config/README.md b/pkg/config/legacy/README.md similarity index 100% rename from pkg/config/README.md rename to pkg/config/legacy/README.md diff --git a/pkg/config/client.go b/pkg/config/legacy/client.go similarity index 96% rename from pkg/config/client.go rename to pkg/config/legacy/client.go index 029470dc1de..f7257cb55f2 100644 --- a/pkg/config/client.go +++ b/pkg/config/legacy/client.go @@ -1,4 +1,4 @@ -// Copyright 2020 The frp Authors +// Copyright 2023 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. @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package config +package legacy import ( "fmt" @@ -23,15 +23,16 @@ import ( "github.com/samber/lo" "gopkg.in/ini.v1" - "github.com/fatedier/frp/pkg/auth" + legacyauth "github.com/fatedier/frp/pkg/auth/legacy" "github.com/fatedier/frp/pkg/util/util" ) -// ClientCommonConf contains information for a client service. It is +// ClientCommonConf is the configuration parsed from ini. +// It contains information for a client service. It is // recommended to use GetDefaultClientConf instead of creating this object // directly, so that all unspecified fields have reasonable default values. type ClientCommonConf struct { - auth.ClientConfig `ini:",extends"` + legacyauth.ClientConfig `ini:",extends"` // ServerAddr specifies the address of the server to connect to. By // default, this value is "0.0.0.0". @@ -122,9 +123,9 @@ type ClientCommonConf struct { // is "tcp". Protocol string `ini:"protocol" json:"protocol"` // QUIC protocol options - QUICKeepalivePeriod int `ini:"quic_keepalive_period" json:"quic_keepalive_period" validate:"gte=0"` - QUICMaxIdleTimeout int `ini:"quic_max_idle_timeout" json:"quic_max_idle_timeout" validate:"gte=0"` - QUICMaxIncomingStreams int `ini:"quic_max_incoming_streams" json:"quic_max_incoming_streams" validate:"gte=0"` + QUICKeepalivePeriod int `ini:"quic_keepalive_period" json:"quic_keepalive_period"` + QUICMaxIdleTimeout int `ini:"quic_max_idle_timeout" json:"quic_max_idle_timeout"` + QUICMaxIncomingStreams int `ini:"quic_max_incoming_streams" json:"quic_max_incoming_streams"` // TLSEnable specifies whether or not TLS should be used when communicating // with the server. If "tls_cert_file" and "tls_key_file" are valid, // client will load the supplied tls configuration. @@ -168,85 +169,6 @@ type ClientCommonConf struct { PprofEnable bool `ini:"pprof_enable" json:"pprof_enable"` } -// GetDefaultClientConf returns a client configuration with default values. -func GetDefaultClientConf() ClientCommonConf { - return ClientCommonConf{ - ClientConfig: auth.GetDefaultClientConf(), - ServerAddr: "0.0.0.0", - ServerPort: 7000, - NatHoleSTUNServer: "stun.easyvoip.com:3478", - DialServerTimeout: 10, - DialServerKeepAlive: 7200, - HTTPProxy: os.Getenv("http_proxy"), - LogFile: "console", - LogWay: "console", - LogLevel: "info", - LogMaxDays: 3, - AdminAddr: "127.0.0.1", - PoolCount: 1, - TCPMux: true, - TCPMuxKeepaliveInterval: 60, - LoginFailExit: true, - Start: make([]string, 0), - Protocol: "tcp", - QUICKeepalivePeriod: 10, - QUICMaxIdleTimeout: 30, - QUICMaxIncomingStreams: 100000, - TLSEnable: true, - DisableCustomTLSFirstByte: true, - HeartbeatInterval: 30, - HeartbeatTimeout: 90, - Metas: make(map[string]string), - UDPPacketSize: 1500, - IncludeConfigFiles: make([]string, 0), - } -} - -func (cfg *ClientCommonConf) Complete() { - if cfg.LogFile == "console" { - cfg.LogWay = "console" - } else { - cfg.LogWay = "file" - } -} - -func (cfg *ClientCommonConf) Validate() error { - if cfg.HeartbeatTimeout > 0 && cfg.HeartbeatInterval > 0 { - if cfg.HeartbeatTimeout < cfg.HeartbeatInterval { - return fmt.Errorf("invalid heartbeat_timeout, heartbeat_timeout is less than heartbeat_interval") - } - } - - if !cfg.TLSEnable { - if cfg.TLSCertFile != "" { - fmt.Println("WARNING! tls_cert_file is invalid when tls_enable is false") - } - - if cfg.TLSKeyFile != "" { - fmt.Println("WARNING! tls_key_file is invalid when tls_enable is false") - } - - if cfg.TLSTrustedCaFile != "" { - fmt.Println("WARNING! tls_trusted_ca_file is invalid when tls_enable is false") - } - } - - if !lo.Contains([]string{"tcp", "kcp", "quic", "websocket", "wss"}, cfg.Protocol) { - return fmt.Errorf("invalid protocol") - } - - for _, f := range cfg.IncludeConfigFiles { - absDir, err := filepath.Abs(filepath.Dir(f)) - if err != nil { - return fmt.Errorf("include: parse directory of %s failed: %v", f, absDir) - } - if _, err := os.Stat(absDir); os.IsNotExist(err) { - return fmt.Errorf("include: directory of %s not exist", f) - } - } - return nil -} - // Supported sources including: string(file path), []byte, Reader interface. func UnmarshalClientConfFromIni(source interface{}) (ClientCommonConf, error) { f, err := ini.LoadSources(ini.LoadOptions{ @@ -421,3 +343,74 @@ func copySection(source, target *ini.Section) { _, _ = target.NewKey(key, value) } } + +// GetDefaultClientConf returns a client configuration with default values. +func GetDefaultClientConf() ClientCommonConf { + return ClientCommonConf{ + ClientConfig: legacyauth.GetDefaultClientConf(), + ServerAddr: "0.0.0.0", + ServerPort: 7000, + NatHoleSTUNServer: "stun.easyvoip.com:3478", + DialServerTimeout: 10, + DialServerKeepAlive: 7200, + HTTPProxy: os.Getenv("http_proxy"), + LogFile: "console", + LogWay: "console", + LogLevel: "info", + LogMaxDays: 3, + AdminAddr: "127.0.0.1", + PoolCount: 1, + TCPMux: true, + TCPMuxKeepaliveInterval: 60, + LoginFailExit: true, + Start: make([]string, 0), + Protocol: "tcp", + QUICKeepalivePeriod: 10, + QUICMaxIdleTimeout: 30, + QUICMaxIncomingStreams: 100000, + TLSEnable: true, + DisableCustomTLSFirstByte: true, + HeartbeatInterval: 30, + HeartbeatTimeout: 90, + Metas: make(map[string]string), + UDPPacketSize: 1500, + IncludeConfigFiles: make([]string, 0), + } +} + +func (cfg *ClientCommonConf) Validate() error { + if cfg.HeartbeatTimeout > 0 && cfg.HeartbeatInterval > 0 { + if cfg.HeartbeatTimeout < cfg.HeartbeatInterval { + return fmt.Errorf("invalid heartbeat_timeout, heartbeat_timeout is less than heartbeat_interval") + } + } + + if !cfg.TLSEnable { + if cfg.TLSCertFile != "" { + fmt.Println("WARNING! tls_cert_file is invalid when tls_enable is false") + } + + if cfg.TLSKeyFile != "" { + fmt.Println("WARNING! tls_key_file is invalid when tls_enable is false") + } + + if cfg.TLSTrustedCaFile != "" { + fmt.Println("WARNING! tls_trusted_ca_file is invalid when tls_enable is false") + } + } + + if !lo.Contains([]string{"tcp", "kcp", "quic", "websocket", "wss"}, cfg.Protocol) { + return fmt.Errorf("invalid protocol") + } + + for _, f := range cfg.IncludeConfigFiles { + absDir, err := filepath.Abs(filepath.Dir(f)) + if err != nil { + return fmt.Errorf("include: parse directory of %s failed: %v", f, err) + } + if _, err := os.Stat(absDir); os.IsNotExist(err) { + return fmt.Errorf("include: directory of %s not exist", f) + } + } + return nil +} diff --git a/pkg/config/legacy/conversion.go b/pkg/config/legacy/conversion.go new file mode 100644 index 00000000000..189cd73dbdb --- /dev/null +++ b/pkg/config/legacy/conversion.go @@ -0,0 +1,350 @@ +// Copyright 2023 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 legacy + +import ( + "strings" + + "github.com/samber/lo" + + "github.com/fatedier/frp/pkg/config/types" + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func Convert_ClientCommonConf_To_v1(conf *ClientCommonConf) *v1.ClientCommonConfig { + out := &v1.ClientCommonConfig{} + out.User = conf.User + out.Auth.Method = v1.AuthMethod(conf.ClientConfig.AuthenticationMethod) + out.Auth.Token = conf.ClientConfig.Token + if conf.ClientConfig.AuthenticateHeartBeats { + out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeHeartBeats) + } + if conf.ClientConfig.AuthenticateNewWorkConns { + out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeNewWorkConns) + } + out.Auth.OIDC.ClientID = conf.ClientConfig.OidcClientID + out.Auth.OIDC.ClientSecret = conf.ClientConfig.OidcClientSecret + out.Auth.OIDC.Audience = conf.ClientConfig.OidcAudience + out.Auth.OIDC.Scope = conf.ClientConfig.OidcScope + out.Auth.OIDC.TokenEndpointURL = conf.ClientConfig.OidcTokenEndpointURL + out.Auth.OIDC.AdditionalEndpointParams = conf.ClientConfig.OidcAdditionalEndpointParams + + out.ServerAddr = conf.ServerAddr + out.ServerPort = conf.ServerPort + out.NatHoleSTUNServer = conf.NatHoleSTUNServer + out.Transport.DialServerTimeout = conf.DialServerTimeout + out.Transport.DialServerKeepAlive = conf.DialServerKeepAlive + out.Transport.ConnectServerLocalIP = conf.ConnectServerLocalIP + out.Transport.ProxyURL = conf.HTTPProxy + out.Transport.PoolCount = conf.PoolCount + out.Transport.TCPMux = lo.ToPtr(conf.TCPMux) + out.Transport.TCPMuxKeepaliveInterval = conf.TCPMuxKeepaliveInterval + out.Transport.Protocol = conf.Protocol + out.Transport.HeartbeatInterval = conf.HeartbeatInterval + out.Transport.HeartbeatTimeout = conf.HeartbeatTimeout + out.Transport.QUIC.KeepalivePeriod = conf.QUICKeepalivePeriod + out.Transport.QUIC.MaxIdleTimeout = conf.QUICMaxIdleTimeout + out.Transport.QUIC.MaxIncomingStreams = conf.QUICMaxIncomingStreams + out.Transport.TLS.Enable = lo.ToPtr(conf.TLSEnable) + out.Transport.TLS.DisableCustomTLSFirstByte = lo.ToPtr(conf.DisableCustomTLSFirstByte) + out.Transport.TLS.TLSConfig.CertFile = conf.TLSCertFile + out.Transport.TLS.TLSConfig.KeyFile = conf.TLSKeyFile + out.Transport.TLS.TLSConfig.TrustedCaFile = conf.TLSTrustedCaFile + out.Transport.TLS.TLSConfig.ServerName = conf.TLSServerName + + out.Log.To = conf.LogFile + out.Log.Level = conf.LogLevel + out.Log.MaxDays = conf.LogMaxDays + out.Log.DisablePrintColor = conf.DisableLogColor + + out.WebServer.Addr = conf.AdminAddr + out.WebServer.Port = conf.AdminPort + out.WebServer.Password = conf.AdminPwd + out.WebServer.AssetsDir = conf.AssetsDir + out.WebServer.PprofEnable = conf.PprofEnable + + out.DNSServer = conf.DNSServer + out.LoginFailExit = lo.ToPtr(conf.LoginFailExit) + out.Start = conf.Start + out.UDPPacketSize = conf.UDPPacketSize + out.Metadatas = conf.Metas + out.IncludeConfigFiles = conf.IncludeConfigFiles + return out +} + +func Convert_ServerCommonConf_To_v1(conf *ServerCommonConf) *v1.ServerConfig { + out := &v1.ServerConfig{} + out.Auth.Method = v1.AuthMethod(conf.ServerConfig.AuthenticationMethod) + out.Auth.Token = conf.ServerConfig.Token + if conf.ServerConfig.AuthenticateHeartBeats { + out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeHeartBeats) + } + if conf.ServerConfig.AuthenticateNewWorkConns { + out.Auth.AdditionalScopes = append(out.Auth.AdditionalScopes, v1.AuthScopeNewWorkConns) + } + out.Auth.OIDC.Audience = conf.ServerConfig.OidcAudience + out.Auth.OIDC.Issuer = conf.ServerConfig.OidcIssuer + out.Auth.OIDC.SkipExpiryCheck = conf.ServerConfig.OidcSkipExpiryCheck + out.Auth.OIDC.SkipIssuerCheck = conf.ServerConfig.OidcSkipIssuerCheck + + out.BindAddr = conf.BindAddr + out.BindPort = conf.BindPort + out.KCPBindPort = conf.KCPBindPort + out.QUICBindPort = conf.QUICBindPort + out.Transport.QUIC.KeepalivePeriod = conf.QUICKeepalivePeriod + out.Transport.QUIC.MaxIdleTimeout = conf.QUICMaxIdleTimeout + out.Transport.QUIC.MaxIncomingStreams = conf.QUICMaxIncomingStreams + + out.ProxyBindAddr = conf.ProxyBindAddr + out.VhostHTTPPort = conf.VhostHTTPPort + out.VhostHTTPSPort = conf.VhostHTTPSPort + out.TCPMuxHTTPConnectPort = conf.TCPMuxHTTPConnectPort + out.TCPMuxPassthrough = conf.TCPMuxPassthrough + out.VhostHTTPTimeout = conf.VhostHTTPTimeout + + out.WebServer.Addr = conf.DashboardAddr + out.WebServer.Port = conf.DashboardPort + out.WebServer.User = conf.DashboardUser + out.WebServer.Password = conf.DashboardPwd + out.WebServer.AssetsDir = conf.AssetsDir + if conf.DashboardTLSMode { + out.WebServer.TLS = &v1.TLSConfig{} + out.WebServer.TLS.CertFile = conf.DashboardTLSCertFile + out.WebServer.TLS.KeyFile = conf.DashboardTLSKeyFile + out.WebServer.PprofEnable = conf.PprofEnable + } + + out.EnablePrometheus = conf.EnablePrometheus + + out.Log.To = conf.LogFile + out.Log.Level = conf.LogLevel + out.Log.MaxDays = conf.LogMaxDays + out.Log.DisablePrintColor = conf.DisableLogColor + + out.DetailedErrorsToClient = lo.ToPtr(conf.DetailedErrorsToClient) + out.SubDomainHost = conf.SubDomainHost + out.Custom404Page = conf.Custom404Page + out.UserConnTimeout = conf.UserConnTimeout + out.UDPPacketSize = conf.UDPPacketSize + out.NatHoleAnalysisDataReserveHours = conf.NatHoleAnalysisDataReserveHours + + out.Transport.TCPMux = lo.ToPtr(conf.TCPMux) + out.Transport.TCPMuxKeepaliveInterval = conf.TCPMuxKeepaliveInterval + out.Transport.TCPKeepAlive = conf.TCPKeepAlive + out.Transport.MaxPoolCount = conf.MaxPoolCount + out.Transport.HeartbeatTimeout = conf.HeartbeatTimeout + + out.Transport.TLS.Force = conf.TLSOnly + out.Transport.TLS.CertFile = conf.TLSCertFile + out.Transport.TLS.KeyFile = conf.TLSKeyFile + out.Transport.TLS.TrustedCaFile = conf.TLSTrustedCaFile + + out.MaxPortsPerClient = conf.MaxPortsPerClient + + for _, v := range conf.HTTPPlugins { + out.HTTPPlugins = append(out.HTTPPlugins, v1.HTTPPluginOptions{ + Name: v.Name, + Addr: v.Addr, + Path: v.Path, + Ops: v.Ops, + TLSVerify: v.TLSVerify, + }) + } + + out.AllowPorts, _ = types.NewPortsRangeSliceFromString(conf.AllowPortsStr) + return out +} + +func transformHeadersFromPluginParams(params map[string]string) v1.HeaderOperations { + out := v1.HeaderOperations{} + for k, v := range params { + if !strings.HasPrefix(k, "plugin_header_") { + continue + } + if k = strings.TrimPrefix(k, "plugin_header_"); k != "" { + out.Set[k] = v + } + } + return out +} + +func Convert_ProxyConf_To_v1_Base(conf ProxyConf) *v1.ProxyBaseConfig { + out := &v1.ProxyBaseConfig{} + base := conf.GetBaseConfig() + + out.Name = base.ProxyName + out.Type = base.ProxyType + out.Metadatas = base.Metas + + out.Transport.UseEncryption = base.UseEncryption + out.Transport.UseCompression = base.UseCompression + out.Transport.BandwidthLimit = base.BandwidthLimit + out.Transport.BandwidthLimitMode = base.BandwidthLimitMode + out.Transport.ProxyProtocolVersion = base.ProxyProtocolVersion + + out.LoadBalancer.Group = base.Group + out.LoadBalancer.GroupKey = base.GroupKey + + out.HealthCheck.Type = base.HealthCheckType + out.HealthCheck.TimeoutSeconds = base.HealthCheckTimeoutS + out.HealthCheck.MaxFailed = base.HealthCheckMaxFailed + out.HealthCheck.IntervalSeconds = base.HealthCheckIntervalS + out.HealthCheck.Path = base.HealthCheckURL + + out.LocalIP = base.LocalIP + out.LocalPort = base.LocalPort + + switch base.Plugin { + case "http2https": + out.Plugin.ClientPluginOptions = &v1.HTTP2HTTPSPluginOptions{ + LocalAddr: base.PluginParams["plugin_local_addr"], + HostHeaderRewrite: base.PluginParams["plugin_host_header_rewrite"], + RequestHeaders: transformHeadersFromPluginParams(base.PluginParams), + } + case "http_proxy": + out.Plugin.ClientPluginOptions = &v1.HTTPProxyPluginOptions{ + HTTPUser: base.PluginParams["plugin_http_user"], + HTTPPassword: base.PluginParams["plugin_http_passwd"], + } + case "https2http": + out.Plugin.ClientPluginOptions = &v1.HTTPS2HTTPPluginOptions{ + LocalAddr: base.PluginParams["plugin_local_addr"], + HostHeaderRewrite: base.PluginParams["plugin_host_header_rewrite"], + RequestHeaders: transformHeadersFromPluginParams(base.PluginParams), + CrtPath: base.PluginParams["plugin_crt_path"], + KeyPath: base.PluginParams["plugin_key_path"], + } + case "https2https": + out.Plugin.ClientPluginOptions = &v1.HTTPS2HTTPSPluginOptions{ + LocalAddr: base.PluginParams["plugin_local_addr"], + HostHeaderRewrite: base.PluginParams["plugin_host_header_rewrite"], + RequestHeaders: transformHeadersFromPluginParams(base.PluginParams), + CrtPath: base.PluginParams["plugin_crt_path"], + KeyPath: base.PluginParams["plugin_key_path"], + } + case "socks5": + out.Plugin.ClientPluginOptions = &v1.Socks5PluginOptions{ + Username: base.PluginParams["plugin_user"], + Password: base.PluginParams["plugin_passwd"], + } + case "static_file": + out.Plugin.ClientPluginOptions = &v1.StaticFilePluginOptions{ + LocalPath: base.PluginParams["plugin_local_path"], + StripPrefix: base.PluginParams["plugin_strip_prefix"], + HTTPUser: base.PluginParams["plugin_http_user"], + HTTPPassword: base.PluginParams["plugin_http_passwd"], + } + case "unix_domain_socket": + out.Plugin.ClientPluginOptions = &v1.UnixDomainSocketPluginOptions{ + UnixPath: base.PluginParams["plugin_unix_path"], + } + } + out.Plugin.Type = base.Plugin + return out +} + +func Convert_ProxyConf_To_v1(conf ProxyConf) v1.ProxyConfigurer { + outBase := Convert_ProxyConf_To_v1_Base(conf) + var out v1.ProxyConfigurer + switch v := conf.(type) { + case *TCPProxyConf: + c := &v1.TCPProxyConfig{ProxyBaseConfig: *outBase} + c.RemotePort = v.RemotePort + out = c + case *UDPProxyConf: + c := &v1.UDPProxyConfig{ProxyBaseConfig: *outBase} + c.RemotePort = v.RemotePort + out = c + case *HTTPProxyConf: + c := &v1.HTTPProxyConfig{ProxyBaseConfig: *outBase} + c.CustomDomains = v.CustomDomains + c.SubDomain = v.SubDomain + c.Locations = v.Locations + c.HTTPUser = v.HTTPUser + c.HTTPPassword = v.HTTPPwd + c.HostHeaderRewrite = v.HostHeaderRewrite + c.RequestHeaders.Set = v.Headers + c.RouteByHTTPUser = v.RouteByHTTPUser + out = c + case *HTTPSProxyConf: + c := &v1.HTTPSProxyConfig{ProxyBaseConfig: *outBase} + c.CustomDomains = v.CustomDomains + c.SubDomain = v.SubDomain + out = c + case *TCPMuxProxyConf: + c := &v1.TCPMuxProxyConfig{ProxyBaseConfig: *outBase} + c.CustomDomains = v.CustomDomains + c.SubDomain = v.SubDomain + c.HTTPUser = v.HTTPUser + c.HTTPPassword = v.HTTPPwd + c.RouteByHTTPUser = v.RouteByHTTPUser + c.Multiplexer = v.Multiplexer + out = c + case *STCPProxyConf: + c := &v1.STCPProxyConfig{ProxyBaseConfig: *outBase} + c.Secretkey = v.Sk + c.AllowUsers = v.AllowUsers + out = c + case *SUDPProxyConf: + c := &v1.SUDPProxyConfig{ProxyBaseConfig: *outBase} + c.Secretkey = v.Sk + c.AllowUsers = v.AllowUsers + out = c + case *XTCPProxyConf: + c := &v1.XTCPProxyConfig{ProxyBaseConfig: *outBase} + c.Secretkey = v.Sk + c.AllowUsers = v.AllowUsers + } + return out +} + +func Convert_VisitorConf_To_v1_Base(conf VisitorConf) *v1.VisitorBaseConfig { + out := &v1.VisitorBaseConfig{} + base := conf.GetBaseConfig() + + out.Name = base.ProxyName + out.Type = base.ProxyType + out.Transport.UseEncryption = base.UseEncryption + out.Transport.UseCompression = base.UseCompression + out.SecretKey = base.Sk + out.ServerUser = base.ServerUser + out.ServerName = base.ServerName + out.BindAddr = base.BindAddr + out.BindPort = base.BindPort + return out +} + +func Convert_VisitorConf_To_v1(conf VisitorConf) v1.VisitorConfigurer { + outBase := Convert_VisitorConf_To_v1_Base(conf) + var out v1.VisitorConfigurer + switch v := conf.(type) { + case *STCPVisitorConf: + c := &v1.STCPVisitorConfig{VisitorBaseConfig: *outBase} + out = c + case *SUDPVisitorConf: + c := &v1.SUDPVisitorConfig{VisitorBaseConfig: *outBase} + out = c + case *XTCPVisitorConf: + c := &v1.XTCPVisitorConfig{VisitorBaseConfig: *outBase} + c.Protocol = v.Protocol + c.KeepTunnelOpen = v.KeepTunnelOpen + c.MaxRetriesAnHour = v.MaxRetriesAnHour + c.MinRetryInterval = v.MinRetryInterval + c.FallbackTo = v.FallbackTo + c.FallbackTimeoutMs = v.FallbackTimeoutMs + out = c + } + return out +} diff --git a/pkg/config/parse.go b/pkg/config/legacy/parse.go similarity index 98% rename from pkg/config/parse.go rename to pkg/config/legacy/parse.go index 9f4cdb6b74b..637783bc73b 100644 --- a/pkg/config/parse.go +++ b/pkg/config/legacy/parse.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package config +package legacy import ( "bytes" @@ -40,7 +40,6 @@ func ParseClientConfig(filePath string) ( if err != nil { return } - cfg.Complete() if err = cfg.Validate(); err != nil { err = fmt.Errorf("parse config error: %v", err) return diff --git a/pkg/config/legacy/proxy.go b/pkg/config/legacy/proxy.go new file mode 100644 index 00000000000..e6653ee9787 --- /dev/null +++ b/pkg/config/legacy/proxy.go @@ -0,0 +1,387 @@ +// Copyright 2023 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 legacy + +import ( + "fmt" + "reflect" + + "gopkg.in/ini.v1" + + "github.com/fatedier/frp/pkg/config/types" +) + +type ProxyType string + +const ( + ProxyTypeTCP ProxyType = "tcp" + ProxyTypeUDP ProxyType = "udp" + ProxyTypeTCPMUX ProxyType = "tcpmux" + ProxyTypeHTTP ProxyType = "http" + ProxyTypeHTTPS ProxyType = "https" + ProxyTypeSTCP ProxyType = "stcp" + ProxyTypeXTCP ProxyType = "xtcp" + ProxyTypeSUDP ProxyType = "sudp" +) + +// Proxy +var ( + proxyConfTypeMap = map[ProxyType]reflect.Type{ + ProxyTypeTCP: reflect.TypeOf(TCPProxyConf{}), + ProxyTypeUDP: reflect.TypeOf(UDPProxyConf{}), + ProxyTypeTCPMUX: reflect.TypeOf(TCPMuxProxyConf{}), + ProxyTypeHTTP: reflect.TypeOf(HTTPProxyConf{}), + ProxyTypeHTTPS: reflect.TypeOf(HTTPSProxyConf{}), + ProxyTypeSTCP: reflect.TypeOf(STCPProxyConf{}), + ProxyTypeXTCP: reflect.TypeOf(XTCPProxyConf{}), + ProxyTypeSUDP: reflect.TypeOf(SUDPProxyConf{}), + } +) + +type ProxyConf interface { + // GetBaseConfig returns the BaseProxyConf for this config. + GetBaseConfig() *BaseProxyConf + // UnmarshalFromIni unmarshals a ini.Section into this config. This function + // will be called on the frpc side. + UnmarshalFromIni(string, string, *ini.Section) error +} + +func NewConfByType(proxyType ProxyType) ProxyConf { + v, ok := proxyConfTypeMap[proxyType] + if !ok { + return nil + } + cfg := reflect.New(v).Interface().(ProxyConf) + return cfg +} + +// Proxy Conf Loader +// DefaultProxyConf creates a empty ProxyConf object by proxyType. +// If proxyType doesn't exist, return nil. +func DefaultProxyConf(proxyType ProxyType) ProxyConf { + return NewConfByType(proxyType) +} + +// Proxy loaded from ini +func NewProxyConfFromIni(prefix, name string, section *ini.Section) (ProxyConf, error) { + // section.Key: if key not exists, section will set it with default value. + proxyType := ProxyType(section.Key("type").String()) + if proxyType == "" { + proxyType = ProxyTypeTCP + } + + conf := DefaultProxyConf(proxyType) + if conf == nil { + return nil, fmt.Errorf("invalid type [%s]", proxyType) + } + + if err := conf.UnmarshalFromIni(prefix, name, section); err != nil { + return nil, err + } + return conf, nil +} + +// LocalSvrConf configures what location the client will to, or what +// plugin will be used. +type LocalSvrConf struct { + // LocalIP specifies the IP address or host name to to. + LocalIP string `ini:"local_ip" json:"local_ip"` + // LocalPort specifies the port to to. + LocalPort int `ini:"local_port" json:"local_port"` + + // Plugin specifies what plugin should be used for ng. If this value + // is set, the LocalIp and LocalPort values will be ignored. By default, + // this value is "". + Plugin string `ini:"plugin" json:"plugin"` + // PluginParams specify parameters to be passed to the plugin, if one is + // being used. By default, this value is an empty map. + PluginParams map[string]string `ini:"-"` +} + +// HealthCheckConf configures health checking. This can be useful for load +// balancing purposes to detect and remove proxies to failing services. +type HealthCheckConf struct { + // HealthCheckType specifies what protocol to use for health checking. + // Valid values include "tcp", "http", and "". If this value is "", health + // checking will not be performed. By default, this value is "". + // + // If the type is "tcp", a connection will be attempted to the target + // server. If a connection cannot be established, the health check fails. + // + // If the type is "http", a GET request will be made to the endpoint + // specified by HealthCheckURL. If the response is not a 200, the health + // check fails. + HealthCheckType string `ini:"health_check_type" json:"health_check_type"` // tcp | http + // HealthCheckTimeoutS specifies the number of seconds to wait for a health + // check attempt to connect. If the timeout is reached, this counts as a + // health check failure. By default, this value is 3. + HealthCheckTimeoutS int `ini:"health_check_timeout_s" json:"health_check_timeout_s"` + // HealthCheckMaxFailed specifies the number of allowed failures before the + // is stopped. By default, this value is 1. + HealthCheckMaxFailed int `ini:"health_check_max_failed" json:"health_check_max_failed"` + // HealthCheckIntervalS specifies the time in seconds between health + // checks. By default, this value is 10. + HealthCheckIntervalS int `ini:"health_check_interval_s" json:"health_check_interval_s"` + // HealthCheckURL specifies the address to send health checks to if the + // health check type is "http". + HealthCheckURL string `ini:"health_check_url" json:"health_check_url"` + // HealthCheckAddr specifies the address to connect to if the health check + // type is "tcp". + HealthCheckAddr string `ini:"-"` +} + +// BaseProxyConf provides configuration info that is common to all types. +type BaseProxyConf struct { + // ProxyName is the name of this + ProxyName string `ini:"name" json:"name"` + // ProxyType specifies the type of this Valid values include "tcp", + // "udp", "http", "https", "stcp", and "xtcp". By default, this value is + // "tcp". + ProxyType string `ini:"type" json:"type"` + + // UseEncryption controls whether or not communication with the server will + // be encrypted. Encryption is done using the tokens supplied in the server + // and client configuration. By default, this value is false. + UseEncryption bool `ini:"use_encryption" json:"use_encryption"` + // UseCompression controls whether or not communication with the server + // will be compressed. By default, this value is false. + UseCompression bool `ini:"use_compression" json:"use_compression"` + // Group specifies which group the is a part of. The server will use + // this information to load balance proxies in the same group. If the value + // is "", this will not be in a group. By default, this value is "". + Group string `ini:"group" json:"group"` + // GroupKey specifies a group key, which should be the same among proxies + // of the same group. By default, this value is "". + GroupKey string `ini:"group_key" json:"group_key"` + + // ProxyProtocolVersion specifies which protocol version to use. Valid + // values include "v1", "v2", and "". If the value is "", a protocol + // version will be automatically selected. By default, this value is "". + ProxyProtocolVersion string `ini:"proxy_protocol_version" json:"proxy_protocol_version"` + + // BandwidthLimit limit the bandwidth + // 0 means no limit + BandwidthLimit types.BandwidthQuantity `ini:"bandwidth_limit" json:"bandwidth_limit"` + // BandwidthLimitMode specifies whether to limit the bandwidth on the + // client or server side. Valid values include "client" and "server". + // By default, this value is "client". + BandwidthLimitMode string `ini:"bandwidth_limit_mode" json:"bandwidth_limit_mode"` + + // meta info for each proxy + Metas map[string]string `ini:"-" json:"metas"` + + LocalSvrConf `ini:",extends"` + HealthCheckConf `ini:",extends"` +} + +// Base +func (cfg *BaseProxyConf) GetBaseConfig() *BaseProxyConf { + return cfg +} + +// BaseProxyConf apply custom logic changes. +func (cfg *BaseProxyConf) decorate(_ string, name string, section *ini.Section) error { + cfg.ProxyName = name + // metas_xxx + cfg.Metas = GetMapWithoutPrefix(section.KeysHash(), "meta_") + + // bandwidth_limit + if bandwidth, err := section.GetKey("bandwidth_limit"); err == nil { + cfg.BandwidthLimit, err = types.NewBandwidthQuantity(bandwidth.String()) + if err != nil { + return err + } + } + + // plugin_xxx + cfg.LocalSvrConf.PluginParams = GetMapByPrefix(section.KeysHash(), "plugin_") + return nil +} + +type DomainConf struct { + CustomDomains []string `ini:"custom_domains" json:"custom_domains"` + SubDomain string `ini:"subdomain" json:"subdomain"` +} + +type RoleServerCommonConf struct { + Role string `ini:"role" json:"role"` + Sk string `ini:"sk" json:"sk"` + AllowUsers []string `ini:"allow_users" json:"allow_users"` +} + +// HTTP +type HTTPProxyConf struct { + BaseProxyConf `ini:",extends"` + DomainConf `ini:",extends"` + + Locations []string `ini:"locations" json:"locations"` + HTTPUser string `ini:"http_user" json:"http_user"` + HTTPPwd string `ini:"http_pwd" json:"http_pwd"` + HostHeaderRewrite string `ini:"host_header_rewrite" json:"host_header_rewrite"` + Headers map[string]string `ini:"-" json:"headers"` + RouteByHTTPUser string `ini:"route_by_http_user" json:"route_by_http_user"` +} + +func (cfg *HTTPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { + err := preUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return err + } + + // Add custom logic unmarshal if exists + cfg.Headers = GetMapWithoutPrefix(section.KeysHash(), "header_") + return nil +} + +// HTTPS +type HTTPSProxyConf struct { + BaseProxyConf `ini:",extends"` + DomainConf `ini:",extends"` +} + +func (cfg *HTTPSProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { + err := preUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return err + } + + // Add custom logic unmarshal if exists + return nil +} + +// TCP +type TCPProxyConf struct { + BaseProxyConf `ini:",extends"` + RemotePort int `ini:"remote_port" json:"remote_port"` +} + +func (cfg *TCPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { + err := preUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return err + } + + // Add custom logic unmarshal if exists + + return nil +} + +// UDP +type UDPProxyConf struct { + BaseProxyConf `ini:",extends"` + + RemotePort int `ini:"remote_port" json:"remote_port"` +} + +func (cfg *UDPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { + err := preUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return err + } + + // Add custom logic unmarshal if exists + + return nil +} + +// TCPMux +type TCPMuxProxyConf struct { + BaseProxyConf `ini:",extends"` + DomainConf `ini:",extends"` + HTTPUser string `ini:"http_user" json:"http_user,omitempty"` + HTTPPwd string `ini:"http_pwd" json:"http_pwd,omitempty"` + RouteByHTTPUser string `ini:"route_by_http_user" json:"route_by_http_user"` + + Multiplexer string `ini:"multiplexer"` +} + +func (cfg *TCPMuxProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { + err := preUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return err + } + + // Add custom logic unmarshal if exists + + return nil +} + +// STCP +type STCPProxyConf struct { + BaseProxyConf `ini:",extends"` + RoleServerCommonConf `ini:",extends"` +} + +func (cfg *STCPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { + err := preUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return err + } + + // Add custom logic unmarshal if exists + if cfg.Role == "" { + cfg.Role = "server" + } + return nil +} + +// XTCP +type XTCPProxyConf struct { + BaseProxyConf `ini:",extends"` + RoleServerCommonConf `ini:",extends"` +} + +func (cfg *XTCPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { + err := preUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return err + } + + // Add custom logic unmarshal if exists + if cfg.Role == "" { + cfg.Role = "server" + } + return nil +} + +// SUDP +type SUDPProxyConf struct { + BaseProxyConf `ini:",extends"` + RoleServerCommonConf `ini:",extends"` +} + +func (cfg *SUDPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { + err := preUnmarshalFromIni(cfg, prefix, name, section) + if err != nil { + return err + } + + // Add custom logic unmarshal if exists + return nil +} + +func preUnmarshalFromIni(cfg ProxyConf, prefix string, name string, section *ini.Section) error { + err := section.MapTo(cfg) + if err != nil { + return err + } + + err = cfg.GetBaseConfig().decorate(prefix, name, section) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/config/server.go b/pkg/config/legacy/server.go similarity index 84% rename from pkg/config/server.go rename to pkg/config/legacy/server.go index 7d3310f647c..acdb93b40a6 100644 --- a/pkg/config/server.go +++ b/pkg/config/legacy/server.go @@ -1,4 +1,4 @@ -// Copyright 2020 The frp Authors +// Copyright 2023 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. @@ -12,60 +12,64 @@ // See the License for the specific language governing permissions and // limitations under the License. -package config +package legacy import ( - "fmt" "strings" - "github.com/go-playground/validator/v10" "gopkg.in/ini.v1" - "github.com/fatedier/frp/pkg/auth" - plugin "github.com/fatedier/frp/pkg/plugin/server" - "github.com/fatedier/frp/pkg/util/util" + legacyauth "github.com/fatedier/frp/pkg/auth/legacy" ) +type HTTPPluginOptions struct { + Name string `ini:"name"` + Addr string `ini:"addr"` + Path string `ini:"path"` + Ops []string `ini:"ops"` + TLSVerify bool `ini:"tls_verify"` +} + // ServerCommonConf contains information for a server service. It is // recommended to use GetDefaultServerConf instead of creating this object // directly, so that all unspecified fields have reasonable default values. type ServerCommonConf struct { - auth.ServerConfig `ini:",extends"` + legacyauth.ServerConfig `ini:",extends"` // BindAddr specifies the address that the server binds to. By default, // this value is "0.0.0.0". BindAddr string `ini:"bind_addr" json:"bind_addr"` // BindPort specifies the port that the server listens on. By default, this // value is 7000. - BindPort int `ini:"bind_port" json:"bind_port" validate:"gte=0,lte=65535"` + BindPort int `ini:"bind_port" json:"bind_port"` // KCPBindPort specifies the KCP port that the server listens on. If this // value is 0, the server will not listen for KCP connections. By default, // this value is 0. - KCPBindPort int `ini:"kcp_bind_port" json:"kcp_bind_port" validate:"gte=0,lte=65535"` + KCPBindPort int `ini:"kcp_bind_port" json:"kcp_bind_port"` // QUICBindPort specifies the QUIC port that the server listens on. // Set this value to 0 will disable this feature. // By default, the value is 0. - QUICBindPort int `ini:"quic_bind_port" json:"quic_bind_port" validate:"gte=0,lte=65535"` + QUICBindPort int `ini:"quic_bind_port" json:"quic_bind_port"` // QUIC protocol options - QUICKeepalivePeriod int `ini:"quic_keepalive_period" json:"quic_keepalive_period" validate:"gte=0"` - QUICMaxIdleTimeout int `ini:"quic_max_idle_timeout" json:"quic_max_idle_timeout" validate:"gte=0"` - QUICMaxIncomingStreams int `ini:"quic_max_incoming_streams" json:"quic_max_incoming_streams" validate:"gte=0"` + QUICKeepalivePeriod int `ini:"quic_keepalive_period" json:"quic_keepalive_period"` + QUICMaxIdleTimeout int `ini:"quic_max_idle_timeout" json:"quic_max_idle_timeout"` + QUICMaxIncomingStreams int `ini:"quic_max_incoming_streams" json:"quic_max_incoming_streams"` // ProxyBindAddr specifies the address that the proxy binds to. This value // may be the same as BindAddr. ProxyBindAddr string `ini:"proxy_bind_addr" json:"proxy_bind_addr"` // VhostHTTPPort specifies the port that the server listens for HTTP Vhost // requests. If this value is 0, the server will not listen for HTTP // requests. By default, this value is 0. - VhostHTTPPort int `ini:"vhost_http_port" json:"vhost_http_port" validate:"gte=0,lte=65535"` + VhostHTTPPort int `ini:"vhost_http_port" json:"vhost_http_port"` // VhostHTTPSPort specifies the port that the server listens for HTTPS // Vhost requests. If this value is 0, the server will not listen for HTTPS // requests. By default, this value is 0. - VhostHTTPSPort int `ini:"vhost_https_port" json:"vhost_https_port" validate:"gte=0,lte=65535"` + VhostHTTPSPort int `ini:"vhost_https_port" json:"vhost_https_port"` // TCPMuxHTTPConnectPort specifies the port that the server listens for TCP // HTTP CONNECT requests. If the value is 0, the server will not multiplex TCP // requests on one single port. If it's not - it will listen on this value for // HTTP CONNECT requests. By default, this value is 0. - TCPMuxHTTPConnectPort int `ini:"tcpmux_httpconnect_port" json:"tcpmux_httpconnect_port" validate:"gte=0,lte=65535"` + TCPMuxHTTPConnectPort int `ini:"tcpmux_httpconnect_port" json:"tcpmux_httpconnect_port"` // If TCPMuxPassthrough is true, frps won't do any update on traffic. TCPMuxPassthrough bool `ini:"tcpmux_passthrough" json:"tcpmux_passthrough"` // VhostHTTPTimeout specifies the response header timeout for the Vhost @@ -77,7 +81,7 @@ type ServerCommonConf struct { // DashboardPort specifies the port that the dashboard listens on. If this // value is 0, the dashboard will not be started. By default, this value is // 0. - DashboardPort int `ini:"dashboard_port" json:"dashboard_port" validate:"gte=0,lte=65535"` + DashboardPort int `ini:"dashboard_port" json:"dashboard_port"` // DashboardTLSCertFile specifies the path of the cert file that the server will // load. If "dashboard_tls_cert_file", "dashboard_tls_key_file" are valid, the server will use this // supplied tls configuration. @@ -185,7 +189,7 @@ type ServerCommonConf struct { // connection. By default, this value is 10. UserConnTimeout int64 `ini:"user_conn_timeout" json:"user_conn_timeout"` // HTTPPlugins specify the server plugins support HTTP protocol. - HTTPPlugins map[string]plugin.HTTPPluginOptions `ini:"-" json:"http_plugins"` + HTTPPlugins map[string]HTTPPluginOptions `ini:"-" json:"http_plugins"` // UDPPacketSize specifies the UDP packet size // By default, this value is 1500 UDPPacketSize int64 `ini:"udp_packet_size" json:"udp_packet_size"` @@ -200,7 +204,7 @@ type ServerCommonConf struct { // defaults. func GetDefaultServerConf() ServerCommonConf { return ServerCommonConf{ - ServerConfig: auth.GetDefaultServerConf(), + ServerConfig: legacyauth.GetDefaultServerConf(), BindAddr: "0.0.0.0", BindPort: 7000, QUICKeepalivePeriod: 10, @@ -221,7 +225,7 @@ func GetDefaultServerConf() ServerCommonConf { MaxPortsPerClient: 0, HeartbeatTimeout: 90, UserConnTimeout: 10, - HTTPPlugins: make(map[string]plugin.HTTPPluginOptions), + HTTPPlugins: make(map[string]HTTPPluginOptions), UDPPacketSize: 1500, NatHoleAnalysisDataReserveHours: 7 * 24, } @@ -253,18 +257,11 @@ func UnmarshalServerConfFromIni(source interface{}) (ServerCommonConf, error) { // allow_ports allowPortStr := s.Key("allow_ports").String() if allowPortStr != "" { - allowPorts, err := util.ParseRangeNumbers(allowPortStr) - if err != nil { - return ServerCommonConf{}, fmt.Errorf("invalid allow_ports: %v", err) - } - for _, port := range allowPorts { - common.AllowPorts[int(port)] = struct{}{} - } common.AllowPortsStr = allowPortStr } // plugin.xxx - pluginOpts := make(map[string]plugin.HTTPPluginOptions) + pluginOpts := make(map[string]HTTPPluginOptions) for _, section := range f.Sections() { name := section.Name() if !strings.HasPrefix(name, "plugin.") { @@ -283,47 +280,10 @@ func UnmarshalServerConfFromIni(source interface{}) (ServerCommonConf, error) { return common, nil } -func (cfg *ServerCommonConf) Complete() { - if cfg.LogFile == "console" { - cfg.LogWay = "console" - } else { - cfg.LogWay = "file" - } - - if cfg.ProxyBindAddr == "" { - cfg.ProxyBindAddr = cfg.BindAddr - } - - if cfg.TLSTrustedCaFile != "" { - cfg.TLSOnly = true - } -} - -func (cfg *ServerCommonConf) Validate() error { - if !cfg.DashboardTLSMode { - if cfg.DashboardTLSCertFile != "" { - fmt.Println("WARNING! dashboard_tls_cert_file is invalid when dashboard_tls_mode is false") - } - - if cfg.DashboardTLSKeyFile != "" { - fmt.Println("WARNING! dashboard_tls_key_file is invalid when dashboard_tls_mode is false") - } - } else { - if cfg.DashboardTLSCertFile == "" { - return fmt.Errorf("ERROR! dashboard_tls_cert_file must be specified when dashboard_tls_mode is true") - } - - if cfg.DashboardTLSKeyFile == "" { - return fmt.Errorf("ERROR! dashboard_tls_cert_file must be specified when dashboard_tls_mode is true") - } - } - return validator.New().Struct(cfg) -} - -func loadHTTPPluginOpt(section *ini.Section) (*plugin.HTTPPluginOptions, error) { +func loadHTTPPluginOpt(section *ini.Section) (*HTTPPluginOptions, error) { name := strings.TrimSpace(strings.TrimPrefix(section.Name(), "plugin.")) - opt := new(plugin.HTTPPluginOptions) + opt := &HTTPPluginOptions{} err := section.MapTo(opt) if err != nil { return nil, err diff --git a/pkg/config/utils.go b/pkg/config/legacy/utils.go similarity index 98% rename from pkg/config/utils.go rename to pkg/config/legacy/utils.go index aef674d4308..9366f8c76c1 100644 --- a/pkg/config/utils.go +++ b/pkg/config/legacy/utils.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package config +package legacy import ( "strings" diff --git a/pkg/config/value.go b/pkg/config/legacy/value.go similarity index 99% rename from pkg/config/value.go rename to pkg/config/legacy/value.go index 2f228823376..ecf805c9912 100644 --- a/pkg/config/value.go +++ b/pkg/config/legacy/value.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package config +package legacy import ( "bytes" diff --git a/pkg/config/visitor.go b/pkg/config/legacy/visitor.go similarity index 69% rename from pkg/config/visitor.go rename to pkg/config/legacy/visitor.go index 31a8a02bd4f..031c29bfa9a 100644 --- a/pkg/config/visitor.go +++ b/pkg/config/legacy/visitor.go @@ -1,4 +1,4 @@ -// Copyright 2018 fatedier, fatedier@gmail.com +// Copyright 2023 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. @@ -12,24 +12,29 @@ // See the License for the specific language governing permissions and // limitations under the License. -package config +package legacy import ( "fmt" "reflect" - "github.com/samber/lo" "gopkg.in/ini.v1" +) + +type VisitorType string - "github.com/fatedier/frp/pkg/consts" +const ( + VisitorTypeSTCP VisitorType = "stcp" + VisitorTypeXTCP VisitorType = "xtcp" + VisitorTypeSUDP VisitorType = "sudp" ) // Visitor var ( - visitorConfTypeMap = map[string]reflect.Type{ - consts.STCPProxy: reflect.TypeOf(STCPVisitorConf{}), - consts.XTCPProxy: reflect.TypeOf(XTCPVisitorConf{}), - consts.SUDPProxy: reflect.TypeOf(SUDPVisitorConf{}), + visitorConfTypeMap = map[VisitorType]reflect.Type{ + VisitorTypeSTCP: reflect.TypeOf(STCPVisitorConf{}), + VisitorTypeXTCP: reflect.TypeOf(XTCPVisitorConf{}), + VisitorTypeSUDP: reflect.TypeOf(SUDPVisitorConf{}), } ) @@ -38,8 +43,16 @@ type VisitorConf interface { GetBaseConfig() *BaseVisitorConf // UnmarshalFromIni unmarshals config from ini. UnmarshalFromIni(prefix string, name string, section *ini.Section) error - // Validate validates config. - Validate() error +} + +// DefaultVisitorConf creates a empty VisitorConf object by visitorType. +// If visitorType doesn't exist, return nil. +func DefaultVisitorConf(visitorType VisitorType) VisitorConf { + v, ok := visitorConfTypeMap[visitorType] + if !ok { + return nil + } + return reflect.New(v).Interface().(VisitorConf) } type BaseVisitorConf struct { @@ -59,96 +72,14 @@ type BaseVisitorConf struct { BindPort int `ini:"bind_port" json:"bind_port"` } -type SUDPVisitorConf struct { - BaseVisitorConf `ini:",extends"` -} - -type STCPVisitorConf struct { - BaseVisitorConf `ini:",extends"` -} - -type XTCPVisitorConf struct { - BaseVisitorConf `ini:",extends"` - - Protocol string `ini:"protocol" json:"protocol,omitempty"` - KeepTunnelOpen bool `ini:"keep_tunnel_open" json:"keep_tunnel_open,omitempty"` - MaxRetriesAnHour int `ini:"max_retries_an_hour" json:"max_retries_an_hour,omitempty"` - MinRetryInterval int `ini:"min_retry_interval" json:"min_retry_interval,omitempty"` - FallbackTo string `ini:"fallback_to" json:"fallback_to,omitempty"` - FallbackTimeoutMs int `ini:"fallback_timeout_ms" json:"fallback_timeout_ms,omitempty"` -} - -// DefaultVisitorConf creates a empty VisitorConf object by visitorType. -// If visitorType doesn't exist, return nil. -func DefaultVisitorConf(visitorType string) VisitorConf { - v, ok := visitorConfTypeMap[visitorType] - if !ok { - return nil - } - return reflect.New(v).Interface().(VisitorConf) -} - -// Visitor loaded from ini -func NewVisitorConfFromIni(prefix string, name string, section *ini.Section) (VisitorConf, error) { - // section.Key: if key not exists, section will set it with default value. - visitorType := section.Key("type").String() - - if visitorType == "" { - return nil, fmt.Errorf("type shouldn't be empty") - } - - conf := DefaultVisitorConf(visitorType) - if conf == nil { - return nil, fmt.Errorf("type [%s] error", visitorType) - } - - if err := conf.UnmarshalFromIni(prefix, name, section); err != nil { - return nil, fmt.Errorf("type [%s] error", visitorType) - } - - if err := conf.Validate(); err != nil { - return nil, err - } - - return conf, nil -} - // Base func (cfg *BaseVisitorConf) GetBaseConfig() *BaseVisitorConf { return cfg } -func (cfg *BaseVisitorConf) validate() (err error) { - if cfg.Role != "visitor" { - err = fmt.Errorf("invalid role") - return - } - if cfg.BindAddr == "" { - err = fmt.Errorf("bind_addr shouldn't be empty") - return - } - // BindPort can be less than 0, it means don't bind to the port and only receive connections redirected from - // other visitors - if cfg.BindPort == 0 { - err = fmt.Errorf("bind_port is required") - return - } - return -} - -func (cfg *BaseVisitorConf) unmarshalFromIni(prefix string, name string, section *ini.Section) error { - _ = section - +func (cfg *BaseVisitorConf) unmarshalFromIni(_ string, name string, _ *ini.Section) error { // Custom decoration after basic unmarshal: - // proxy name - cfg.ProxyName = prefix + name - - // server_name - if cfg.ServerUser == "" { - cfg.ServerName = prefix + cfg.ServerName - } else { - cfg.ServerName = cfg.ServerUser + "." + cfg.ServerName - } + cfg.ProxyName = name // bind_addr if cfg.BindAddr == "" { @@ -170,8 +101,9 @@ func preVisitorUnmarshalFromIni(cfg VisitorConf, prefix string, name string, sec return nil } -// SUDP -var _ VisitorConf = &SUDPVisitorConf{} +type SUDPVisitorConf struct { + BaseVisitorConf `ini:",extends"` +} func (cfg *SUDPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) { err = preVisitorUnmarshalFromIni(cfg, prefix, name, section) @@ -184,19 +116,10 @@ func (cfg *SUDPVisitorConf) UnmarshalFromIni(prefix string, name string, section return } -func (cfg *SUDPVisitorConf) Validate() (err error) { - if err = cfg.BaseVisitorConf.validate(); err != nil { - return - } - - // Add custom logic validate, if exists - - return +type STCPVisitorConf struct { + BaseVisitorConf `ini:",extends"` } -// STCP -var _ VisitorConf = &STCPVisitorConf{} - func (cfg *STCPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) { err = preVisitorUnmarshalFromIni(cfg, prefix, name, section) if err != nil { @@ -208,19 +131,17 @@ func (cfg *STCPVisitorConf) UnmarshalFromIni(prefix string, name string, section return } -func (cfg *STCPVisitorConf) Validate() (err error) { - if err = cfg.BaseVisitorConf.validate(); err != nil { - return - } - - // Add custom logic validate, if exists +type XTCPVisitorConf struct { + BaseVisitorConf `ini:",extends"` - return + Protocol string `ini:"protocol" json:"protocol,omitempty"` + KeepTunnelOpen bool `ini:"keep_tunnel_open" json:"keep_tunnel_open,omitempty"` + MaxRetriesAnHour int `ini:"max_retries_an_hour" json:"max_retries_an_hour,omitempty"` + MinRetryInterval int `ini:"min_retry_interval" json:"min_retry_interval,omitempty"` + FallbackTo string `ini:"fallback_to" json:"fallback_to,omitempty"` + FallbackTimeoutMs int `ini:"fallback_timeout_ms" json:"fallback_timeout_ms,omitempty"` } -// XTCP -var _ VisitorConf = &XTCPVisitorConf{} - func (cfg *XTCPVisitorConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) (err error) { err = preVisitorUnmarshalFromIni(cfg, prefix, name, section) if err != nil { @@ -243,14 +164,22 @@ func (cfg *XTCPVisitorConf) UnmarshalFromIni(prefix string, name string, section return } -func (cfg *XTCPVisitorConf) Validate() (err error) { - if err = cfg.BaseVisitorConf.validate(); err != nil { - return +// Visitor loaded from ini +func NewVisitorConfFromIni(prefix string, name string, section *ini.Section) (VisitorConf, error) { + // section.Key: if key not exists, section will set it with default value. + visitorType := VisitorType(section.Key("type").String()) + + if visitorType == "" { + return nil, fmt.Errorf("type shouldn't be empty") + } + + conf := DefaultVisitorConf(visitorType) + if conf == nil { + return nil, fmt.Errorf("type [%s] error", visitorType) } - // Add custom logic validate, if exists - if !lo.Contains([]string{"", "kcp", "quic"}, cfg.Protocol) { - return fmt.Errorf("protocol should be 'kcp' or 'quic'") + if err := conf.UnmarshalFromIni(prefix, name, section); err != nil { + return nil, fmt.Errorf("type [%s] error", visitorType) } - return + return conf, nil } diff --git a/pkg/config/load.go b/pkg/config/load.go new file mode 100644 index 00000000000..af2c3e80704 --- /dev/null +++ b/pkg/config/load.go @@ -0,0 +1,281 @@ +// Copyright 2023 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 config + +import ( + "bytes" + "encoding/json" + "fmt" + "html/template" + "os" + "path/filepath" + "strings" + + toml "github.com/pelletier/go-toml/v2" + "github.com/samber/lo" + "gopkg.in/ini.v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/yaml" + + "github.com/fatedier/frp/pkg/config/legacy" + v1 "github.com/fatedier/frp/pkg/config/v1" + "github.com/fatedier/frp/pkg/config/v1/validation" + "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/util/util" +) + +var glbEnvs map[string]string + +func init() { + glbEnvs = make(map[string]string) + envs := os.Environ() + for _, env := range envs { + pair := strings.SplitN(env, "=", 2) + if len(pair) != 2 { + continue + } + glbEnvs[pair[0]] = pair[1] + } +} + +type Values struct { + Envs map[string]string // environment vars +} + +func GetValues() *Values { + return &Values{ + Envs: glbEnvs, + } +} + +func DetectLegacyINIFormat(content []byte) bool { + f, err := ini.Load(content) + if err != nil { + return false + } + if _, err := f.GetSection("common"); err == nil { + return true + } + return false +} + +func DetectLegacyINIFormatFromFile(path string) bool { + b, err := os.ReadFile(path) + if err != nil { + return false + } + return DetectLegacyINIFormat(b) +} + +func RenderWithTemplate(in []byte, values *Values) ([]byte, error) { + tmpl, err := template.New("frp").Parse(string(in)) + if err != nil { + return nil, err + } + + buffer := bytes.NewBufferString("") + if err := tmpl.Execute(buffer, values); err != nil { + return nil, err + } + return buffer.Bytes(), nil +} + +func LoadFileContentWithTemplate(path string, values *Values) ([]byte, error) { + b, err := os.ReadFile(path) + if err != nil { + return nil, err + } + return RenderWithTemplate(b, values) +} + +func LoadConfigureFromFile(path string, c any) error { + content, err := LoadFileContentWithTemplate(path, GetValues()) + if err != nil { + return err + } + return LoadConfigure(content, c) +} + +// LoadConfigure loads configuration from bytes and unmarshal into c. +// Now it supports json, yaml and toml format. +func LoadConfigure(b []byte, c any) error { + var tomlObj interface{} + if err := toml.Unmarshal(b, &tomlObj); err == nil { + b, err = json.Marshal(&tomlObj) + if err != nil { + return err + } + } + decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(b), 4096) + return decoder.Decode(c) +} + +func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.ProxyConfigurer, error) { + m.ProxyType = util.EmptyOr(m.ProxyType, string(v1.ProxyTypeTCP)) + + configurer := v1.NewProxyConfigurerByType(v1.ProxyType(m.ProxyType)) + if configurer == nil { + return nil, fmt.Errorf("unknown proxy type: %s", m.ProxyType) + } + + configurer.UnmarshalFromMsg(m) + configurer.Complete("") + + if err := validation.ValidateProxyConfigurerForServer(configurer, serverCfg); err != nil { + return nil, err + } + return configurer, nil +} + +func LoadServerConfig(path string) (*v1.ServerConfig, bool, error) { + var ( + svrCfg *v1.ServerConfig + isLegacyFormat bool + ) + // detect legacy ini format + if DetectLegacyINIFormatFromFile(path) { + content, err := legacy.GetRenderedConfFromFile(path) + if err != nil { + return nil, true, err + } + legacyCfg, err := legacy.UnmarshalServerConfFromIni(content) + if err != nil { + return nil, true, err + } + svrCfg = legacy.Convert_ServerCommonConf_To_v1(&legacyCfg) + isLegacyFormat = true + } else { + svrCfg = &v1.ServerConfig{} + if err := LoadConfigureFromFile(path, svrCfg); err != nil { + return nil, false, err + } + } + if svrCfg != nil { + svrCfg.Complete() + } + return svrCfg, isLegacyFormat, nil +} + +func LoadClientConfig(path string) ( + *v1.ClientCommonConfig, + []v1.ProxyConfigurer, + []v1.VisitorConfigurer, + bool, error, +) { + var ( + cliCfg *v1.ClientCommonConfig + pxyCfgs = make([]v1.ProxyConfigurer, 0) + visitorCfgs = make([]v1.VisitorConfigurer, 0) + isLegacyFormat bool + ) + + if DetectLegacyINIFormatFromFile(path) { + legacyCommon, legacyPxyCfgs, legacyVisitorCfgs, err := legacy.ParseClientConfig(path) + if err != nil { + return nil, nil, nil, true, err + } + cliCfg = legacy.Convert_ClientCommonConf_To_v1(&legacyCommon) + for _, c := range legacyPxyCfgs { + pxyCfgs = append(pxyCfgs, legacy.Convert_ProxyConf_To_v1(c)) + } + for _, c := range legacyVisitorCfgs { + visitorCfgs = append(visitorCfgs, legacy.Convert_VisitorConf_To_v1(c)) + } + isLegacyFormat = true + } else { + allCfg := v1.ClientConfig{} + if err := LoadConfigureFromFile(path, &allCfg); err != nil { + return nil, nil, nil, false, err + } + cliCfg = &allCfg.ClientCommonConfig + for _, c := range allCfg.Proxies { + pxyCfgs = append(pxyCfgs, c.ProxyConfigurer) + } + for _, c := range allCfg.Visitors { + visitorCfgs = append(visitorCfgs, c.VisitorConfigurer) + } + } + + // Load additional config from includes. + // legacy ini format alredy handle this in ParseClientConfig. + if len(cliCfg.IncludeConfigFiles) > 0 && !isLegacyFormat { + extPxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(cliCfg.IncludeConfigFiles, isLegacyFormat) + if err != nil { + return nil, nil, nil, isLegacyFormat, err + } + pxyCfgs = append(pxyCfgs, extPxyCfgs...) + visitorCfgs = append(visitorCfgs, extVisitorCfgs...) + } + + // Filter by start + if len(cliCfg.Start) > 0 { + startSet := sets.New(cliCfg.Start...) + pxyCfgs = lo.Filter(pxyCfgs, func(c v1.ProxyConfigurer, _ int) bool { + return startSet.Has(c.GetBaseConfig().Name) + }) + visitorCfgs = lo.Filter(visitorCfgs, func(c v1.VisitorConfigurer, _ int) bool { + return startSet.Has(c.GetBaseConfig().Name) + }) + } + + if cliCfg != nil { + cliCfg.Complete() + } + for _, c := range pxyCfgs { + c.Complete(cliCfg.User) + } + for _, c := range visitorCfgs { + c.Complete(cliCfg) + } + return cliCfg, pxyCfgs, visitorCfgs, isLegacyFormat, nil +} + +func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) { + pxyCfgs := make([]v1.ProxyConfigurer, 0) + visitorCfgs := make([]v1.VisitorConfigurer, 0) + for _, path := range paths { + absDir, err := filepath.Abs(filepath.Dir(path)) + if err != nil { + return nil, nil, err + } + if _, err := os.Stat(absDir); os.IsNotExist(err) { + return nil, nil, err + } + files, err := os.ReadDir(absDir) + if err != nil { + return nil, nil, err + } + for _, fi := range files { + if fi.IsDir() { + continue + } + absFile := filepath.Join(absDir, fi.Name()) + if matched, _ := filepath.Match(filepath.Join(absDir, filepath.Base(path)), absFile); matched { + // support yaml/json/toml + cfg := v1.ClientConfig{} + if err := LoadConfigureFromFile(absFile, &cfg); err != nil { + return nil, nil, fmt.Errorf("load additional config from %s error: %v", absFile, err) + } + for _, c := range cfg.Proxies { + pxyCfgs = append(pxyCfgs, c.ProxyConfigurer) + } + for _, c := range cfg.Visitors { + visitorCfgs = append(visitorCfgs, c.VisitorConfigurer) + } + } + } + } + return pxyCfgs, visitorCfgs, nil +} diff --git a/pkg/config/load_test.go b/pkg/config/load_test.go new file mode 100644 index 00000000000..eab4ba96b7d --- /dev/null +++ b/pkg/config/load_test.go @@ -0,0 +1,45 @@ +// Copyright 2023 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 config + +import ( + "testing" + + "github.com/stretchr/testify/require" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func TestLoadConfigure(t *testing.T) { + require := require.New(t) + content := ` +bindAddr = "127.0.0.1" +kcpBindPort = 7000 +quicBindPort = 7001 +tcpmuxHTTPConnectPort = 7005 +custom404Page = "/abc.html" +transport.tcpKeepalive = 10 +` + + svrCfg := v1.ServerConfig{} + err := LoadConfigure([]byte(content), &svrCfg) + require.NoError(err) + require.EqualValues("127.0.0.1", svrCfg.BindAddr) + require.EqualValues(7000, svrCfg.KCPBindPort) + require.EqualValues(7001, svrCfg.QUICBindPort) + require.EqualValues(7005, svrCfg.TCPMuxHTTPConnectPort) + require.EqualValues("/abc.html", svrCfg.Custom404Page) + require.EqualValues(10, svrCfg.Transport.TCPKeepAlive) +} diff --git a/pkg/config/proxy.go b/pkg/config/proxy.go deleted file mode 100644 index 1169fd7527e..00000000000 --- a/pkg/config/proxy.go +++ /dev/null @@ -1,921 +0,0 @@ -// Copyright 2016 fatedier, fatedier@gmail.com -// -// 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 config - -import ( - "fmt" - "net" - "reflect" - "strconv" - "strings" - - "gopkg.in/ini.v1" - - "github.com/fatedier/frp/pkg/consts" - "github.com/fatedier/frp/pkg/msg" -) - -// Proxy -var ( - proxyConfTypeMap = map[string]reflect.Type{ - consts.TCPProxy: reflect.TypeOf(TCPProxyConf{}), - consts.TCPMuxProxy: reflect.TypeOf(TCPMuxProxyConf{}), - consts.UDPProxy: reflect.TypeOf(UDPProxyConf{}), - consts.HTTPProxy: reflect.TypeOf(HTTPProxyConf{}), - consts.HTTPSProxy: reflect.TypeOf(HTTPSProxyConf{}), - consts.STCPProxy: reflect.TypeOf(STCPProxyConf{}), - consts.XTCPProxy: reflect.TypeOf(XTCPProxyConf{}), - consts.SUDPProxy: reflect.TypeOf(SUDPProxyConf{}), - } -) - -func NewConfByType(proxyType string) ProxyConf { - v, ok := proxyConfTypeMap[proxyType] - if !ok { - return nil - } - cfg := reflect.New(v).Interface().(ProxyConf) - return cfg -} - -type ProxyConf interface { - // GetBaseConfig returns the BaseProxyConf for this config. - GetBaseConfig() *BaseProxyConf - // SetDefaultValues sets the default values for this config. - SetDefaultValues() - // UnmarshalFromMsg unmarshals a msg.NewProxy message into this config. - // This function will be called on the frps side. - UnmarshalFromMsg(*msg.NewProxy) - // UnmarshalFromIni unmarshals a ini.Section into this config. This function - // will be called on the frpc side. - UnmarshalFromIni(string, string, *ini.Section) error - // MarshalToMsg marshals this config into a msg.NewProxy message. This - // function will be called on the frpc side. - MarshalToMsg(*msg.NewProxy) - // ValidateForClient checks that the config is valid for the frpc side. - ValidateForClient() error - // ValidateForServer checks that the config is valid for the frps side. - ValidateForServer(ServerCommonConf) error -} - -// LocalSvrConf configures what location the client will to, or what -// plugin will be used. -type LocalSvrConf struct { - // LocalIP specifies the IP address or host name to to. - LocalIP string `ini:"local_ip" json:"local_ip"` - // LocalPort specifies the port to to. - LocalPort int `ini:"local_port" json:"local_port"` - - // Plugin specifies what plugin should be used for ng. If this value - // is set, the LocalIp and LocalPort values will be ignored. By default, - // this value is "". - Plugin string `ini:"plugin" json:"plugin"` - // PluginParams specify parameters to be passed to the plugin, if one is - // being used. By default, this value is an empty map. - PluginParams map[string]string `ini:"-"` -} - -// HealthCheckConf configures health checking. This can be useful for load -// balancing purposes to detect and remove proxies to failing services. -type HealthCheckConf struct { - // HealthCheckType specifies what protocol to use for health checking. - // Valid values include "tcp", "http", and "". If this value is "", health - // checking will not be performed. By default, this value is "". - // - // If the type is "tcp", a connection will be attempted to the target - // server. If a connection cannot be established, the health check fails. - // - // If the type is "http", a GET request will be made to the endpoint - // specified by HealthCheckURL. If the response is not a 200, the health - // check fails. - HealthCheckType string `ini:"health_check_type" json:"health_check_type"` // tcp | http - // HealthCheckTimeoutS specifies the number of seconds to wait for a health - // check attempt to connect. If the timeout is reached, this counts as a - // health check failure. By default, this value is 3. - HealthCheckTimeoutS int `ini:"health_check_timeout_s" json:"health_check_timeout_s"` - // HealthCheckMaxFailed specifies the number of allowed failures before the - // is stopped. By default, this value is 1. - HealthCheckMaxFailed int `ini:"health_check_max_failed" json:"health_check_max_failed"` - // HealthCheckIntervalS specifies the time in seconds between health - // checks. By default, this value is 10. - HealthCheckIntervalS int `ini:"health_check_interval_s" json:"health_check_interval_s"` - // HealthCheckURL specifies the address to send health checks to if the - // health check type is "http". - HealthCheckURL string `ini:"health_check_url" json:"health_check_url"` - // HealthCheckAddr specifies the address to connect to if the health check - // type is "tcp". - HealthCheckAddr string `ini:"-"` -} - -// BaseProxyConf provides configuration info that is common to all types. -type BaseProxyConf struct { - // ProxyName is the name of this - ProxyName string `ini:"name" json:"name"` - // ProxyType specifies the type of this Valid values include "tcp", - // "udp", "http", "https", "stcp", and "xtcp". By default, this value is - // "tcp". - ProxyType string `ini:"type" json:"type"` - - // UseEncryption controls whether or not communication with the server will - // be encrypted. Encryption is done using the tokens supplied in the server - // and client configuration. By default, this value is false. - UseEncryption bool `ini:"use_encryption" json:"use_encryption"` - // UseCompression controls whether or not communication with the server - // will be compressed. By default, this value is false. - UseCompression bool `ini:"use_compression" json:"use_compression"` - // Group specifies which group the is a part of. The server will use - // this information to load balance proxies in the same group. If the value - // is "", this will not be in a group. By default, this value is "". - Group string `ini:"group" json:"group"` - // GroupKey specifies a group key, which should be the same among proxies - // of the same group. By default, this value is "". - GroupKey string `ini:"group_key" json:"group_key"` - - // ProxyProtocolVersion specifies which protocol version to use. Valid - // values include "v1", "v2", and "". If the value is "", a protocol - // version will be automatically selected. By default, this value is "". - ProxyProtocolVersion string `ini:"proxy_protocol_version" json:"proxy_protocol_version"` - - // BandwidthLimit limit the bandwidth - // 0 means no limit - BandwidthLimit BandwidthQuantity `ini:"bandwidth_limit" json:"bandwidth_limit"` - // BandwidthLimitMode specifies whether to limit the bandwidth on the - // client or server side. Valid values include "client" and "server". - // By default, this value is "client". - BandwidthLimitMode string `ini:"bandwidth_limit_mode" json:"bandwidth_limit_mode"` - - // meta info for each proxy - Metas map[string]string `ini:"-" json:"metas"` - - LocalSvrConf `ini:",extends"` - HealthCheckConf `ini:",extends"` -} - -type DomainConf struct { - CustomDomains []string `ini:"custom_domains" json:"custom_domains"` - SubDomain string `ini:"subdomain" json:"subdomain"` -} - -type RoleServerCommonConf struct { - Role string `ini:"role" json:"role"` - Sk string `ini:"sk" json:"sk"` - AllowUsers []string `ini:"allow_users" json:"allow_users"` -} - -func (cfg *RoleServerCommonConf) setDefaultValues() { - cfg.Role = "server" -} - -func (cfg *RoleServerCommonConf) marshalToMsg(m *msg.NewProxy) { - m.Sk = cfg.Sk - m.AllowUsers = cfg.AllowUsers -} - -func (cfg *RoleServerCommonConf) unmarshalFromMsg(m *msg.NewProxy) { - cfg.Sk = m.Sk - cfg.AllowUsers = m.AllowUsers -} - -// HTTP -type HTTPProxyConf struct { - BaseProxyConf `ini:",extends"` - DomainConf `ini:",extends"` - - Locations []string `ini:"locations" json:"locations"` - HTTPUser string `ini:"http_user" json:"http_user"` - HTTPPwd string `ini:"http_pwd" json:"http_pwd"` - HostHeaderRewrite string `ini:"host_header_rewrite" json:"host_header_rewrite"` - Headers map[string]string `ini:"-" json:"headers"` - RouteByHTTPUser string `ini:"route_by_http_user" json:"route_by_http_user"` -} - -// HTTPS -type HTTPSProxyConf struct { - BaseProxyConf `ini:",extends"` - DomainConf `ini:",extends"` -} - -// TCP -type TCPProxyConf struct { - BaseProxyConf `ini:",extends"` - RemotePort int `ini:"remote_port" json:"remote_port"` -} - -// UDP -type UDPProxyConf struct { - BaseProxyConf `ini:",extends"` - - RemotePort int `ini:"remote_port" json:"remote_port"` -} - -// TCPMux -type TCPMuxProxyConf struct { - BaseProxyConf `ini:",extends"` - DomainConf `ini:",extends"` - HTTPUser string `ini:"http_user" json:"http_user,omitempty"` - HTTPPwd string `ini:"http_pwd" json:"http_pwd,omitempty"` - RouteByHTTPUser string `ini:"route_by_http_user" json:"route_by_http_user"` - - Multiplexer string `ini:"multiplexer"` -} - -// STCP -type STCPProxyConf struct { - BaseProxyConf `ini:",extends"` - RoleServerCommonConf `ini:",extends"` -} - -// XTCP -type XTCPProxyConf struct { - BaseProxyConf `ini:",extends"` - RoleServerCommonConf `ini:",extends"` -} - -// SUDP -type SUDPProxyConf struct { - BaseProxyConf `ini:",extends"` - RoleServerCommonConf `ini:",extends"` -} - -// Proxy Conf Loader -// DefaultProxyConf creates a empty ProxyConf object by proxyType. -// If proxyType doesn't exist, return nil. -func DefaultProxyConf(proxyType string) ProxyConf { - conf := NewConfByType(proxyType) - if conf != nil { - conf.SetDefaultValues() - } - return conf -} - -// Proxy loaded from ini -func NewProxyConfFromIni(prefix, name string, section *ini.Section) (ProxyConf, error) { - // section.Key: if key not exists, section will set it with default value. - proxyType := section.Key("type").String() - if proxyType == "" { - proxyType = consts.TCPProxy - } - - conf := DefaultProxyConf(proxyType) - if conf == nil { - return nil, fmt.Errorf("invalid type [%s]", proxyType) - } - - if err := conf.UnmarshalFromIni(prefix, name, section); err != nil { - return nil, err - } - - if err := conf.ValidateForClient(); err != nil { - return nil, err - } - return conf, nil -} - -// Proxy loaded from msg -func NewProxyConfFromMsg(m *msg.NewProxy, serverCfg ServerCommonConf) (ProxyConf, error) { - if m.ProxyType == "" { - m.ProxyType = consts.TCPProxy - } - - conf := DefaultProxyConf(m.ProxyType) - if conf == nil { - return nil, fmt.Errorf("proxy [%s] type [%s] error", m.ProxyName, m.ProxyType) - } - - conf.UnmarshalFromMsg(m) - - err := conf.ValidateForServer(serverCfg) - if err != nil { - return nil, err - } - - return conf, nil -} - -// Base -func (cfg *BaseProxyConf) GetBaseConfig() *BaseProxyConf { - return cfg -} - -func (cfg *BaseProxyConf) SetDefaultValues() { - cfg.LocalSvrConf = LocalSvrConf{ - LocalIP: "127.0.0.1", - } - cfg.BandwidthLimitMode = BandwidthLimitModeClient -} - -// BaseProxyConf apply custom logic changes. -func (cfg *BaseProxyConf) decorate(prefix string, name string, section *ini.Section) error { - // proxy_name - cfg.ProxyName = prefix + name - - // metas_xxx - cfg.Metas = GetMapWithoutPrefix(section.KeysHash(), "meta_") - - // bandwidth_limit - if bandwidth, err := section.GetKey("bandwidth_limit"); err == nil { - cfg.BandwidthLimit, err = NewBandwidthQuantity(bandwidth.String()) - if err != nil { - return err - } - } - - // plugin_xxx - cfg.LocalSvrConf.PluginParams = GetMapByPrefix(section.KeysHash(), "plugin_") - - // custom logic code - if cfg.HealthCheckType == "tcp" && cfg.Plugin == "" { - cfg.HealthCheckAddr = cfg.LocalIP + fmt.Sprintf(":%d", cfg.LocalPort) - } - - if cfg.HealthCheckType == "http" && cfg.Plugin == "" && cfg.HealthCheckURL != "" { - s := "http://" + net.JoinHostPort(cfg.LocalIP, strconv.Itoa(cfg.LocalPort)) - if !strings.HasPrefix(cfg.HealthCheckURL, "/") { - s += "/" - } - cfg.HealthCheckURL = s + cfg.HealthCheckURL - } - - return nil -} - -func (cfg *BaseProxyConf) marshalToMsg(m *msg.NewProxy) { - m.ProxyName = cfg.ProxyName - m.ProxyType = cfg.ProxyType - m.UseEncryption = cfg.UseEncryption - m.UseCompression = cfg.UseCompression - m.BandwidthLimit = cfg.BandwidthLimit.String() - // leave it empty for default value to reduce traffic - if cfg.BandwidthLimitMode != "client" { - m.BandwidthLimitMode = cfg.BandwidthLimitMode - } - m.Group = cfg.Group - m.GroupKey = cfg.GroupKey - m.Metas = cfg.Metas -} - -func (cfg *BaseProxyConf) unmarshalFromMsg(m *msg.NewProxy) { - cfg.ProxyName = m.ProxyName - cfg.ProxyType = m.ProxyType - cfg.UseEncryption = m.UseEncryption - cfg.UseCompression = m.UseCompression - if m.BandwidthLimit != "" { - cfg.BandwidthLimit, _ = NewBandwidthQuantity(m.BandwidthLimit) - } - if m.BandwidthLimitMode != "" { - cfg.BandwidthLimitMode = m.BandwidthLimitMode - } - cfg.Group = m.Group - cfg.GroupKey = m.GroupKey - cfg.Metas = m.Metas -} - -func (cfg *BaseProxyConf) validateForClient() (err error) { - if cfg.ProxyProtocolVersion != "" { - if cfg.ProxyProtocolVersion != "v1" && cfg.ProxyProtocolVersion != "v2" { - return fmt.Errorf("no support proxy protocol version: %s", cfg.ProxyProtocolVersion) - } - } - - if cfg.BandwidthLimitMode != "client" && cfg.BandwidthLimitMode != "server" { - return fmt.Errorf("bandwidth_limit_mode should be client or server") - } - - if err = cfg.LocalSvrConf.validateForClient(); err != nil { - return - } - if err = cfg.HealthCheckConf.validateForClient(); err != nil { - return - } - return nil -} - -func (cfg *BaseProxyConf) validateForServer() (err error) { - if cfg.BandwidthLimitMode != "client" && cfg.BandwidthLimitMode != "server" { - return fmt.Errorf("bandwidth_limit_mode should be client or server") - } - return nil -} - -// DomainConf -func (cfg *DomainConf) check() (err error) { - if len(cfg.CustomDomains) == 0 && cfg.SubDomain == "" { - err = fmt.Errorf("custom_domains and subdomain should set at least one of them") - return - } - return -} - -func (cfg *DomainConf) validateForClient() (err error) { - if err = cfg.check(); err != nil { - return - } - return -} - -func (cfg *DomainConf) validateForServer(serverCfg ServerCommonConf) (err error) { - if err = cfg.check(); err != nil { - return - } - - for _, domain := range cfg.CustomDomains { - if serverCfg.SubDomainHost != "" && len(strings.Split(serverCfg.SubDomainHost, ".")) < len(strings.Split(domain, ".")) { - if strings.Contains(domain, serverCfg.SubDomainHost) { - return fmt.Errorf("custom domain [%s] should not belong to subdomain_host [%s]", domain, serverCfg.SubDomainHost) - } - } - } - - if cfg.SubDomain != "" { - if serverCfg.SubDomainHost == "" { - return fmt.Errorf("subdomain is not supported because this feature is not enabled in remote frps") - } - if strings.Contains(cfg.SubDomain, ".") || strings.Contains(cfg.SubDomain, "*") { - return fmt.Errorf("'.' and '*' is not supported in subdomain") - } - } - return nil -} - -// LocalSvrConf -func (cfg *LocalSvrConf) validateForClient() (err error) { - if cfg.Plugin == "" { - if cfg.LocalIP == "" { - err = fmt.Errorf("local ip or plugin is required") - return - } - if cfg.LocalPort <= 0 { - err = fmt.Errorf("error local_port") - return - } - } - return -} - -// HealthCheckConf -func (cfg *HealthCheckConf) validateForClient() error { - if cfg.HealthCheckType != "" && cfg.HealthCheckType != "tcp" && cfg.HealthCheckType != "http" { - return fmt.Errorf("unsupport health check type") - } - if cfg.HealthCheckType != "" { - if cfg.HealthCheckType == "http" && cfg.HealthCheckURL == "" { - return fmt.Errorf("health_check_url is required for health check type 'http'") - } - } - return nil -} - -func preUnmarshalFromIni(cfg ProxyConf, prefix string, name string, section *ini.Section) error { - err := section.MapTo(cfg) - if err != nil { - return err - } - - err = cfg.GetBaseConfig().decorate(prefix, name, section) - if err != nil { - return err - } - - return nil -} - -// TCP -func (cfg *TCPProxyConf) UnmarshalFromMsg(m *msg.NewProxy) { - cfg.BaseProxyConf.unmarshalFromMsg(m) - - // Add custom logic unmarshal if exists - cfg.RemotePort = m.RemotePort -} - -func (cfg *TCPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { - err := preUnmarshalFromIni(cfg, prefix, name, section) - if err != nil { - return err - } - - // Add custom logic unmarshal if exists - - return nil -} - -func (cfg *TCPProxyConf) MarshalToMsg(m *msg.NewProxy) { - cfg.BaseProxyConf.marshalToMsg(m) - - // Add custom logic marshal if exists - m.RemotePort = cfg.RemotePort -} - -func (cfg *TCPProxyConf) ValidateForClient() (err error) { - if err = cfg.BaseProxyConf.validateForClient(); err != nil { - return - } - - // Add custom logic check if exists - - return -} - -func (cfg *TCPProxyConf) ValidateForServer(_ ServerCommonConf) error { - return cfg.BaseProxyConf.validateForServer() -} - -// TCPMux -func (cfg *TCPMuxProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { - err := preUnmarshalFromIni(cfg, prefix, name, section) - if err != nil { - return err - } - - // Add custom logic unmarshal if exists - - return nil -} - -func (cfg *TCPMuxProxyConf) UnmarshalFromMsg(m *msg.NewProxy) { - cfg.BaseProxyConf.unmarshalFromMsg(m) - - // Add custom logic unmarshal if exists - cfg.CustomDomains = m.CustomDomains - cfg.SubDomain = m.SubDomain - cfg.Multiplexer = m.Multiplexer - cfg.HTTPUser = m.HTTPUser - cfg.HTTPPwd = m.HTTPPwd - cfg.RouteByHTTPUser = m.RouteByHTTPUser -} - -func (cfg *TCPMuxProxyConf) MarshalToMsg(m *msg.NewProxy) { - cfg.BaseProxyConf.marshalToMsg(m) - - // Add custom logic marshal if exists - m.CustomDomains = cfg.CustomDomains - m.SubDomain = cfg.SubDomain - m.Multiplexer = cfg.Multiplexer - m.HTTPUser = cfg.HTTPUser - m.HTTPPwd = cfg.HTTPPwd - m.RouteByHTTPUser = cfg.RouteByHTTPUser -} - -func (cfg *TCPMuxProxyConf) ValidateForClient() (err error) { - if err = cfg.BaseProxyConf.validateForClient(); err != nil { - return - } - - // Add custom logic check if exists - if err = cfg.DomainConf.validateForClient(); err != nil { - return - } - - if cfg.Multiplexer != consts.HTTPConnectTCPMultiplexer { - return fmt.Errorf("parse conf error: incorrect multiplexer [%s]", cfg.Multiplexer) - } - - return -} - -func (cfg *TCPMuxProxyConf) ValidateForServer(serverCfg ServerCommonConf) (err error) { - if err := cfg.BaseProxyConf.validateForServer(); err != nil { - return err - } - - if cfg.Multiplexer != consts.HTTPConnectTCPMultiplexer { - return fmt.Errorf("proxy [%s] incorrect multiplexer [%s]", cfg.ProxyName, cfg.Multiplexer) - } - - if cfg.Multiplexer == consts.HTTPConnectTCPMultiplexer && serverCfg.TCPMuxHTTPConnectPort == 0 { - return fmt.Errorf("proxy [%s] type [tcpmux] with multiplexer [httpconnect] requires tcpmux_httpconnect_port configuration", cfg.ProxyName) - } - - if err = cfg.DomainConf.validateForServer(serverCfg); err != nil { - err = fmt.Errorf("proxy [%s] domain conf check error: %v", cfg.ProxyName, err) - return - } - - return -} - -// UDP -func (cfg *UDPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { - err := preUnmarshalFromIni(cfg, prefix, name, section) - if err != nil { - return err - } - - // Add custom logic unmarshal if exists - - return nil -} - -func (cfg *UDPProxyConf) UnmarshalFromMsg(m *msg.NewProxy) { - cfg.BaseProxyConf.unmarshalFromMsg(m) - - // Add custom logic unmarshal if exists - cfg.RemotePort = m.RemotePort -} - -func (cfg *UDPProxyConf) MarshalToMsg(m *msg.NewProxy) { - cfg.BaseProxyConf.marshalToMsg(m) - - // Add custom logic marshal if exists - m.RemotePort = cfg.RemotePort -} - -func (cfg *UDPProxyConf) ValidateForClient() (err error) { - if err = cfg.BaseProxyConf.validateForClient(); err != nil { - return - } - - // Add custom logic check if exists - - return -} - -func (cfg *UDPProxyConf) ValidateForServer(_ ServerCommonConf) error { - return cfg.BaseProxyConf.validateForServer() -} - -// HTTP -func (cfg *HTTPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { - err := preUnmarshalFromIni(cfg, prefix, name, section) - if err != nil { - return err - } - - // Add custom logic unmarshal if exists - cfg.Headers = GetMapWithoutPrefix(section.KeysHash(), "header_") - return nil -} - -func (cfg *HTTPProxyConf) UnmarshalFromMsg(m *msg.NewProxy) { - cfg.BaseProxyConf.unmarshalFromMsg(m) - - // Add custom logic unmarshal if exists - cfg.CustomDomains = m.CustomDomains - cfg.SubDomain = m.SubDomain - cfg.Locations = m.Locations - cfg.HostHeaderRewrite = m.HostHeaderRewrite - cfg.HTTPUser = m.HTTPUser - cfg.HTTPPwd = m.HTTPPwd - cfg.Headers = m.Headers - cfg.RouteByHTTPUser = m.RouteByHTTPUser -} - -func (cfg *HTTPProxyConf) MarshalToMsg(m *msg.NewProxy) { - cfg.BaseProxyConf.marshalToMsg(m) - - // Add custom logic marshal if exists - m.CustomDomains = cfg.CustomDomains - m.SubDomain = cfg.SubDomain - m.Locations = cfg.Locations - m.HostHeaderRewrite = cfg.HostHeaderRewrite - m.HTTPUser = cfg.HTTPUser - m.HTTPPwd = cfg.HTTPPwd - m.Headers = cfg.Headers - m.RouteByHTTPUser = cfg.RouteByHTTPUser -} - -func (cfg *HTTPProxyConf) ValidateForClient() (err error) { - if err = cfg.BaseProxyConf.validateForClient(); err != nil { - return - } - - // Add custom logic check if exists - if err = cfg.DomainConf.validateForClient(); err != nil { - return - } - - return -} - -func (cfg *HTTPProxyConf) ValidateForServer(serverCfg ServerCommonConf) (err error) { - if err := cfg.BaseProxyConf.validateForServer(); err != nil { - return err - } - - if serverCfg.VhostHTTPPort == 0 { - return fmt.Errorf("type [http] not support when vhost_http_port is not set") - } - - if err = cfg.DomainConf.validateForServer(serverCfg); err != nil { - err = fmt.Errorf("proxy [%s] domain conf check error: %v", cfg.ProxyName, err) - return - } - - return -} - -// HTTPS -func (cfg *HTTPSProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { - err := preUnmarshalFromIni(cfg, prefix, name, section) - if err != nil { - return err - } - - // Add custom logic unmarshal if exists - return nil -} - -func (cfg *HTTPSProxyConf) UnmarshalFromMsg(m *msg.NewProxy) { - cfg.BaseProxyConf.unmarshalFromMsg(m) - - // Add custom logic unmarshal if exists - cfg.CustomDomains = m.CustomDomains - cfg.SubDomain = m.SubDomain -} - -func (cfg *HTTPSProxyConf) MarshalToMsg(m *msg.NewProxy) { - cfg.BaseProxyConf.marshalToMsg(m) - - // Add custom logic marshal if exists - m.CustomDomains = cfg.CustomDomains - m.SubDomain = cfg.SubDomain -} - -func (cfg *HTTPSProxyConf) ValidateForClient() (err error) { - if err = cfg.BaseProxyConf.validateForClient(); err != nil { - return - } - - // Add custom logic check if exists - if err = cfg.DomainConf.validateForClient(); err != nil { - return - } - return -} - -func (cfg *HTTPSProxyConf) ValidateForServer(serverCfg ServerCommonConf) (err error) { - if err := cfg.BaseProxyConf.validateForServer(); err != nil { - return err - } - - if serverCfg.VhostHTTPSPort == 0 { - return fmt.Errorf("type [https] not support when vhost_https_port is not set") - } - - if err = cfg.DomainConf.validateForServer(serverCfg); err != nil { - err = fmt.Errorf("proxy [%s] domain conf check error: %v", cfg.ProxyName, err) - return - } - - return -} - -// SUDP -func (cfg *SUDPProxyConf) SetDefaultValues() { - cfg.BaseProxyConf.SetDefaultValues() - cfg.RoleServerCommonConf.setDefaultValues() -} - -func (cfg *SUDPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { - err := preUnmarshalFromIni(cfg, prefix, name, section) - if err != nil { - return err - } - - // Add custom logic unmarshal if exists - return nil -} - -// Only for role server. -func (cfg *SUDPProxyConf) UnmarshalFromMsg(m *msg.NewProxy) { - cfg.BaseProxyConf.unmarshalFromMsg(m) - - // Add custom logic unmarshal if exists - cfg.RoleServerCommonConf.unmarshalFromMsg(m) -} - -func (cfg *SUDPProxyConf) MarshalToMsg(m *msg.NewProxy) { - cfg.BaseProxyConf.marshalToMsg(m) - - // Add custom logic marshal if exists - cfg.RoleServerCommonConf.marshalToMsg(m) -} - -func (cfg *SUDPProxyConf) ValidateForClient() (err error) { - if err := cfg.BaseProxyConf.validateForClient(); err != nil { - return err - } - - // Add custom logic check if exists - if cfg.Role != "server" { - return fmt.Errorf("role should be 'server'") - } - - return nil -} - -func (cfg *SUDPProxyConf) ValidateForServer(_ ServerCommonConf) error { - return cfg.BaseProxyConf.validateForServer() -} - -// STCP -func (cfg *STCPProxyConf) SetDefaultValues() { - cfg.BaseProxyConf.SetDefaultValues() - cfg.RoleServerCommonConf.setDefaultValues() -} - -func (cfg *STCPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { - err := preUnmarshalFromIni(cfg, prefix, name, section) - if err != nil { - return err - } - - // Add custom logic unmarshal if exists - if cfg.Role == "" { - cfg.Role = "server" - } - return nil -} - -// Only for role server. -func (cfg *STCPProxyConf) UnmarshalFromMsg(m *msg.NewProxy) { - cfg.BaseProxyConf.unmarshalFromMsg(m) - - // Add custom logic unmarshal if exists - cfg.RoleServerCommonConf.unmarshalFromMsg(m) -} - -func (cfg *STCPProxyConf) MarshalToMsg(m *msg.NewProxy) { - cfg.BaseProxyConf.marshalToMsg(m) - - // Add custom logic marshal if exists - cfg.RoleServerCommonConf.marshalToMsg(m) -} - -func (cfg *STCPProxyConf) ValidateForClient() (err error) { - if err = cfg.BaseProxyConf.validateForClient(); err != nil { - return - } - - // Add custom logic check if exists - if cfg.Role != "server" { - return fmt.Errorf("role should be 'server'") - } - - return -} - -func (cfg *STCPProxyConf) ValidateForServer(_ ServerCommonConf) error { - return cfg.BaseProxyConf.validateForServer() -} - -// XTCP -func (cfg *XTCPProxyConf) SetDefaultValues() { - cfg.BaseProxyConf.SetDefaultValues() - cfg.RoleServerCommonConf.setDefaultValues() -} - -func (cfg *XTCPProxyConf) UnmarshalFromIni(prefix string, name string, section *ini.Section) error { - err := preUnmarshalFromIni(cfg, prefix, name, section) - if err != nil { - return err - } - - // Add custom logic unmarshal if exists - if cfg.Role == "" { - cfg.Role = "server" - } - return nil -} - -// Only for role server. -func (cfg *XTCPProxyConf) UnmarshalFromMsg(m *msg.NewProxy) { - cfg.BaseProxyConf.unmarshalFromMsg(m) - - // Add custom logic unmarshal if exists - cfg.RoleServerCommonConf.unmarshalFromMsg(m) -} - -func (cfg *XTCPProxyConf) MarshalToMsg(m *msg.NewProxy) { - cfg.BaseProxyConf.marshalToMsg(m) - - // Add custom logic marshal if exists - cfg.RoleServerCommonConf.marshalToMsg(m) -} - -func (cfg *XTCPProxyConf) ValidateForClient() (err error) { - if err = cfg.BaseProxyConf.validateForClient(); err != nil { - return - } - - // Add custom logic check if exists - if cfg.Role != "server" { - return fmt.Errorf("role should be 'server'") - } - return -} - -func (cfg *XTCPProxyConf) ValidateForServer(_ ServerCommonConf) error { - return cfg.BaseProxyConf.validateForServer() -} diff --git a/pkg/config/proxy_test.go b/pkg/config/proxy_test.go deleted file mode 100644 index 99d60811cf5..00000000000 --- a/pkg/config/proxy_test.go +++ /dev/null @@ -1,478 +0,0 @@ -// Copyright 2020 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 config - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "gopkg.in/ini.v1" - - "github.com/fatedier/frp/pkg/consts" -) - -var ( - testLoadOptions = ini.LoadOptions{ - Insensitive: false, - InsensitiveSections: false, - InsensitiveKeys: false, - IgnoreInlineComment: true, - AllowBooleanKeys: true, - } - - testProxyPrefix = "test." -) - -func Test_Proxy_Interface(_ *testing.T) { - for name := range proxyConfTypeMap { - NewConfByType(name) - } -} - -func Test_Proxy_UnmarshalFromIni(t *testing.T) { - assert := assert.New(t) - - testcases := []struct { - sname string - source []byte - expected ProxyConf - }{ - { - sname: "ssh", - source: []byte(` - [ssh] - # tcp | udp | http | https | stcp | xtcp, default is tcp - type = tcp - local_ip = 127.0.0.9 - local_port = 29 - bandwidth_limit = 19MB - bandwidth_limit_mode = server - use_encryption - use_compression - remote_port = 6009 - group = test_group - group_key = 123456 - health_check_type = tcp - health_check_timeout_s = 3 - health_check_max_failed = 3 - health_check_interval_s = 19 - meta_var1 = 123 - meta_var2 = 234`), - expected: &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "ssh", - ProxyType: consts.TCPProxy, - UseCompression: true, - UseEncryption: true, - Group: "test_group", - GroupKey: "123456", - BandwidthLimit: MustBandwidthQuantity("19MB"), - BandwidthLimitMode: BandwidthLimitModeServer, - Metas: map[string]string{ - "var1": "123", - "var2": "234", - }, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 29, - }, - HealthCheckConf: HealthCheckConf{ - HealthCheckType: consts.TCPProxy, - HealthCheckTimeoutS: 3, - HealthCheckMaxFailed: 3, - HealthCheckIntervalS: 19, - HealthCheckAddr: "127.0.0.9:29", - }, - }, - RemotePort: 6009, - }, - }, - { - sname: "ssh_random", - source: []byte(` - [ssh_random] - type = tcp - local_ip = 127.0.0.9 - local_port = 29 - remote_port = 9 - `), - expected: &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "ssh_random", - ProxyType: consts.TCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 29, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - RemotePort: 9, - }, - }, - { - sname: "dns", - source: []byte(` - [dns] - type = udp - local_ip = 114.114.114.114 - local_port = 59 - remote_port = 6009 - use_encryption - use_compression - `), - expected: &UDPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "dns", - ProxyType: consts.UDPProxy, - UseEncryption: true, - UseCompression: true, - LocalSvrConf: LocalSvrConf{ - LocalIP: "114.114.114.114", - LocalPort: 59, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - RemotePort: 6009, - }, - }, - { - sname: "web01", - source: []byte(` - [web01] - type = http - local_ip = 127.0.0.9 - local_port = 89 - use_encryption - use_compression - http_user = admin - http_pwd = admin - subdomain = web01 - custom_domains = web02.yourdomain.com - locations = /,/pic - host_header_rewrite = example.com - header_X-From-Where = frp - health_check_type = http - health_check_url = /status - health_check_interval_s = 19 - health_check_max_failed = 3 - health_check_timeout_s = 3 - `), - expected: &HTTPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "web01", - ProxyType: consts.HTTPProxy, - UseCompression: true, - UseEncryption: true, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 89, - }, - HealthCheckConf: HealthCheckConf{ - HealthCheckType: consts.HTTPProxy, - HealthCheckTimeoutS: 3, - HealthCheckMaxFailed: 3, - HealthCheckIntervalS: 19, - HealthCheckURL: "http://127.0.0.9:89/status", - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - DomainConf: DomainConf{ - CustomDomains: []string{"web02.yourdomain.com"}, - SubDomain: "web01", - }, - Locations: []string{"/", "/pic"}, - HTTPUser: "admin", - HTTPPwd: "admin", - HostHeaderRewrite: "example.com", - Headers: map[string]string{ - "X-From-Where": "frp", - }, - }, - }, - { - sname: "web02", - source: []byte(` - [web02] - type = https - local_ip = 127.0.0.9 - local_port = 8009 - use_encryption - use_compression - subdomain = web01 - custom_domains = web02.yourdomain.com - proxy_protocol_version = v2 - `), - expected: &HTTPSProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "web02", - ProxyType: consts.HTTPSProxy, - UseCompression: true, - UseEncryption: true, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 8009, - }, - ProxyProtocolVersion: "v2", - BandwidthLimitMode: BandwidthLimitModeClient, - }, - DomainConf: DomainConf{ - CustomDomains: []string{"web02.yourdomain.com"}, - SubDomain: "web01", - }, - }, - }, - { - sname: "secret_tcp", - source: []byte(` - [secret_tcp] - type = stcp - sk = abcdefg - local_ip = 127.0.0.1 - local_port = 22 - use_encryption = false - use_compression = false - `), - expected: &STCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "secret_tcp", - ProxyType: consts.STCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.1", - LocalPort: 22, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - RoleServerCommonConf: RoleServerCommonConf{ - Role: "server", - Sk: "abcdefg", - }, - }, - }, - { - sname: "p2p_tcp", - source: []byte(` - [p2p_tcp] - type = xtcp - sk = abcdefg - local_ip = 127.0.0.1 - local_port = 22 - use_encryption = false - use_compression = false - `), - expected: &XTCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "p2p_tcp", - ProxyType: consts.XTCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.1", - LocalPort: 22, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - RoleServerCommonConf: RoleServerCommonConf{ - Role: "server", - Sk: "abcdefg", - }, - }, - }, - { - sname: "tcpmuxhttpconnect", - source: []byte(` - [tcpmuxhttpconnect] - type = tcpmux - multiplexer = httpconnect - local_ip = 127.0.0.1 - local_port = 10701 - custom_domains = tunnel1 - `), - expected: &TCPMuxProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "tcpmuxhttpconnect", - ProxyType: consts.TCPMuxProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.1", - LocalPort: 10701, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - DomainConf: DomainConf{ - CustomDomains: []string{"tunnel1"}, - SubDomain: "", - }, - Multiplexer: "httpconnect", - }, - }, - } - - for _, c := range testcases { - f, err := ini.LoadSources(testLoadOptions, c.source) - assert.NoError(err) - - proxyType := f.Section(c.sname).Key("type").String() - assert.NotEmpty(proxyType) - - actual := DefaultProxyConf(proxyType) - assert.NotNil(actual) - - err = actual.UnmarshalFromIni(testProxyPrefix, c.sname, f.Section(c.sname)) - assert.NoError(err) - assert.Equal(c.expected, actual) - } -} - -func Test_RangeProxy_UnmarshalFromIni(t *testing.T) { - assert := assert.New(t) - - testcases := []struct { - sname string - source []byte - expected map[string]ProxyConf - }{ - { - sname: "range:tcp_port", - source: []byte(` - [range:tcp_port] - type = tcp - local_ip = 127.0.0.9 - local_port = 6010-6011,6019 - remote_port = 6010-6011,6019 - use_encryption = false - use_compression = false - `), - expected: map[string]ProxyConf{ - "tcp_port_0": &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "tcp_port_0", - ProxyType: consts.TCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 6010, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - RemotePort: 6010, - }, - "tcp_port_1": &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "tcp_port_1", - ProxyType: consts.TCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 6011, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - RemotePort: 6011, - }, - "tcp_port_2": &TCPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "tcp_port_2", - ProxyType: consts.TCPProxy, - LocalSvrConf: LocalSvrConf{ - LocalIP: "127.0.0.9", - LocalPort: 6019, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - RemotePort: 6019, - }, - }, - }, - { - sname: "range:udp_port", - source: []byte(` - [range:udp_port] - type = udp - local_ip = 114.114.114.114 - local_port = 6000,6010-6011 - remote_port = 6000,6010-6011 - use_encryption - use_compression - `), - expected: map[string]ProxyConf{ - "udp_port_0": &UDPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "udp_port_0", - ProxyType: consts.UDPProxy, - UseEncryption: true, - UseCompression: true, - LocalSvrConf: LocalSvrConf{ - LocalIP: "114.114.114.114", - LocalPort: 6000, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - RemotePort: 6000, - }, - "udp_port_1": &UDPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "udp_port_1", - ProxyType: consts.UDPProxy, - UseEncryption: true, - UseCompression: true, - LocalSvrConf: LocalSvrConf{ - LocalIP: "114.114.114.114", - LocalPort: 6010, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - RemotePort: 6010, - }, - "udp_port_2": &UDPProxyConf{ - BaseProxyConf: BaseProxyConf{ - ProxyName: testProxyPrefix + "udp_port_2", - ProxyType: consts.UDPProxy, - UseEncryption: true, - UseCompression: true, - LocalSvrConf: LocalSvrConf{ - LocalIP: "114.114.114.114", - LocalPort: 6011, - }, - BandwidthLimitMode: BandwidthLimitModeClient, - }, - RemotePort: 6011, - }, - }, - }, - } - - for _, c := range testcases { - - f, err := ini.LoadSources(testLoadOptions, c.source) - assert.NoError(err) - - actual := make(map[string]ProxyConf) - s := f.Section(c.sname) - - err = renderRangeProxyTemplates(f, s) - assert.NoError(err) - - f.DeleteSection(ini.DefaultSection) - f.DeleteSection(c.sname) - - for _, section := range f.Sections() { - proxyType := section.Key("type").String() - newsname := section.Name() - - tmp := DefaultProxyConf(proxyType) - err = tmp.UnmarshalFromIni(testProxyPrefix, newsname, section) - assert.NoError(err) - - actual[newsname] = tmp - } - - assert.Equal(c.expected, actual) - } -} diff --git a/pkg/config/server_test.go b/pkg/config/server_test.go deleted file mode 100644 index 7218397b7ac..00000000000 --- a/pkg/config/server_test.go +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright 2020 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 config - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/fatedier/frp/pkg/auth" - plugin "github.com/fatedier/frp/pkg/plugin/server" -) - -func Test_LoadServerCommonConf(t *testing.T) { - assert := assert.New(t) - - testcases := []struct { - source []byte - expected ServerCommonConf - }{ - { - source: []byte(` - # [common] is integral section - [common] - bind_addr = 0.0.0.9 - bind_port = 7009 - kcp_bind_port = 7007 - proxy_bind_addr = 127.0.0.9 - vhost_http_port = 89 - vhost_https_port = 449 - vhost_http_timeout = 69 - tcpmux_httpconnect_port = 1339 - dashboard_addr = 0.0.0.9 - dashboard_port = 7509 - dashboard_user = admin9 - dashboard_pwd = admin9 - enable_prometheus - assets_dir = ./static9 - log_file = ./frps.log9 - log_way = file - log_level = info9 - log_max_days = 39 - disable_log_color = false - detailed_errors_to_client - authentication_method = token - authenticate_heartbeats = false - authenticate_new_work_conns = false - token = 123456789 - oidc_issuer = test9 - oidc_audience = test9 - oidc_skip_expiry_check - oidc_skip_issuer_check - heartbeat_timeout = 99 - user_conn_timeout = 9 - allow_ports = 10-12,99 - max_pool_count = 59 - max_ports_per_client = 9 - tls_only = false - tls_cert_file = server.crt - tls_key_file = server.key - tls_trusted_ca_file = ca.crt - subdomain_host = frps.com - tcp_mux - udp_packet_size = 1509 - [plugin.user-manager] - addr = 127.0.0.1:9009 - path = /handler - ops = Login - [plugin.port-manager] - addr = 127.0.0.1:9009 - path = /handler - ops = NewProxy - tls_verify - `), - expected: ServerCommonConf{ - ServerConfig: auth.ServerConfig{ - BaseConfig: auth.BaseConfig{ - AuthenticationMethod: "token", - AuthenticateHeartBeats: false, - AuthenticateNewWorkConns: false, - }, - TokenConfig: auth.TokenConfig{ - Token: "123456789", - }, - OidcServerConfig: auth.OidcServerConfig{ - OidcIssuer: "test9", - OidcAudience: "test9", - OidcSkipExpiryCheck: true, - OidcSkipIssuerCheck: true, - }, - }, - BindAddr: "0.0.0.9", - BindPort: 7009, - KCPBindPort: 7007, - QUICKeepalivePeriod: 10, - QUICMaxIdleTimeout: 30, - QUICMaxIncomingStreams: 100000, - ProxyBindAddr: "127.0.0.9", - VhostHTTPPort: 89, - VhostHTTPSPort: 449, - VhostHTTPTimeout: 69, - TCPMuxHTTPConnectPort: 1339, - DashboardAddr: "0.0.0.9", - DashboardPort: 7509, - DashboardUser: "admin9", - DashboardPwd: "admin9", - EnablePrometheus: true, - AssetsDir: "./static9", - LogFile: "./frps.log9", - LogWay: "file", - LogLevel: "info9", - LogMaxDays: 39, - DisableLogColor: false, - DetailedErrorsToClient: true, - HeartbeatTimeout: 99, - UserConnTimeout: 9, - AllowPorts: map[int]struct{}{ - 10: {}, - 11: {}, - 12: {}, - 99: {}, - }, - AllowPortsStr: "10-12,99", - MaxPoolCount: 59, - MaxPortsPerClient: 9, - TLSOnly: true, - TLSCertFile: "server.crt", - TLSKeyFile: "server.key", - TLSTrustedCaFile: "ca.crt", - SubDomainHost: "frps.com", - TCPMux: true, - TCPMuxKeepaliveInterval: 60, - TCPKeepAlive: 7200, - UDPPacketSize: 1509, - NatHoleAnalysisDataReserveHours: 7 * 24, - - HTTPPlugins: map[string]plugin.HTTPPluginOptions{ - "user-manager": { - Name: "user-manager", - Addr: "127.0.0.1:9009", - Path: "/handler", - Ops: []string{"Login"}, - }, - "port-manager": { - Name: "port-manager", - Addr: "127.0.0.1:9009", - Path: "/handler", - Ops: []string{"NewProxy"}, - TLSVerify: true, - }, - }, - }, - }, - { - source: []byte(` - # [common] is integral section - [common] - bind_addr = 0.0.0.9 - bind_port = 7009 - `), - expected: ServerCommonConf{ - ServerConfig: auth.ServerConfig{ - BaseConfig: auth.BaseConfig{ - AuthenticationMethod: "token", - AuthenticateHeartBeats: false, - AuthenticateNewWorkConns: false, - }, - }, - BindAddr: "0.0.0.9", - BindPort: 7009, - QUICKeepalivePeriod: 10, - QUICMaxIdleTimeout: 30, - QUICMaxIncomingStreams: 100000, - ProxyBindAddr: "0.0.0.9", - VhostHTTPTimeout: 60, - DashboardAddr: "0.0.0.0", - DashboardUser: "", - DashboardPwd: "", - EnablePrometheus: false, - LogFile: "console", - LogWay: "console", - LogLevel: "info", - LogMaxDays: 3, - DetailedErrorsToClient: true, - TCPMux: true, - TCPMuxKeepaliveInterval: 60, - TCPKeepAlive: 7200, - AllowPorts: make(map[int]struct{}), - MaxPoolCount: 5, - HeartbeatTimeout: 90, - UserConnTimeout: 10, - HTTPPlugins: make(map[string]plugin.HTTPPluginOptions), - UDPPacketSize: 1500, - NatHoleAnalysisDataReserveHours: 7 * 24, - }, - }, - } - - for _, c := range testcases { - actual, err := UnmarshalServerConfFromIni(c.source) - assert.NoError(err) - actual.Complete() - assert.Equal(c.expected, actual) - } -} diff --git a/pkg/config/types.go b/pkg/config/types/types.go similarity index 58% rename from pkg/config/types.go rename to pkg/config/types/types.go index 7aefee12600..fac29d74612 100644 --- a/pkg/config/types.go +++ b/pkg/config/types/types.go @@ -12,11 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -package config +package types import ( "encoding/json" "errors" + "fmt" "strconv" "strings" ) @@ -123,3 +124,65 @@ func (q *BandwidthQuantity) MarshalJSON() ([]byte, error) { func (q *BandwidthQuantity) Bytes() int64 { return q.i } + +type PortsRange struct { + Start int `json:"start,omitempty"` + End int `json:"end,omitempty"` + Single int `json:"single,omitempty"` +} + +type PortsRangeSlice []PortsRange + +func (p PortsRangeSlice) String() string { + if len(p) == 0 { + return "" + } + strs := []string{} + for _, v := range p { + if v.Single > 0 { + strs = append(strs, strconv.Itoa(v.Single)) + } else { + strs = append(strs, strconv.Itoa(v.Start)+"-"+strconv.Itoa(v.End)) + } + } + return strings.Join(strs, ",") +} + +// the format of str is like "1000-2000,3000,4000-5000" +func NewPortsRangeSliceFromString(str string) ([]PortsRange, error) { + str = strings.TrimSpace(str) + out := []PortsRange{} + numRanges := strings.Split(str, ",") + for _, numRangeStr := range numRanges { + // 1000-2000 or 2001 + numArray := strings.Split(numRangeStr, "-") + // length: only 1 or 2 is correct + rangeType := len(numArray) + switch rangeType { + case 1: + // single number + singleNum, err := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64) + if err != nil { + return nil, fmt.Errorf("range number is invalid, %v", err) + } + out = append(out, PortsRange{Single: int(singleNum)}) + case 2: + // range numbers + min, err := strconv.ParseInt(strings.TrimSpace(numArray[0]), 10, 64) + if err != nil { + return nil, fmt.Errorf("range number is invalid, %v", err) + } + max, err := strconv.ParseInt(strings.TrimSpace(numArray[1]), 10, 64) + if err != nil { + return nil, fmt.Errorf("range number is invalid, %v", err) + } + if max < min { + return nil, fmt.Errorf("range number is invalid") + } + out = append(out, PortsRange{Start: int(min), End: int(max)}) + default: + return nil, fmt.Errorf("range number is invalid") + } + } + return out, nil +} diff --git a/pkg/config/types_test.go b/pkg/config/types/types_test.go similarity index 54% rename from pkg/config/types_test.go rename to pkg/config/types/types_test.go index ab03dfd2a34..8843de5af81 100644 --- a/pkg/config/types_test.go +++ b/pkg/config/types/types_test.go @@ -12,13 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -package config +package types import ( "encoding/json" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) type Wrap struct { @@ -27,14 +27,46 @@ type Wrap struct { } func TestBandwidthQuantity(t *testing.T) { - assert := assert.New(t) + require := require.New(t) var w Wrap err := json.Unmarshal([]byte(`{"b":"1KB","int":5}`), &w) - assert.NoError(err) - assert.EqualValues(1*KB, w.B.Bytes()) + require.NoError(err) + require.EqualValues(1*KB, w.B.Bytes()) buf, err := json.Marshal(&w) - assert.NoError(err) - assert.Equal(`{"b":"1KB","int":5}`, string(buf)) + require.NoError(err) + require.Equal(`{"b":"1KB","int":5}`, string(buf)) +} + +func TestPortsRangeSlice2String(t *testing.T) { + require := require.New(t) + + ports := []PortsRange{ + { + Start: 1000, + End: 2000, + }, + { + Single: 3000, + }, + } + str := PortsRangeSlice(ports).String() + require.Equal("1000-2000,3000", str) +} + +func TestNewPortsRangeSliceFromString(t *testing.T) { + require := require.New(t) + + ports, err := NewPortsRangeSliceFromString("1000-2000,3000") + require.NoError(err) + require.Equal([]PortsRange{ + { + Start: 1000, + End: 2000, + }, + { + Single: 3000, + }, + }, ports) } diff --git a/pkg/config/v1/api.go b/pkg/config/v1/api.go new file mode 100644 index 00000000000..0991fc93513 --- /dev/null +++ b/pkg/config/v1/api.go @@ -0,0 +1,19 @@ +// Copyright 2023 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 v1 + +type APIMetadata struct { + Version string `json:"version"` +} diff --git a/pkg/config/v1/client.go b/pkg/config/v1/client.go new file mode 100644 index 00000000000..76e317108f7 --- /dev/null +++ b/pkg/config/v1/client.go @@ -0,0 +1,199 @@ +// Copyright 2023 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 v1 + +import ( + "os" + + "github.com/samber/lo" + + "github.com/fatedier/frp/pkg/util/util" +) + +type ClientConfig struct { + ClientCommonConfig + + Proxies []TypedProxyConfig `json:"proxies,omitempty"` + Visitors []TypedVisitorConfig `json:"visitors,omitempty"` +} + +type ClientCommonConfig struct { + APIMetadata + + Auth AuthClientConfig `json:"auth,omitempty"` + // User specifies a prefix for proxy names to distinguish them from other + // clients. If this value is not "", proxy names will automatically be + // changed to "{user}.{proxy_name}". + User string `json:"user,omitempty"` + + // ServerAddr specifies the address of the server to connect to. By + // default, this value is "0.0.0.0". + ServerAddr string `json:"serverAddr,omitempty"` + // ServerPort specifies the port to connect to the server on. By default, + // this value is 7000. + ServerPort int `json:"serverPort,omitempty"` + // STUN server to help penetrate NAT hole. + NatHoleSTUNServer string `json:"natHoleStunServer,omitempty"` + // DNSServer specifies a DNS server address for FRPC to use. If this value + // is "", the default DNS will be used. + DNSServer string `json:"dnsServer,omitempty"` + // LoginFailExit controls whether or not the client should exit after a + // failed login attempt. If false, the client will retry until a login + // attempt succeeds. By default, this value is true. + LoginFailExit *bool `json:"loginFailExit,omitempty"` + // Start specifies a set of enabled proxies by name. If this set is empty, + // all supplied proxies are enabled. By default, this value is an empty + // set. + Start []string `json:"start,omitempty"` + + Log LogConfig `json:"log,omitempty"` + WebServer WebServerConfig `json:"webServer,omitempty"` + Transport ClientTransportConfig `json:"transport,omitempty"` + + // UDPPacketSize specifies the udp packet size + // By default, this value is 1500 + UDPPacketSize int64 `json:"udpPacketSize,omitempty"` + // Client metadata info + Metadatas map[string]string `json:"metadatas,omitempty"` + + // Include other config files for proxies. + IncludeConfigFiles []string `json:"includes,omitempty"` +} + +func (c *ClientCommonConfig) Complete() { + c.ServerAddr = util.EmptyOr(c.ServerAddr, "0.0.0.0") + c.ServerPort = util.EmptyOr(c.ServerPort, 7000) + c.LoginFailExit = util.EmptyOr(c.LoginFailExit, lo.ToPtr(true)) + + c.Auth.Complete() + c.Log.Complete() + c.Transport.Complete() + c.WebServer.Complete() + + c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500) +} + +type ClientTransportConfig struct { + // Protocol specifies the protocol to use when interacting with the server. + // Valid values are "tcp", "kcp", "quic", "websocket" and "wss". By default, this value + // is "tcp". + Protocol string `json:"protocol,omitempty"` + // The maximum amount of time a dial to server will wait for a connect to complete. + DialServerTimeout int64 `json:"dialServerTimeout,omitempty"` + // DialServerKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps. + // If negative, keep-alive probes are disabled. + DialServerKeepAlive int64 `json:"dialServerKeepalive,omitempty"` + // ConnectServerLocalIP specifies the address of the client bind when it connect to server. + // Note: This value only use in TCP/Websocket protocol. Not support in KCP protocol. + ConnectServerLocalIP string `json:"connectServerLocalIP,omitempty"` + // ProxyURL specifies a proxy address to connect to the server through. If + // this value is "", the server will be connected to directly. By default, + // this value is read from the "http_proxy" environment variable. + ProxyURL string `json:"proxyURL,omitempty"` + // PoolCount specifies the number of connections the client will make to + // the server in advance. + PoolCount int `json:"poolCount,omitempty"` + // TCPMux toggles TCP stream multiplexing. This allows multiple requests + // from a client to share a single TCP connection. If this value is true, + // the server must have TCP multiplexing enabled as well. By default, this + // value is true. + TCPMux *bool `json:"tcpMux,omitempty"` + // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler. + // If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux. + TCPMuxKeepaliveInterval int64 `json:"tcpMuxKeepaliveInterval,omitempty"` + // QUIC protocol options. + QUIC QUICOptions `json:"quic,omitempty"` + // HeartBeatInterval specifies at what interval heartbeats are sent to the + // server, in seconds. It is not recommended to change this value. By + // default, this value is 30. Set negative value to disable it. + HeartbeatInterval int64 `json:"heartbeatInterval,omitempty"` + // HeartBeatTimeout specifies the maximum allowed heartbeat response delay + // before the connection is terminated, in seconds. It is not recommended + // to change this value. By default, this value is 90. Set negative value to disable it. + HeartbeatTimeout int64 `json:"heartbeatTimeout,omitempty"` + // TLS specifies TLS settings for the connection to the server. + TLS TLSClientConfig `json:"tls,omitempty"` +} + +func (c *ClientTransportConfig) Complete() { + c.Protocol = util.EmptyOr(c.Protocol, "tcp") + c.DialServerTimeout = util.EmptyOr(c.DialServerTimeout, 10) + c.DialServerKeepAlive = util.EmptyOr(c.DialServerKeepAlive, 7200) + c.ProxyURL = util.EmptyOr(c.ProxyURL, os.Getenv("http_proxy")) + c.PoolCount = util.EmptyOr(c.PoolCount, 1) + c.TCPMux = util.EmptyOr(c.TCPMux, lo.ToPtr(true)) + c.TCPMuxKeepaliveInterval = util.EmptyOr(c.TCPMuxKeepaliveInterval, 60) + c.HeartbeatInterval = util.EmptyOr(c.HeartbeatInterval, 30) + c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, 90) + c.QUIC.Complete() + c.TLS.Complete() +} + +type TLSClientConfig struct { + // TLSEnable specifies whether or not TLS should be used when communicating + // with the server. If "tls.certFile" and "tls.keyFile" are valid, + // client will load the supplied tls configuration. + // Since v0.50.0, the default value has been changed to true, and tls is enabled by default. + Enable *bool `json:"enable,omitempty"` + // If DisableCustomTLSFirstByte is set to false, frpc will establish a connection with frps using the + // 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. + DisableCustomTLSFirstByte *bool `json:"disableCustomTLSFirstByte,omitempty"` + + TLSConfig +} + +func (c *TLSClientConfig) Complete() { + c.Enable = util.EmptyOr(c.Enable, lo.ToPtr(true)) + c.DisableCustomTLSFirstByte = util.EmptyOr(c.DisableCustomTLSFirstByte, lo.ToPtr(true)) +} + +type AuthClientConfig struct { + // Method specifies what authentication method to use to + // authenticate frpc with frps. If "token" is specified - token will be + // read into login message. If "oidc" is specified - OIDC (Open ID Connect) + // token will be issued using OIDC settings. By default, this value is "token". + Method AuthMethod `json:"method,omitempty"` + // Specify whether to include auth info in additional scope. + // Current supported scopes are: "HeartBeats", "NewWorkConns". + AdditionalScopes []AuthScope `json:"additionalScopes,omitempty"` + // Token specifies the authorization token used to create keys to be sent + // to the server. The server must have a matching token for authorization + // to succeed. By default, this value is "". + Token string `json:"token,omitempty"` + OIDC AuthOIDCClientConfig `json:"oidc,omitempty"` +} + +func (c *AuthClientConfig) Complete() { + c.Method = util.EmptyOr(c.Method, "token") +} + +type AuthOIDCClientConfig struct { + // ClientID specifies the client ID to use to get a token in OIDC authentication. + ClientID string `json:"clientID,omitempty"` + // ClientSecret specifies the client secret to use to get a token in OIDC + // authentication. + ClientSecret string `json:"clientSecret,omitempty"` + // Audience specifies the audience of the token in OIDC authentication. + Audience string `json:"audience,omitempty"` + // Scope specifies the scope of the token in OIDC authentication. + Scope string `json:"scope,omitempty"` + // TokenEndpointURL specifies the URL which implements OIDC Token Endpoint. + // It will be used to get an OIDC token. + TokenEndpointURL string `json:"tokenEndpointURL,omitempty"` + // AdditionalEndpointParams specifies additional parameters to be sent + // this field will be transfer to map[string][]string in OIDC token generator. + AdditionalEndpointParams map[string]string `json:"additionalEndpointParams,omitempty"` +} diff --git a/pkg/config/v1/client_test.go b/pkg/config/v1/client_test.go new file mode 100644 index 00000000000..9a3662caa86 --- /dev/null +++ b/pkg/config/v1/client_test.go @@ -0,0 +1,34 @@ +// Copyright 2023 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 v1 + +import ( + "testing" + + "github.com/samber/lo" + "github.com/stretchr/testify/require" +) + +func TestClientConfigComplete(t *testing.T) { + require := require.New(t) + c := &ClientConfig{} + c.Complete() + + require.EqualValues("token", c.Auth.Method) + require.Equal(true, lo.FromPtr(c.Transport.TCPMux)) + require.Equal(true, lo.FromPtr(c.LoginFailExit)) + require.Equal(true, lo.FromPtr(c.Transport.TLS.Enable)) + require.Equal(true, lo.FromPtr(c.Transport.TLS.DisableCustomTLSFirstByte)) +} diff --git a/pkg/config/v1/common.go b/pkg/config/v1/common.go new file mode 100644 index 00000000000..119c024cbfd --- /dev/null +++ b/pkg/config/v1/common.go @@ -0,0 +1,117 @@ +// Copyright 2023 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 v1 + +import ( + "github.com/fatedier/frp/pkg/util/util" +) + +type AuthScope string + +const ( + AuthScopeHeartBeats AuthScope = "HeartBeats" + AuthScopeNewWorkConns AuthScope = "NewWorkConns" +) + +type AuthMethod string + +const ( + AuthMethodToken AuthMethod = "token" + AuthMethodOIDC AuthMethod = "oidc" +) + +// QUIC protocol options +type QUICOptions struct { + KeepalivePeriod int `json:"keepalivePeriod,omitempty"` + MaxIdleTimeout int `json:"maxIdleTimeout,omitempty"` + MaxIncomingStreams int `json:"maxIncomingStreams,omitempty"` +} + +func (c *QUICOptions) Complete() { + c.KeepalivePeriod = util.EmptyOr(c.KeepalivePeriod, 10) + c.MaxIdleTimeout = util.EmptyOr(c.MaxIdleTimeout, 30) + c.MaxIncomingStreams = util.EmptyOr(c.MaxIncomingStreams, 100000) +} + +type WebServerConfig struct { + // This is the network address to bind on for serving the web interface and API. + // By default, this value is "127.0.0.1". + Addr string `json:"addr,omitempty"` + // Port specifies the port for the web server to listen on. If this + // value is 0, the admin server will not be started. + Port int `json:"port,omitempty"` + // User specifies the username that the web server will use for login. + User string `json:"user,omitempty"` + // Password specifies the password that the admin server will use for login. + Password string `json:"password,omitempty"` + // AssetsDir specifies the local directory that the admin server will load + // resources from. If this value is "", assets will be loaded from the + // bundled executable using embed package. + AssetsDir string `json:"assetsDir,omitempty"` + // Enable golang pprof handlers. + PprofEnable bool `json:"pprofEnable,omitempty"` + // Enable TLS if TLSConfig is not nil. + TLS *TLSConfig `json:"tls,omitempty"` +} + +func (c *WebServerConfig) Complete() { + c.Addr = util.EmptyOr(c.Addr, "127.0.0.1") +} + +type TLSConfig struct { + // CertPath specifies the path of the cert file that client will load. + CertFile string `json:"certFile,omitempty"` + // KeyPath specifies the path of the secret key file that client will load. + KeyFile string `json:"keyFile,omitempty"` + // TrustedCaFile specifies the path of the trusted ca file that will load. + TrustedCaFile string `json:"trustedCaFile,omitempty"` + // ServerName specifies the custom server name of tls certificate. By + // default, server name if same to ServerAddr. + ServerName string `json:"serverName,omitempty"` +} + +type LogConfig struct { + // This is destination where frp should wirte the logs. + // If "console" is used, logs will be printed to stdout, otherwise, + // logs will be written to the specified file. + // By default, this value is "console". + To string `json:"to,omitempty"` + // Level specifies the minimum log level. Valid values are "trace", + // "debug", "info", "warn", and "error". By default, this value is "info". + Level string `json:"level,omitempty"` + // MaxDays specifies the maximum number of days to store log information + // before deletion. + MaxDays int64 `json:"maxDays"` + // DisablePrintColor disables log colors when log.to is "console". + DisablePrintColor bool `json:"disablePrintColor,omitempty"` +} + +func (c *LogConfig) Complete() { + c.To = util.EmptyOr(c.To, "console") + c.Level = util.EmptyOr(c.Level, "info") + c.MaxDays = util.EmptyOr(c.MaxDays, 3) +} + +type HTTPPluginOptions struct { + Name string `json:"name"` + Addr string `json:"addr"` + Path string `json:"path"` + Ops []string `json:"ops"` + TLSVerify bool `json:"tls_verify,omitempty"` +} + +type HeaderOperations struct { + Set map[string]string `json:"set,omitempty"` +} diff --git a/pkg/config/v1/plugin.go b/pkg/config/v1/plugin.go new file mode 100644 index 00000000000..a01d10227be --- /dev/null +++ b/pkg/config/v1/plugin.go @@ -0,0 +1,118 @@ +// Copyright 2023 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 v1 + +import ( + "encoding/json" + "errors" + "fmt" + "reflect" +) + +type ClientPluginOptions interface{} + +type TypedClientPluginOptions struct { + Type string `json:"type"` + ClientPluginOptions +} + +func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error { + if len(b) == 4 && string(b) == "null" { + return errors.New("type is required") + } + + typeStruct := struct { + Type string `json:"type"` + }{} + if err := json.Unmarshal(b, &typeStruct); err != nil { + return err + } + + c.Type = typeStruct.Type + + v, ok := clientPluginOptionsTypeMap[typeStruct.Type] + if !ok { + return fmt.Errorf("unknown plugin type: %s", typeStruct.Type) + } + options := reflect.New(v).Interface().(ClientPluginOptions) + if err := json.Unmarshal(b, options); err != nil { + return err + } + c.ClientPluginOptions = options + return nil +} + +const ( + PluginHTTP2HTTPS = "http2https" + PluginHTTPProxy = "http_proxy" + PluginHTTPS2HTTP = "https2http" + PluginHTTPS2HTTPS = "https2https" + PluginSocks5 = "socks5" + PluginStaticFile = "static_file" + PluginUnixDomainSocket = "unix_domain_socket" +) + +var clientPluginOptionsTypeMap = map[string]reflect.Type{ + PluginHTTP2HTTPS: reflect.TypeOf(HTTP2HTTPSPluginOptions{}), + PluginHTTPProxy: reflect.TypeOf(HTTPProxyPluginOptions{}), + PluginHTTPS2HTTP: reflect.TypeOf(HTTPS2HTTPPluginOptions{}), + PluginHTTPS2HTTPS: reflect.TypeOf(HTTPS2HTTPSPluginOptions{}), + PluginSocks5: reflect.TypeOf(Socks5PluginOptions{}), + PluginStaticFile: reflect.TypeOf(StaticFilePluginOptions{}), + PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}), +} + +type HTTP2HTTPSPluginOptions struct { + LocalAddr string `json:"localAddr,omitempty"` + HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"` + RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` +} + +type HTTPProxyPluginOptions struct { + HTTPUser string `json:"httpUser,omitempty"` + HTTPPassword string `json:"httpPassword,omitempty"` +} + +type HTTPS2HTTPPluginOptions struct { + LocalAddr string `json:"localAddr,omitempty"` + HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"` + RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` + CrtPath string `json:"crtPath,omitempty"` + KeyPath string `json:"keyPath,omitempty"` +} + +type HTTPS2HTTPSPluginOptions struct { + LocalAddr string `json:"localAddr,omitempty"` + HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"` + RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` + CrtPath string `json:"crtPath,omitempty"` + KeyPath string `json:"keyPath,omitempty"` +} + +type Socks5PluginOptions struct { + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` +} + +type StaticFilePluginOptions struct { + LocalPath string `json:"localPath,omitempty"` + StripPrefix string `json:"stripPrefix,omitempty"` + HTTPUser string `json:"httpUser,omitempty"` + HTTPPassword string `json:"httpPassword,omitempty"` +} + +type UnixDomainSocketPluginOptions struct { + UnixPath string `json:"unixPath,omitempty"` +} diff --git a/pkg/config/v1/proxy.go b/pkg/config/v1/proxy.go new file mode 100644 index 00000000000..41bb13414b7 --- /dev/null +++ b/pkg/config/v1/proxy.go @@ -0,0 +1,438 @@ +// Copyright 2023 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 v1 + +import ( + "encoding/json" + "errors" + "fmt" + "reflect" + + "github.com/samber/lo" + + "github.com/fatedier/frp/pkg/config/types" + "github.com/fatedier/frp/pkg/msg" + "github.com/fatedier/frp/pkg/util/util" +) + +type ProxyTransport struct { + // UseEncryption controls whether or not communication with the server will + // be encrypted. Encryption is done using the tokens supplied in the server + // and client configuration. + UseEncryption bool `json:"useEncryption,omitempty"` + // UseCompression controls whether or not communication with the server + // will be compressed. + UseCompression bool `json:"useCompression,omitempty"` + // BandwidthLimit limit the bandwidth + // 0 means no limit + BandwidthLimit types.BandwidthQuantity `json:"bandwidthLimit,omitempty"` + // BandwidthLimitMode specifies whether to limit the bandwidth on the + // client or server side. Valid values include "client" and "server". + // By default, this value is "client". + BandwidthLimitMode string `json:"bandwidthLimitMode,omitempty"` + // ProxyProtocolVersion specifies which protocol version to use. Valid + // values include "v1", "v2", and "". If the value is "", a protocol + // version will be automatically selected. By default, this value is "". + ProxyProtocolVersion string `json:"proxyProtocolVersion,omitempty"` +} + +type LoadBalancerConfig struct { + // Group specifies which group the is a part of. The server will use + // this information to load balance proxies in the same group. If the value + // is "", this will not be in a group. + Group string `json:"group"` + // GroupKey specifies a group key, which should be the same among proxies + // of the same group. + GroupKey string `json:"groupKey,omitempty"` +} + +type ProxyBackend struct { + // LocalIP specifies the IP address or host name of the backend. + LocalIP string `json:"localIP,omitempty"` + // LocalPort specifies the port of the backend. + LocalPort int `json:"localPort,omitempty"` + + // Plugin specifies what plugin should be used for handling connections. If this value + // is set, the LocalIP and LocalPort values will be ignored. + Plugin TypedClientPluginOptions `json:"plugin,omitempty"` +} + +// HealthCheckConfig configures health checking. This can be useful for load +// balancing purposes to detect and remove proxies to failing services. +type HealthCheckConfig struct { + // Type specifies what protocol to use for health checking. + // Valid values include "tcp", "http", and "". If this value is "", health + // checking will not be performed. + // + // If the type is "tcp", a connection will be attempted to the target + // server. If a connection cannot be established, the health check fails. + // + // If the type is "http", a GET request will be made to the endpoint + // specified by HealthCheckURL. If the response is not a 200, the health + // check fails. + Type string `json:"type"` // tcp | http + // TimeoutSeconds specifies the number of seconds to wait for a health + // check attempt to connect. If the timeout is reached, this counts as a + // health check failure. By default, this value is 3. + TimeoutSeconds int `json:"timeoutSeconds,omitempty"` + // MaxFailed specifies the number of allowed failures before the + // is stopped. By default, this value is 1. + MaxFailed int `json:"maxFailed,omitempty"` + // IntervalSeconds specifies the time in seconds between health + // checks. By default, this value is 10. + IntervalSeconds int `json:"intervalSeconds"` + // Path specifies the path to send health checks to if the + // health check type is "http". + Path string `json:"path,omitempty"` +} + +type DomainConfig struct { + CustomDomains []string `json:"customDomains,omitempty"` + SubDomain string `json:"subdomain,omitempty"` +} + +type ProxyBaseConfig struct { + Name string `json:"name"` + Type string `json:"type"` + Transport ProxyTransport `json:"transport,omitempty"` + // metadata info for each proxy + Metadatas map[string]string `json:"metadatas,omitempty"` + LoadBalancer LoadBalancerConfig `json:"loadBalancer,omitempty"` + HealthCheck HealthCheckConfig `json:"healthCheck,omitempty"` + ProxyBackend +} + +func (c *ProxyBaseConfig) GetBaseConfig() *ProxyBaseConfig { + return c +} + +func (c *ProxyBaseConfig) Complete(namePrefix string) { + c.Name = lo.Ternary(namePrefix == "", "", namePrefix+".") + c.Name + c.LocalIP = util.EmptyOr(c.LocalIP, "127.0.0.1") + c.Transport.BandwidthLimitMode = util.EmptyOr(c.Transport.BandwidthLimitMode, types.BandwidthLimitModeClient) +} + +func (c *ProxyBaseConfig) MarshalToMsg(m *msg.NewProxy) { + m.ProxyName = c.Name + m.ProxyType = c.Type + m.UseEncryption = c.Transport.UseEncryption + m.UseCompression = c.Transport.UseCompression + m.BandwidthLimit = c.Transport.BandwidthLimit.String() + // leave it empty for default value to reduce traffic + if c.Transport.BandwidthLimitMode != "client" { + m.BandwidthLimitMode = c.Transport.BandwidthLimitMode + } + m.Group = c.LoadBalancer.Group + m.GroupKey = c.LoadBalancer.GroupKey + m.Metas = c.Metadatas +} + +func (c *ProxyBaseConfig) UnmarshalFromMsg(m *msg.NewProxy) { + c.Name = m.ProxyName + c.Type = m.ProxyType + c.Transport.UseEncryption = m.UseEncryption + c.Transport.UseCompression = m.UseCompression + if m.BandwidthLimit != "" { + c.Transport.BandwidthLimit, _ = types.NewBandwidthQuantity(m.BandwidthLimit) + } + if m.BandwidthLimitMode != "" { + c.Transport.BandwidthLimitMode = m.BandwidthLimitMode + } + c.LoadBalancer.Group = m.Group + c.LoadBalancer.GroupKey = m.GroupKey + c.Metadatas = m.Metas +} + +type TypedProxyConfig struct { + Type string `json:"type"` + ProxyConfigurer +} + +func (c *TypedProxyConfig) UnmarshalJSON(b []byte) error { + if len(b) == 4 && string(b) == "null" { + return errors.New("type is required") + } + + typeStruct := struct { + Type string `json:"type"` + }{} + if err := json.Unmarshal(b, &typeStruct); err != nil { + return err + } + + c.Type = typeStruct.Type + configurer := NewProxyConfigurerByType(ProxyType(typeStruct.Type)) + if configurer == nil { + return fmt.Errorf("unknown proxy type: %s", typeStruct.Type) + } + if err := json.Unmarshal(b, configurer); err != nil { + return err + } + c.ProxyConfigurer = configurer + return nil +} + +type ProxyConfigurer interface { + Complete(namePrefix string) + GetBaseConfig() *ProxyBaseConfig + // MarshalToMsg marshals this config into a msg.NewProxy message. This + // function will be called on the frpc side. + MarshalToMsg(*msg.NewProxy) + // UnmarshalFromMsg unmarshals a msg.NewProxy message into this config. + // This function will be called on the frps side. + UnmarshalFromMsg(*msg.NewProxy) +} + +type ProxyType string + +const ( + ProxyTypeTCP ProxyType = "tcp" + ProxyTypeUDP ProxyType = "udp" + ProxyTypeTCPMUX ProxyType = "tcpmux" + ProxyTypeHTTP ProxyType = "http" + ProxyTypeHTTPS ProxyType = "https" + ProxyTypeSTCP ProxyType = "stcp" + ProxyTypeXTCP ProxyType = "xtcp" + ProxyTypeSUDP ProxyType = "sudp" +) + +var proxyConfigTypeMap = map[ProxyType]reflect.Type{ + ProxyTypeTCP: reflect.TypeOf(TCPProxyConfig{}), + ProxyTypeUDP: reflect.TypeOf(UDPProxyConfig{}), + ProxyTypeHTTP: reflect.TypeOf(HTTPProxyConfig{}), + ProxyTypeHTTPS: reflect.TypeOf(HTTPSProxyConfig{}), + ProxyTypeTCPMUX: reflect.TypeOf(TCPMuxProxyConfig{}), + ProxyTypeSTCP: reflect.TypeOf(STCPProxyConfig{}), + ProxyTypeXTCP: reflect.TypeOf(XTCPProxyConfig{}), + ProxyTypeSUDP: reflect.TypeOf(SUDPProxyConfig{}), +} + +func NewProxyConfigurerByType(proxyType ProxyType) ProxyConfigurer { + v, ok := proxyConfigTypeMap[proxyType] + if !ok { + return nil + } + return reflect.New(v).Interface().(ProxyConfigurer) +} + +var _ ProxyConfigurer = &TCPProxyConfig{} + +type TCPProxyConfig struct { + ProxyBaseConfig + + RemotePort int `json:"remotePort,omitempty"` +} + +func (c *TCPProxyConfig) MarshalToMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.MarshalToMsg(m) + + m.RemotePort = c.RemotePort +} + +func (c *TCPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.UnmarshalFromMsg(m) + + c.RemotePort = m.RemotePort +} + +var _ ProxyConfigurer = &UDPProxyConfig{} + +type UDPProxyConfig struct { + ProxyBaseConfig + + RemotePort int `json:"remotePort,omitempty"` +} + +func (c *UDPProxyConfig) MarshalToMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.MarshalToMsg(m) + + m.RemotePort = c.RemotePort +} + +func (c *UDPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.UnmarshalFromMsg(m) + + c.RemotePort = m.RemotePort +} + +var _ ProxyConfigurer = &HTTPProxyConfig{} + +type HTTPProxyConfig struct { + ProxyBaseConfig + DomainConfig + + Locations []string `json:"locations,omitempty"` + HTTPUser string `json:"httpUser,omitempty"` + HTTPPassword string `json:"httpPassword,omitempty"` + HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"` + RequestHeaders HeaderOperations `json:"requestHeaders,omitempty"` + RouteByHTTPUser string `json:"routeByHTTPUser,omitempty"` +} + +func (c *HTTPProxyConfig) MarshalToMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.MarshalToMsg(m) + + m.CustomDomains = c.CustomDomains + m.SubDomain = c.SubDomain + m.Locations = c.Locations + m.HostHeaderRewrite = c.HostHeaderRewrite + m.HTTPUser = c.HTTPUser + m.HTTPPwd = c.HTTPPassword + m.Headers = c.RequestHeaders.Set + m.RouteByHTTPUser = c.RouteByHTTPUser +} + +func (c *HTTPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.UnmarshalFromMsg(m) + + c.CustomDomains = m.CustomDomains + c.SubDomain = m.SubDomain + c.Locations = m.Locations + c.HostHeaderRewrite = m.HostHeaderRewrite + c.HTTPUser = m.HTTPUser + c.HTTPPassword = m.HTTPPwd + c.RequestHeaders.Set = m.Headers + c.RouteByHTTPUser = m.RouteByHTTPUser +} + +var _ ProxyConfigurer = &HTTPSProxyConfig{} + +type HTTPSProxyConfig struct { + ProxyBaseConfig + DomainConfig +} + +func (c *HTTPSProxyConfig) MarshalToMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.MarshalToMsg(m) + + m.CustomDomains = c.CustomDomains + m.SubDomain = c.SubDomain +} + +func (c *HTTPSProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.UnmarshalFromMsg(m) + + c.CustomDomains = m.CustomDomains + c.SubDomain = m.SubDomain +} + +type TCPMultiplexerType string + +const ( + TCPMultiplexerHTTPConnect TCPMultiplexerType = "httpconnect" +) + +var _ ProxyConfigurer = &TCPMuxProxyConfig{} + +type TCPMuxProxyConfig struct { + ProxyBaseConfig + DomainConfig + + HTTPUser string `json:"httpUser,omitempty"` + HTTPPassword string `json:"httpPassword,omitempty"` + RouteByHTTPUser string `json:"routeByHTTPUser,omitempty"` + Multiplexer string `json:"multiplexer,omitempty"` +} + +func (c *TCPMuxProxyConfig) MarshalToMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.MarshalToMsg(m) + + m.CustomDomains = c.CustomDomains + m.SubDomain = c.SubDomain + m.Multiplexer = c.Multiplexer + m.HTTPUser = c.HTTPUser + m.HTTPPwd = c.HTTPPassword + m.RouteByHTTPUser = c.RouteByHTTPUser +} + +func (c *TCPMuxProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.UnmarshalFromMsg(m) + + c.CustomDomains = m.CustomDomains + c.SubDomain = m.SubDomain + c.Multiplexer = m.Multiplexer + c.HTTPUser = m.HTTPUser + c.HTTPPassword = m.HTTPPwd + c.RouteByHTTPUser = m.RouteByHTTPUser +} + +var _ ProxyConfigurer = &STCPProxyConfig{} + +type STCPProxyConfig struct { + ProxyBaseConfig + + Secretkey string `json:"secretKey,omitempty"` + AllowUsers []string `json:"allowUsers,omitempty"` +} + +func (c *STCPProxyConfig) MarshalToMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.MarshalToMsg(m) + + m.Sk = c.Secretkey + m.AllowUsers = c.AllowUsers +} + +func (c *STCPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.UnmarshalFromMsg(m) + + c.Secretkey = m.Sk + c.AllowUsers = m.AllowUsers +} + +var _ ProxyConfigurer = &XTCPProxyConfig{} + +type XTCPProxyConfig struct { + ProxyBaseConfig + + Secretkey string `json:"secretKey,omitempty"` + AllowUsers []string `json:"allowUsers,omitempty"` +} + +func (c *XTCPProxyConfig) MarshalToMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.MarshalToMsg(m) + + m.Sk = c.Secretkey + m.AllowUsers = c.AllowUsers +} + +func (c *XTCPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.UnmarshalFromMsg(m) + + c.Secretkey = m.Sk + c.AllowUsers = m.AllowUsers +} + +var _ ProxyConfigurer = &SUDPProxyConfig{} + +type SUDPProxyConfig struct { + ProxyBaseConfig + + Secretkey string `json:"secretKey,omitempty"` + AllowUsers []string `json:"allowUsers,omitempty"` +} + +func (c *SUDPProxyConfig) MarshalToMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.MarshalToMsg(m) + + m.Sk = c.Secretkey + m.AllowUsers = c.AllowUsers +} + +func (c *SUDPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) { + c.ProxyBaseConfig.UnmarshalFromMsg(m) + + c.Secretkey = m.Sk + c.AllowUsers = m.AllowUsers +} diff --git a/pkg/config/v1/proxy_test.go b/pkg/config/v1/proxy_test.go new file mode 100644 index 00000000000..64753250818 --- /dev/null +++ b/pkg/config/v1/proxy_test.go @@ -0,0 +1,49 @@ +// Copyright 2023 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 v1 + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUnmarshalTypedProxyConfig(t *testing.T) { + require := require.New(t) + proxyConfigs := struct { + Proxies []TypedProxyConfig `json:"proxies,omitempty"` + }{} + + strs := `{ + "proxies": [ + { + "type": "tcp", + "localPort": 22, + "remotePort": 6000 + }, + { + "type": "http", + "localPort": 80, + "customDomains": ["www.example.com"] + } + ] + }` + err := json.Unmarshal([]byte(strs), &proxyConfigs) + require.NoError(err) + + require.IsType(&TCPProxyConfig{}, proxyConfigs.Proxies[0].ProxyConfigurer) + require.IsType(&HTTPProxyConfig{}, proxyConfigs.Proxies[1].ProxyConfigurer) +} diff --git a/pkg/config/v1/server.go b/pkg/config/v1/server.go new file mode 100644 index 00000000000..5648d2495e4 --- /dev/null +++ b/pkg/config/v1/server.go @@ -0,0 +1,190 @@ +// Copyright 2023 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 v1 + +import ( + "github.com/samber/lo" + + "github.com/fatedier/frp/pkg/config/types" + "github.com/fatedier/frp/pkg/util/util" +) + +type ServerConfig struct { + APIMetadata + + Auth AuthServerConfig `json:"auth,omitempty"` + // BindAddr specifies the address that the server binds to. By default, + // this value is "0.0.0.0". + BindAddr string `json:"bindAddr,omitempty"` + // BindPort specifies the port that the server listens on. By default, this + // value is 7000. + BindPort int `json:"bindPort,omitempty"` + // KCPBindPort specifies the KCP port that the server listens on. If this + // value is 0, the server will not listen for KCP connections. + KCPBindPort int `json:"kcpBindPort,omitempty"` + // QUICBindPort specifies the QUIC port that the server listens on. + // Set this value to 0 will disable this feature. + QUICBindPort int `json:"quicBindPort,omitempty"` + // ProxyBindAddr specifies the address that the proxy binds to. This value + // may be the same as BindAddr. + ProxyBindAddr string `json:"proxyBindAddr,omitempty"` + // VhostHTTPPort specifies the port that the server listens for HTTP Vhost + // requests. If this value is 0, the server will not listen for HTTP + // requests. + VhostHTTPPort int `json:"vhostHTTPPort,omitempty"` + // VhostHTTPTimeout specifies the response header timeout for the Vhost + // HTTP server, in seconds. By default, this value is 60. + VhostHTTPTimeout int64 `json:"vhostHTTPTimeout,omitempty"` + // VhostHTTPSPort specifies the port that the server listens for HTTPS + // Vhost requests. If this value is 0, the server will not listen for HTTPS + // requests. + VhostHTTPSPort int `json:"vhostHTTPSPort,omitempty"` + // TCPMuxHTTPConnectPort specifies the port that the server listens for TCP + // HTTP CONNECT requests. If the value is 0, the server will not multiplex TCP + // requests on one single port. If it's not - it will listen on this value for + // HTTP CONNECT requests. + TCPMuxHTTPConnectPort int `json:"tcpmuxHTTPConnectPort,omitempty"` + // If TCPMuxPassthrough is true, frps won't do any update on traffic. + TCPMuxPassthrough bool `json:"tcpmuxPassthrough,omitempty"` + // SubDomainHost specifies the domain that will be attached to sub-domains + // requested by the client when using Vhost proxying. For example, if this + // value is set to "frps.com" and the client requested the subdomain + // "test", the resulting URL would be "test.frps.com". + SubDomainHost string `json:"subDomainHost,omitempty"` + // Custom404Page specifies a path to a custom 404 page to display. If this + // value is "", a default page will be displayed. + Custom404Page string `json:"custom404Page,omitempty"` + + WebServer WebServerConfig `json:"webServer,omitempty"` + // EnablePrometheus will export prometheus metrics on webserver address + // in /metrics api. + EnablePrometheus bool `json:"enablePrometheus,omitempty"` + + Log LogConfig `json:"log,omitempty"` + + Transport ServerTransportConfig `json:"transport,omitempty"` + + // DetailedErrorsToClient defines whether to send the specific error (with + // debug info) to frpc. By default, this value is true. + DetailedErrorsToClient *bool `json:"detailedErrorsToClient,omitempty"` + // MaxPortsPerClient specifies the maximum number of ports a single client + // may proxy to. If this value is 0, no limit will be applied. + MaxPortsPerClient int64 `json:"maxPortsPerClient,omitempty"` + // UserConnTimeout specifies the maximum time to wait for a work + // connection. By default, this value is 10. + UserConnTimeout int64 `json:"userConnTimeout,omitempty"` + // UDPPacketSize specifies the UDP packet size + // By default, this value is 1500 + UDPPacketSize int64 `json:"udpPacketSize,omitempty"` + // NatHoleAnalysisDataReserveHours specifies the hours to reserve nat hole analysis data. + NatHoleAnalysisDataReserveHours int64 `json:"natholeAnalysisDataReserveHours,omitempty"` + + AllowPorts []types.PortsRange `json:"allowPorts,omitempty"` + + HTTPPlugins []HTTPPluginOptions `json:"httpPlugins,omitempty"` +} + +func (c *ServerConfig) Complete() { + c.Auth.Complete() + c.Log.Complete() + c.Transport.Complete() + c.WebServer.Complete() + + c.BindAddr = util.EmptyOr(c.BindAddr, "0.0.0.0") + c.BindPort = util.EmptyOr(c.BindPort, 7000) + if c.ProxyBindAddr == "" { + c.ProxyBindAddr = c.BindAddr + } + + if c.WebServer.Port > 0 { + c.WebServer.Addr = util.EmptyOr(c.WebServer.Addr, "0.0.0.0") + } + + c.VhostHTTPTimeout = util.EmptyOr(c.VhostHTTPTimeout, 60) + c.DetailedErrorsToClient = util.EmptyOr(c.DetailedErrorsToClient, lo.ToPtr(true)) + c.UserConnTimeout = util.EmptyOr(c.UserConnTimeout, 10) + c.UDPPacketSize = util.EmptyOr(c.UDPPacketSize, 1500) + c.NatHoleAnalysisDataReserveHours = util.EmptyOr(c.NatHoleAnalysisDataReserveHours, 7*24) +} + +type AuthServerConfig struct { + Method AuthMethod `json:"method,omitempty"` + AdditionalScopes []AuthScope `json:"additionalScopes,omitempty"` + Token string `json:"token,omitempty"` + OIDC AuthOIDCServerConfig `json:"oidc,omitempty"` +} + +func (c *AuthServerConfig) Complete() { + c.Method = util.EmptyOr(c.Method, "token") +} + +type AuthOIDCServerConfig struct { + // Issuer specifies the issuer to verify OIDC tokens with. This issuer + // will be used to load public keys to verify signature and will be compared + // with the issuer claim in the OIDC token. + Issuer string `json:"issuer,omitempty"` + // Audience specifies the audience OIDC tokens should contain when validated. + // If this value is empty, audience ("client ID") verification will be skipped. + Audience string `json:"audience,omitempty"` + // SkipExpiryCheck specifies whether to skip checking if the OIDC token is + // expired. + SkipExpiryCheck bool `json:"skipExpiryCheck,omitempty"` + // SkipIssuerCheck specifies whether to skip checking if the OIDC token's + // issuer claim matches the issuer specified in OidcIssuer. + SkipIssuerCheck bool `json:"skipIssuerCheck,omitempty"` +} + +type ServerTransportConfig struct { + // TCPMux toggles TCP stream multiplexing. This allows multiple requests + // from a client to share a single TCP connection. By default, this value + // is true. + TCPMux *bool `json:"tcpMux,omitempty"` + // TCPMuxKeepaliveInterval specifies the keep alive interval for TCP stream multipler. + // If TCPMux is true, heartbeat of application layer is unnecessary because it can only rely on heartbeat in TCPMux. + TCPMuxKeepaliveInterval int64 `json:"tcpMuxKeepaliveInterval,omitempty"` + // TCPKeepAlive specifies the interval between keep-alive probes for an active network connection between frpc and frps. + // If negative, keep-alive probes are disabled. + TCPKeepAlive int64 `json:"tcpKeepalive,omitempty"` + // MaxPoolCount specifies the maximum pool size for each proxy. By default, + // this value is 5. + MaxPoolCount int64 `json:"maxPoolCount,omitempty"` + // HeartBeatTimeout specifies the maximum time to wait for a heartbeat + // before terminating the connection. It is not recommended to change this + // value. By default, this value is 90. Set negative value to disable it. + HeartbeatTimeout int64 `json:"heartbeatTimeout,omitempty"` + // QUIC options. + QUIC QUICOptions `json:"quic,omitempty"` + // TLS specifies TLS settings for the connection from the client. + TLS TLSServerConfig `json:"tls,omitempty"` +} + +func (c *ServerTransportConfig) Complete() { + c.TCPMux = util.EmptyOr(c.TCPMux, lo.ToPtr(true)) + c.TCPMuxKeepaliveInterval = util.EmptyOr(c.TCPMuxKeepaliveInterval, 60) + c.TCPKeepAlive = util.EmptyOr(c.TCPKeepAlive, 7200) + c.MaxPoolCount = util.EmptyOr(c.MaxPoolCount, 5) + c.HeartbeatTimeout = util.EmptyOr(c.HeartbeatTimeout, 90) + c.QUIC.Complete() + if c.TLS.TrustedCaFile != "" { + c.TLS.Force = true + } +} + +type TLSServerConfig struct { + // Force specifies whether to only accept TLS-encrypted connections. + Force bool `json:"force,omitempty"` + + TLSConfig +} diff --git a/pkg/consts/consts.go b/pkg/config/v1/server_test.go similarity index 52% rename from pkg/consts/consts.go rename to pkg/config/v1/server_test.go index 9e4438349b6..3100fc4b12f 100644 --- a/pkg/consts/consts.go +++ b/pkg/config/v1/server_test.go @@ -1,4 +1,4 @@ -// Copyright 2016 fatedier, fatedier@gmail.com +// Copyright 2023 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. @@ -12,30 +12,21 @@ // See the License for the specific language governing permissions and // limitations under the License. -package consts +package v1 -var ( - // proxy status - Idle = "idle" - Working = "working" - Closed = "closed" - Online = "online" - Offline = "offline" +import ( + "testing" - // proxy type - TCPProxy = "tcp" - UDPProxy = "udp" - TCPMuxProxy = "tcpmux" - HTTPProxy = "http" - HTTPSProxy = "https" - STCPProxy = "stcp" - XTCPProxy = "xtcp" - SUDPProxy = "sudp" + "github.com/samber/lo" + "github.com/stretchr/testify/require" +) - // authentication method - TokenAuthMethod = "token" - OidcAuthMethod = "oidc" +func TestServerConfigComplete(t *testing.T) { + require := require.New(t) + c := &ServerConfig{} + c.Complete() - // TCP multiplexer - HTTPConnectTCPMultiplexer = "httpconnect" -) + require.EqualValues("token", c.Auth.Method) + require.Equal(true, lo.FromPtr(c.Transport.TCPMux)) + require.Equal(true, lo.FromPtr(c.DetailedErrorsToClient)) +} diff --git a/pkg/config/v1/validation/client.go b/pkg/config/v1/validation/client.go new file mode 100644 index 00000000000..38123946a59 --- /dev/null +++ b/pkg/config/v1/validation/client.go @@ -0,0 +1,105 @@ +// Copyright 2023 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 ( + "fmt" + "os" + "path/filepath" + + "github.com/samber/lo" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) { + var ( + warnings Warning + errs error + ) + if !lo.Contains(SupportedAuthMethods, c.Auth.Method) { + errs = AppendError(errs, fmt.Errorf("invalid auth method, optional values are %v", SupportedAuthMethods)) + } + if !lo.Every(SupportedAuthAdditionalScopes, c.Auth.AdditionalScopes) { + errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes)) + } + + if err := validateLogConfig(&c.Log); err != nil { + errs = AppendError(errs, err) + } + + if err := validateWebServerConfig(&c.WebServer); err != nil { + errs = AppendError(errs, err) + } + + if c.Transport.HeartbeatTimeout > 0 && c.Transport.HeartbeatInterval > 0 { + if c.Transport.HeartbeatTimeout < c.Transport.HeartbeatInterval { + errs = AppendError(errs, fmt.Errorf("invalid transport.heartbeatTimeout, heartbeat timeout should not less than heartbeat interval")) + } + } + + if !lo.FromPtr(c.Transport.TLS.Enable) { + checkTLSConfig := func(name string, value string) Warning { + if value != "" { + return fmt.Errorf("%s is invalid when transport.tls.enable is false", name) + } + return nil + } + + warnings = AppendError(warnings, checkTLSConfig("transport.tls.certFile", c.Transport.TLS.CertFile)) + warnings = AppendError(warnings, checkTLSConfig("transport.tls.keyFile", c.Transport.TLS.KeyFile)) + warnings = AppendError(warnings, checkTLSConfig("transport.tls.trustedCaFile", c.Transport.TLS.TrustedCaFile)) + } + + if !lo.Contains(SupportedTransportProtocols, c.Transport.Protocol) { + errs = AppendError(errs, fmt.Errorf("invalid transport.protocol, optional values are %v", SupportedTransportProtocols)) + } + + for _, f := range c.IncludeConfigFiles { + absDir, err := filepath.Abs(filepath.Dir(f)) + if err != nil { + errs = AppendError(errs, fmt.Errorf("include: parse directory of %s failed: %v", f, err)) + continue + } + if _, err := os.Stat(absDir); os.IsNotExist(err) { + errs = AppendError(errs, fmt.Errorf("include: directory of %s not exist", f)) + } + } + return warnings, errs +} + +func ValidateAllClientConfig(c *v1.ClientCommonConfig, pxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) (Warning, error) { + var warnings Warning + if c != nil { + warning, err := ValidateClientCommonConfig(c) + warnings = AppendError(warnings, warning) + if err != nil { + return warnings, err + } + } + + for _, c := range pxyCfgs { + if err := ValidateProxyConfigurerForClient(c); err != nil { + return warnings, fmt.Errorf("proxy %s: %v", c.GetBaseConfig().Name, err) + } + } + + for _, c := range visitorCfgs { + if err := ValidateVisitorConfigurer(c); err != nil { + return warnings, fmt.Errorf("visitor %s: %v", c.GetBaseConfig().Name, err) + } + } + return warnings, nil +} diff --git a/pkg/config/v1/validation/common.go b/pkg/config/v1/validation/common.go new file mode 100644 index 00000000000..c159e270bf3 --- /dev/null +++ b/pkg/config/v1/validation/common.go @@ -0,0 +1,51 @@ +// Copyright 2023 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 ( + "fmt" + + "github.com/samber/lo" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func validateWebServerConfig(c *v1.WebServerConfig) error { + if c.TLS != nil { + if c.TLS.CertFile == "" { + return fmt.Errorf("tls.certFile must be specified when tls is enabled") + } + if c.TLS.KeyFile == "" { + return fmt.Errorf("tls.keyFile must be specified when tls is enabled") + } + } + + return ValidatePort(c.Port, "webServer.port") +} + +// ValidatePort checks that the network port is in range +func ValidatePort(port int, fieldPath string) error { + if 0 <= port && port <= 65535 { + return nil + } + return fmt.Errorf("%s: port number %d must be in the range 0..65535", fieldPath, port) +} + +func validateLogConfig(c *v1.LogConfig) error { + if !lo.Contains(SupportedLogLevels, c.Level) { + return fmt.Errorf("invalid log level, optional values are %v", SupportedLogLevels) + } + return nil +} diff --git a/pkg/config/v1/validation/plugin.go b/pkg/config/v1/validation/plugin.go new file mode 100644 index 00000000000..9c88d142cb2 --- /dev/null +++ b/pkg/config/v1/validation/plugin.go @@ -0,0 +1,72 @@ +// Copyright 2023 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 ( + "errors" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func ValidateClientPluginOptions(c v1.ClientPluginOptions) error { + switch v := c.(type) { + case *v1.HTTP2HTTPSPluginOptions: + return validateHTTP2HTTPSPluginOptions(v) + case *v1.HTTPS2HTTPPluginOptions: + return validateHTTPS2HTTPPluginOptions(v) + case *v1.HTTPS2HTTPSPluginOptions: + return validateHTTPS2HTTPSPluginOptions(v) + case *v1.StaticFilePluginOptions: + return validateStaticFilePluginOptions(v) + case *v1.UnixDomainSocketPluginOptions: + return validateUnixDomainSocketPluginOptions(v) + } + return nil +} + +func validateHTTP2HTTPSPluginOptions(c *v1.HTTP2HTTPSPluginOptions) error { + if c.LocalAddr == "" { + return errors.New("localAddr is required") + } + return nil +} + +func validateHTTPS2HTTPPluginOptions(c *v1.HTTPS2HTTPPluginOptions) error { + if c.LocalAddr == "" { + return errors.New("localAddr is required") + } + return nil +} + +func validateHTTPS2HTTPSPluginOptions(c *v1.HTTPS2HTTPSPluginOptions) error { + if c.LocalAddr == "" { + return errors.New("localAddr is required") + } + return nil +} + +func validateStaticFilePluginOptions(c *v1.StaticFilePluginOptions) error { + if c.LocalPath == "" { + return errors.New("localPath is required") + } + return nil +} + +func validateUnixDomainSocketPluginOptions(c *v1.UnixDomainSocketPluginOptions) error { + if c.UnixPath == "" { + return errors.New("unixPath is required") + } + return nil +} diff --git a/pkg/config/v1/validation/proxy.go b/pkg/config/v1/validation/proxy.go new file mode 100644 index 00000000000..65970e9f484 --- /dev/null +++ b/pkg/config/v1/validation/proxy.go @@ -0,0 +1,233 @@ +// Copyright 2023 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 ( + "errors" + "fmt" + "strings" + + "github.com/samber/lo" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func validateProxyBaseConfigForClient(c *v1.ProxyBaseConfig) error { + if c.Name == "" { + return errors.New("name should not be empty") + } + + 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") + } + + if c.Plugin.Type == "" { + if err := ValidatePort(c.LocalPort, "localPort"); err != nil { + return fmt.Errorf("localPort: %v", err) + } + } + + if !lo.Contains([]string{"", "tcp", "http"}, c.HealthCheck.Type) { + return fmt.Errorf("not support health check type: %s", c.HealthCheck.Type) + } + if c.HealthCheck.Type != "" { + if c.HealthCheck.Type == "http" && + c.HealthCheck.Path == "" { + return fmt.Errorf("health check path should not be empty") + } + } + + if c.Plugin.Type != "" { + if err := ValidateClientPluginOptions(c.Plugin.ClientPluginOptions); err != nil { + return fmt.Errorf("plugin %s: %v", c.Plugin.Type, err) + } + } + return nil +} + +func validateProxyBaseConfigForServer(c *v1.ProxyBaseConfig, s *v1.ServerConfig) error { + return nil +} + +func validateDomainConfigForClient(c *v1.DomainConfig) error { + if c.SubDomain == "" && len(c.CustomDomains) == 0 { + return errors.New("subdomain and custom domains should not be both empty") + } + return nil +} + +func validateDomainConfigForServer(c *v1.DomainConfig, s *v1.ServerConfig) error { + for _, domain := range c.CustomDomains { + if s.SubDomainHost != "" && len(strings.Split(s.SubDomainHost, ".")) < len(strings.Split(domain, ".")) { + if strings.Contains(domain, s.SubDomainHost) { + return fmt.Errorf("custom domain [%s] should not belong to subdomain host [%s]", domain, s.SubDomainHost) + } + } + } + + if c.SubDomain != "" { + if s.SubDomainHost == "" { + return errors.New("subdomain is not supported because this feature is not enabled in server") + } + + if strings.Contains(c.SubDomain, ".") || strings.Contains(c.SubDomain, "*") { + return errors.New("'.' and '*' are not supported in subdomain") + } + } + return nil +} + +func ValidateProxyConfigurerForClient(c v1.ProxyConfigurer) error { + base := c.GetBaseConfig() + if err := validateProxyBaseConfigForClient(base); err != nil { + return err + } + + switch v := c.(type) { + case *v1.TCPProxyConfig: + return validateTCPProxyConfigForClient(v) + case *v1.UDPProxyConfig: + return validateUDPProxyConfigForClient(v) + case *v1.TCPMuxProxyConfig: + return validateTCPMuxProxyConfigForClient(v) + case *v1.HTTPProxyConfig: + return validateHTTPProxyConfigForClient(v) + case *v1.HTTPSProxyConfig: + return validateHTTPSProxyConfigForClient(v) + case *v1.STCPProxyConfig: + return validateSTCPProxyConfigForClient(v) + case *v1.XTCPProxyConfig: + return validateXTCPProxyConfigForClient(v) + case *v1.SUDPProxyConfig: + return validateSUDPProxyConfigForClient(v) + } + return errors.New("unknown proxy config type") +} + +func validateTCPProxyConfigForClient(c *v1.TCPProxyConfig) error { + return nil +} + +func validateUDPProxyConfigForClient(c *v1.UDPProxyConfig) error { + return nil +} + +func validateTCPMuxProxyConfigForClient(c *v1.TCPMuxProxyConfig) error { + if err := validateDomainConfigForClient(&c.DomainConfig); err != nil { + return err + } + + if !lo.Contains([]string{string(v1.TCPMultiplexerHTTPConnect)}, c.Multiplexer) { + return fmt.Errorf("not support multiplexer: %s", c.Multiplexer) + } + return nil +} + +func validateHTTPProxyConfigForClient(c *v1.HTTPProxyConfig) error { + return validateDomainConfigForClient(&c.DomainConfig) +} + +func validateHTTPSProxyConfigForClient(c *v1.HTTPSProxyConfig) error { + return validateDomainConfigForClient(&c.DomainConfig) +} + +func validateSTCPProxyConfigForClient(c *v1.STCPProxyConfig) error { + return nil +} + +func validateXTCPProxyConfigForClient(c *v1.XTCPProxyConfig) error { + return nil +} + +func validateSUDPProxyConfigForClient(c *v1.SUDPProxyConfig) error { + return nil +} + +func ValidateProxyConfigurerForServer(c v1.ProxyConfigurer, s *v1.ServerConfig) error { + base := c.GetBaseConfig() + if err := validateProxyBaseConfigForServer(base, s); err != nil { + return err + } + + switch v := c.(type) { + case *v1.TCPProxyConfig: + return validateTCPProxyConfigForServer(v, s) + case *v1.UDPProxyConfig: + return validateUDPProxyConfigForServer(v, s) + case *v1.TCPMuxProxyConfig: + return validateTCPMuxProxyConfigForServer(v, s) + case *v1.HTTPProxyConfig: + return validateHTTPProxyConfigForServer(v, s) + case *v1.HTTPSProxyConfig: + return validateHTTPSProxyConfigForServer(v, s) + case *v1.STCPProxyConfig: + return validateSTCPProxyConfigForServer(v, s) + case *v1.XTCPProxyConfig: + return validateXTCPProxyConfigForServer(v, s) + case *v1.SUDPProxyConfig: + return validateSUDPProxyConfigForServer(v, s) + default: + return errors.New("unknown proxy config type") + } +} + +func validateTCPProxyConfigForServer(c *v1.TCPProxyConfig, s *v1.ServerConfig) error { + return nil +} + +func validateUDPProxyConfigForServer(c *v1.UDPProxyConfig, s *v1.ServerConfig) error { + return nil +} + +func validateTCPMuxProxyConfigForServer(c *v1.TCPMuxProxyConfig, s *v1.ServerConfig) error { + if c.Multiplexer == string(v1.TCPMultiplexerHTTPConnect) && + s.TCPMuxHTTPConnectPort == 0 { + return fmt.Errorf("tcpmux with multiplexer httpconnect not supported because this feature is not enabled in server") + } + + return validateDomainConfigForServer(&c.DomainConfig, s) +} + +func validateHTTPProxyConfigForServer(c *v1.HTTPProxyConfig, s *v1.ServerConfig) error { + if s.VhostHTTPPort == 0 { + return fmt.Errorf("type [http] not supported when vhost http port is not set") + } + + return validateDomainConfigForServer(&c.DomainConfig, s) +} + +func validateHTTPSProxyConfigForServer(c *v1.HTTPSProxyConfig, s *v1.ServerConfig) error { + if s.VhostHTTPSPort == 0 { + return fmt.Errorf("type [https] not supported when vhost https port is not set") + } + + return validateDomainConfigForServer(&c.DomainConfig, s) +} + +func validateSTCPProxyConfigForServer(c *v1.STCPProxyConfig, s *v1.ServerConfig) error { + return nil +} + +func validateXTCPProxyConfigForServer(c *v1.XTCPProxyConfig, s *v1.ServerConfig) error { + return nil +} + +func validateSUDPProxyConfigForServer(c *v1.SUDPProxyConfig, s *v1.ServerConfig) error { + return nil +} diff --git a/pkg/config/v1/validation/server.go b/pkg/config/v1/validation/server.go new file mode 100644 index 00000000000..5f17d6c7a73 --- /dev/null +++ b/pkg/config/v1/validation/server.go @@ -0,0 +1,58 @@ +// Copyright 2023 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 ( + "fmt" + + "github.com/samber/lo" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func ValidateServerConfig(c *v1.ServerConfig) (Warning, error) { + var ( + warnings Warning + errs error + ) + if !lo.Contains(SupportedAuthMethods, c.Auth.Method) { + errs = AppendError(errs, fmt.Errorf("invalid auth method, optional values are %v", SupportedAuthMethods)) + } + if !lo.Every(SupportedAuthAdditionalScopes, c.Auth.AdditionalScopes) { + errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes)) + } + + if err := validateLogConfig(&c.Log); err != nil { + errs = AppendError(errs, err) + } + + if err := validateWebServerConfig(&c.WebServer); err != nil { + errs = AppendError(errs, err) + } + + errs = AppendError(errs, ValidatePort(c.BindPort, "bindPort")) + errs = AppendError(errs, ValidatePort(c.KCPBindPort, "kcpBindPort")) + errs = AppendError(errs, ValidatePort(c.QUICBindPort, "quicBindPort")) + errs = AppendError(errs, ValidatePort(c.VhostHTTPPort, "vhostHTTPPort")) + errs = AppendError(errs, ValidatePort(c.VhostHTTPSPort, "vhostHTTPSPort")) + errs = AppendError(errs, ValidatePort(c.TCPMuxHTTPConnectPort, "tcpMuxHTTPConnectPort")) + + for _, p := range c.HTTPPlugins { + if !lo.Every(SupportedHTTPPluginOps, p.Ops) { + errs = AppendError(errs, fmt.Errorf("invalid http plugin ops, optional values are %v", SupportedHTTPPluginOps)) + } + } + return warnings, errs +} diff --git a/pkg/config/v1/validation/validation.go b/pkg/config/v1/validation/validation.go new file mode 100644 index 00000000000..4ca6b67f0f0 --- /dev/null +++ b/pkg/config/v1/validation/validation.go @@ -0,0 +1,68 @@ +// Copyright 2023 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 ( + "errors" + + v1 "github.com/fatedier/frp/pkg/config/v1" + splugin "github.com/fatedier/frp/pkg/plugin/server" +) + +var ( + SupportedTransportProtocols = []string{ + "tcp", + "kcp", + "quic", + "websocket", + "wss", + } + + SupportedAuthMethods = []v1.AuthMethod{ + "token", + "oidc", + } + + SupportedAuthAdditionalScopes = []v1.AuthScope{ + "HeartBeats", + "NewWorkConns", + } + + SupportedLogLevels = []string{ + "trace", + "debug", + "info", + "warn", + "error", + } + + SupportedHTTPPluginOps = []string{ + splugin.OpLogin, + splugin.OpNewProxy, + splugin.OpCloseProxy, + splugin.OpPing, + splugin.OpNewWorkConn, + splugin.OpNewUserConn, + } +) + +type Warning error + +func AppendError(err error, errs ...error) error { + if len(errs) == 0 { + return err + } + return errors.Join(append([]error{err}, errs...)...) +} diff --git a/pkg/config/v1/validation/visitor.go b/pkg/config/v1/validation/visitor.go new file mode 100644 index 00000000000..5307dc9053b --- /dev/null +++ b/pkg/config/v1/validation/visitor.go @@ -0,0 +1,63 @@ +// Copyright 2023 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 ( + "errors" + "fmt" + + "github.com/samber/lo" + + v1 "github.com/fatedier/frp/pkg/config/v1" +) + +func ValidateVisitorConfigurer(c v1.VisitorConfigurer) error { + base := c.GetBaseConfig() + if err := validateVisitorBaseConfig(base); err != nil { + return err + } + + switch v := c.(type) { + case *v1.STCPVisitorConfig: + case *v1.SUDPVisitorConfig: + case *v1.XTCPVisitorConfig: + return validateXTCPVisitorConfig(v) + default: + return errors.New("unknown visitor config type") + } + return nil +} + +func validateVisitorBaseConfig(c *v1.VisitorBaseConfig) error { + if c.Name == "" { + return errors.New("name is required") + } + + if c.ServerName == "" { + return errors.New("server name is required") + } + + if c.BindPort == 0 { + return errors.New("bind port is required") + } + return nil +} + +func validateXTCPVisitorConfig(c *v1.XTCPVisitorConfig) error { + if !lo.Contains([]string{"kcp", "quic"}, c.Protocol) { + return fmt.Errorf("protocol should be kcp or quic") + } + return nil +} diff --git a/pkg/config/v1/visitor.go b/pkg/config/v1/visitor.go new file mode 100644 index 00000000000..90ecd86d191 --- /dev/null +++ b/pkg/config/v1/visitor.go @@ -0,0 +1,162 @@ +// Copyright 2023 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 v1 + +import ( + "encoding/json" + "errors" + "fmt" + "reflect" + + "github.com/samber/lo" + + "github.com/fatedier/frp/pkg/util/util" +) + +type VisitorTransport struct { + UseEncryption bool `json:"useEncryption,omitempty"` + UseCompression bool `json:"useCompression,omitempty"` +} + +type VisitorBaseConfig struct { + Name string `json:"name"` + Type string `json:"type"` + Transport VisitorTransport `json:"transport,omitempty"` + SecretKey string `json:"secretKey,omitempty"` + // if the server user is not set, it defaults to the current user + ServerUser string `json:"serverUser,omitempty"` + ServerName string `json:"serverName,omitempty"` + BindAddr string `json:"bindAddr,omitempty"` + // BindPort is the port that visitor listens on. + // It can be less than 0, it means don't bind to the port and only receive connections redirected from + // other visitors. (This is not supported for SUDP now) + BindPort int `json:"bindPort,omitempty"` +} + +func (c *VisitorBaseConfig) GetBaseConfig() *VisitorBaseConfig { + return c +} + +func (c *VisitorBaseConfig) Complete(g *ClientCommonConfig) { + if c.BindAddr == "" { + c.BindAddr = "127.0.0.1" + } + + namePrefix := "" + if g.User != "" { + namePrefix = g.User + "." + } + c.Name = namePrefix + c.Name + + if c.ServerUser != "" { + c.ServerName = c.ServerUser + "." + c.ServerName + } else { + c.ServerName = namePrefix + c.ServerName + } +} + +type VisitorConfigurer interface { + Complete(*ClientCommonConfig) + GetBaseConfig() *VisitorBaseConfig +} + +type VisitorType string + +const ( + VisitorTypeSTCP VisitorType = "stcp" + VisitorTypeXTCP VisitorType = "xtcp" + VisitorTypeSUDP VisitorType = "sudp" +) + +var visitorConfigTypeMap = map[VisitorType]reflect.Type{ + VisitorTypeSTCP: reflect.TypeOf(STCPVisitorConfig{}), + VisitorTypeXTCP: reflect.TypeOf(XTCPVisitorConfig{}), + VisitorTypeSUDP: reflect.TypeOf(SUDPVisitorConfig{}), +} + +type TypedVisitorConfig struct { + Type string `json:"type"` + VisitorConfigurer +} + +func (c *TypedVisitorConfig) UnmarshalJSON(b []byte) error { + if len(b) == 4 && string(b) == "null" { + return errors.New("type is required") + } + + typeStruct := struct { + Type string `json:"type"` + }{} + if err := json.Unmarshal(b, &typeStruct); err != nil { + return err + } + + c.Type = typeStruct.Type + configurer := NewVisitorConfigurerByType(VisitorType(typeStruct.Type)) + if configurer == nil { + return fmt.Errorf("unknown visitor type: %s", typeStruct.Type) + } + if err := json.Unmarshal(b, configurer); err != nil { + return err + } + c.VisitorConfigurer = configurer + return nil +} + +func NewVisitorConfigurerByType(t VisitorType) VisitorConfigurer { + v, ok := visitorConfigTypeMap[t] + if !ok { + return nil + } + return reflect.New(v).Interface().(VisitorConfigurer) +} + +var _ VisitorConfigurer = &STCPVisitorConfig{} + +type STCPVisitorConfig struct { + VisitorBaseConfig +} + +var _ VisitorConfigurer = &SUDPVisitorConfig{} + +type SUDPVisitorConfig struct { + VisitorBaseConfig +} + +var _ VisitorConfigurer = &XTCPVisitorConfig{} + +type XTCPVisitorConfig struct { + VisitorBaseConfig + + Protocol string `json:"protocol,omitempty"` + KeepTunnelOpen bool `json:"keepTunnelOpen,omitempty"` + MaxRetriesAnHour int `json:"maxRetriesAnHour,omitempty"` + MinRetryInterval int `json:"minRetryInterval,omitempty"` + FallbackTo string `json:"fallbackTo,omitempty"` + FallbackTimeoutMs int `json:"fallbackTimeoutMs,omitempty"` +} + +func (c *XTCPVisitorConfig) Complete(g *ClientCommonConfig) { + c.VisitorBaseConfig.Complete(g) + + c.Protocol = util.EmptyOr(c.Protocol, "quic") + c.MaxRetriesAnHour = util.EmptyOr(c.MaxRetriesAnHour, 8) + c.MinRetryInterval = util.EmptyOr(c.MinRetryInterval, 90) + c.FallbackTimeoutMs = util.EmptyOr(c.FallbackTimeoutMs, 1000) + + if c.FallbackTo != "" { + c.FallbackTo = lo.Ternary(g.User == "", "", g.User+".") + c.FallbackTo + } +} diff --git a/pkg/config/visitor_test.go b/pkg/config/visitor_test.go deleted file mode 100644 index 46cbef5ab50..00000000000 --- a/pkg/config/visitor_test.go +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2020 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 config - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "gopkg.in/ini.v1" - - "github.com/fatedier/frp/pkg/consts" -) - -const testVisitorPrefix = "test." - -func Test_Visitor_Interface(_ *testing.T) { - for name := range visitorConfTypeMap { - DefaultVisitorConf(name) - } -} - -func Test_Visitor_UnmarshalFromIni(t *testing.T) { - assert := assert.New(t) - - testcases := []struct { - sname string - source []byte - expected VisitorConf - }{ - { - sname: "secret_tcp_visitor", - source: []byte(` - [secret_tcp_visitor] - role = visitor - type = stcp - server_name = secret_tcp - sk = abcdefg - bind_addr = 127.0.0.1 - bind_port = 9000 - use_encryption = false - use_compression = false - `), - expected: &STCPVisitorConf{ - BaseVisitorConf: BaseVisitorConf{ - ProxyName: testVisitorPrefix + "secret_tcp_visitor", - ProxyType: consts.STCPProxy, - Role: "visitor", - Sk: "abcdefg", - ServerName: testVisitorPrefix + "secret_tcp", - BindAddr: "127.0.0.1", - BindPort: 9000, - }, - }, - }, - { - sname: "p2p_tcp_visitor", - source: []byte(` - [p2p_tcp_visitor] - role = visitor - type = xtcp - server_name = p2p_tcp - sk = abcdefg - bind_addr = 127.0.0.1 - bind_port = 9001 - use_encryption = false - use_compression = false - `), - expected: &XTCPVisitorConf{ - BaseVisitorConf: BaseVisitorConf{ - ProxyName: testVisitorPrefix + "p2p_tcp_visitor", - ProxyType: consts.XTCPProxy, - Role: "visitor", - Sk: "abcdefg", - ServerName: testProxyPrefix + "p2p_tcp", - BindAddr: "127.0.0.1", - BindPort: 9001, - }, - Protocol: "quic", - MaxRetriesAnHour: 8, - MinRetryInterval: 90, - FallbackTimeoutMs: 1000, - }, - }, - } - - for _, c := range testcases { - f, err := ini.LoadSources(testLoadOptions, c.source) - assert.NoError(err) - - visitorType := f.Section(c.sname).Key("type").String() - assert.NotEmpty(visitorType) - - actual := DefaultVisitorConf(visitorType) - assert.NotNil(actual) - - err = actual.UnmarshalFromIni(testVisitorPrefix, c.sname, f.Section(c.sname)) - assert.NoError(err) - assert.Equal(c.expected, actual) - } -} diff --git a/pkg/plugin/client/http2https.go b/pkg/plugin/client/http2https.go index beacab00570..7f093af1b0f 100644 --- a/pkg/plugin/client/http2https.go +++ b/pkg/plugin/client/http2https.go @@ -16,55 +16,34 @@ package plugin import ( "crypto/tls" - "fmt" "io" "net" "net/http" "net/http/httputil" - "strings" + v1 "github.com/fatedier/frp/pkg/config/v1" utilnet "github.com/fatedier/frp/pkg/util/net" ) -const PluginHTTP2HTTPS = "http2https" - func init() { - Register(PluginHTTP2HTTPS, NewHTTP2HTTPSPlugin) + Register(v1.PluginHTTP2HTTPS, NewHTTP2HTTPSPlugin) } type HTTP2HTTPSPlugin struct { - hostHeaderRewrite string - localAddr string - headers map[string]string + opts *v1.HTTP2HTTPSPluginOptions l *Listener s *http.Server } -func NewHTTP2HTTPSPlugin(params map[string]string) (Plugin, error) { - localAddr := params["plugin_local_addr"] - hostHeaderRewrite := params["plugin_host_header_rewrite"] - headers := make(map[string]string) - for k, v := range params { - if !strings.HasPrefix(k, "plugin_header_") { - continue - } - if k = strings.TrimPrefix(k, "plugin_header_"); k != "" { - headers[k] = v - } - } - - if localAddr == "" { - return nil, fmt.Errorf("plugin_local_addr is required") - } +func NewHTTP2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) { + opts := options.(*v1.HTTP2HTTPSPluginOptions) listener := NewProxyListener() p := &HTTP2HTTPSPlugin{ - localAddr: localAddr, - hostHeaderRewrite: hostHeaderRewrite, - headers: headers, - l: listener, + opts: opts, + l: listener, } tr := &http.Transport{ @@ -74,11 +53,11 @@ func NewHTTP2HTTPSPlugin(params map[string]string) (Plugin, error) { rp := &httputil.ReverseProxy{ Director: func(req *http.Request) { req.URL.Scheme = "https" - req.URL.Host = p.localAddr - if p.hostHeaderRewrite != "" { - req.Host = p.hostHeaderRewrite + req.URL.Host = p.opts.LocalAddr + if p.opts.HostHeaderRewrite != "" { + req.Host = p.opts.HostHeaderRewrite } - for k, v := range p.headers { + for k, v := range p.opts.RequestHeaders.Set { req.Header.Set(k, v) } }, @@ -97,13 +76,13 @@ func NewHTTP2HTTPSPlugin(params map[string]string) (Plugin, error) { return p, nil } -func (p *HTTP2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ []byte) { +func (p *HTTP2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn) _ = p.l.PutConn(wrapConn) } func (p *HTTP2HTTPSPlugin) Name() string { - return PluginHTTP2HTTPS + return v1.PluginHTTP2HTTPS } func (p *HTTP2HTTPSPlugin) Close() error { diff --git a/pkg/plugin/client/http_proxy.go b/pkg/plugin/client/http_proxy.go index f9e5dae81d1..06c6296a11d 100644 --- a/pkg/plugin/client/http_proxy.go +++ b/pkg/plugin/client/http_proxy.go @@ -26,32 +26,29 @@ import ( libio "github.com/fatedier/golib/io" libnet "github.com/fatedier/golib/net" + v1 "github.com/fatedier/frp/pkg/config/v1" utilnet "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/util" ) -const PluginHTTPProxy = "http_proxy" - func init() { - Register(PluginHTTPProxy, NewHTTPProxyPlugin) + Register(v1.PluginHTTPProxy, NewHTTPProxyPlugin) } type HTTPProxy struct { - l *Listener - s *http.Server - AuthUser string - AuthPasswd string + opts *v1.HTTPProxyPluginOptions + + l *Listener + s *http.Server } -func NewHTTPProxyPlugin(params map[string]string) (Plugin, error) { - user := params["plugin_http_user"] - passwd := params["plugin_http_passwd"] +func NewHTTPProxyPlugin(options v1.ClientPluginOptions) (Plugin, error) { + opts := options.(*v1.HTTPProxyPluginOptions) listener := NewProxyListener() hp := &HTTPProxy{ - l: listener, - AuthUser: user, - AuthPasswd: passwd, + l: listener, + opts: opts, } hp.s = &http.Server{ @@ -65,10 +62,10 @@ func NewHTTPProxyPlugin(params map[string]string) (Plugin, error) { } func (hp *HTTPProxy) Name() string { - return PluginHTTPProxy + return v1.PluginHTTPProxy } -func (hp *HTTPProxy) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ []byte) { +func (hp *HTTPProxy) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn) sc, rd := libnet.NewSharedConn(wrapConn) @@ -162,7 +159,7 @@ func (hp *HTTPProxy) ConnectHandler(rw http.ResponseWriter, req *http.Request) { } func (hp *HTTPProxy) Auth(req *http.Request) bool { - if hp.AuthUser == "" && hp.AuthPasswd == "" { + if hp.opts.HTTPUser == "" && hp.opts.HTTPPassword == "" { return true } @@ -181,8 +178,8 @@ func (hp *HTTPProxy) Auth(req *http.Request) bool { return false } - if !util.ConstantTimeEqString(pair[0], hp.AuthUser) || - !util.ConstantTimeEqString(pair[1], hp.AuthPasswd) { + if !util.ConstantTimeEqString(pair[0], hp.opts.HTTPUser) || + !util.ConstantTimeEqString(pair[1], hp.opts.HTTPPassword) { time.Sleep(200 * time.Millisecond) return false } diff --git a/pkg/plugin/client/https2http.go b/pkg/plugin/client/https2http.go index 71cc025c646..aa498f3f1a9 100644 --- a/pkg/plugin/client/https2http.go +++ b/pkg/plugin/client/https2http.go @@ -21,67 +21,40 @@ import ( "net" "net/http" "net/http/httputil" - "strings" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/transport" utilnet "github.com/fatedier/frp/pkg/util/net" ) -const PluginHTTPS2HTTP = "https2http" - func init() { - Register(PluginHTTPS2HTTP, NewHTTPS2HTTPPlugin) + Register(v1.PluginHTTPS2HTTP, NewHTTPS2HTTPPlugin) } type HTTPS2HTTPPlugin struct { - crtPath string - keyPath string - hostHeaderRewrite string - localAddr string - headers map[string]string + opts *v1.HTTPS2HTTPPluginOptions l *Listener s *http.Server } -func NewHTTPS2HTTPPlugin(params map[string]string) (Plugin, error) { - crtPath := params["plugin_crt_path"] - keyPath := params["plugin_key_path"] - localAddr := params["plugin_local_addr"] - hostHeaderRewrite := params["plugin_host_header_rewrite"] - headers := make(map[string]string) - for k, v := range params { - if !strings.HasPrefix(k, "plugin_header_") { - continue - } - if k = strings.TrimPrefix(k, "plugin_header_"); k != "" { - headers[k] = v - } - } - - if localAddr == "" { - return nil, fmt.Errorf("plugin_local_addr is required") - } - +func NewHTTPS2HTTPPlugin(options v1.ClientPluginOptions) (Plugin, error) { + opts := options.(*v1.HTTPS2HTTPPluginOptions) listener := NewProxyListener() p := &HTTPS2HTTPPlugin{ - crtPath: crtPath, - keyPath: keyPath, - localAddr: localAddr, - hostHeaderRewrite: hostHeaderRewrite, - headers: headers, - l: listener, + opts: opts, + l: listener, } rp := &httputil.ReverseProxy{ Director: func(req *http.Request) { req.URL.Scheme = "http" - req.URL.Host = p.localAddr - if p.hostHeaderRewrite != "" { - req.Host = p.hostHeaderRewrite + req.URL.Host = p.opts.LocalAddr + if p.opts.HostHeaderRewrite != "" { + req.Host = p.opts.HostHeaderRewrite } - for k, v := range p.headers { + for k, v := range p.opts.RequestHeaders.Set { req.Header.Set(k, v) } }, @@ -95,7 +68,7 @@ func NewHTTPS2HTTPPlugin(params map[string]string) (Plugin, error) { tlsConfig *tls.Config err error ) - if crtPath != "" || keyPath != "" { + if opts.CrtPath != "" || opts.KeyPath != "" { tlsConfig, err = p.genTLSConfig() } else { tlsConfig, err = transport.NewServerTLSConfig("", "", "") @@ -113,7 +86,7 @@ func NewHTTPS2HTTPPlugin(params map[string]string) (Plugin, error) { } func (p *HTTPS2HTTPPlugin) genTLSConfig() (*tls.Config, error) { - cert, err := tls.LoadX509KeyPair(p.crtPath, p.keyPath) + cert, err := tls.LoadX509KeyPair(p.opts.CrtPath, p.opts.KeyPath) if err != nil { return nil, err } @@ -122,13 +95,13 @@ func (p *HTTPS2HTTPPlugin) genTLSConfig() (*tls.Config, error) { return config, nil } -func (p *HTTPS2HTTPPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ []byte) { +func (p *HTTPS2HTTPPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn) _ = p.l.PutConn(wrapConn) } func (p *HTTPS2HTTPPlugin) Name() string { - return PluginHTTPS2HTTP + return v1.PluginHTTPS2HTTP } func (p *HTTPS2HTTPPlugin) Close() error { diff --git a/pkg/plugin/client/https2https.go b/pkg/plugin/client/https2https.go index d925ec24ad2..fc38f62b364 100644 --- a/pkg/plugin/client/https2https.go +++ b/pkg/plugin/client/https2https.go @@ -21,57 +21,31 @@ import ( "net" "net/http" "net/http/httputil" - "strings" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/transport" utilnet "github.com/fatedier/frp/pkg/util/net" ) -const PluginHTTPS2HTTPS = "https2https" - func init() { - Register(PluginHTTPS2HTTPS, NewHTTPS2HTTPSPlugin) + Register(v1.PluginHTTPS2HTTPS, NewHTTPS2HTTPSPlugin) } type HTTPS2HTTPSPlugin struct { - crtPath string - keyPath string - hostHeaderRewrite string - localAddr string - headers map[string]string + opts *v1.HTTPS2HTTPSPluginOptions l *Listener s *http.Server } -func NewHTTPS2HTTPSPlugin(params map[string]string) (Plugin, error) { - crtPath := params["plugin_crt_path"] - keyPath := params["plugin_key_path"] - localAddr := params["plugin_local_addr"] - hostHeaderRewrite := params["plugin_host_header_rewrite"] - headers := make(map[string]string) - for k, v := range params { - if !strings.HasPrefix(k, "plugin_header_") { - continue - } - if k = strings.TrimPrefix(k, "plugin_header_"); k != "" { - headers[k] = v - } - } - - if localAddr == "" { - return nil, fmt.Errorf("plugin_local_addr is required") - } +func NewHTTPS2HTTPSPlugin(options v1.ClientPluginOptions) (Plugin, error) { + opts := options.(*v1.HTTPS2HTTPSPluginOptions) listener := NewProxyListener() p := &HTTPS2HTTPSPlugin{ - crtPath: crtPath, - keyPath: keyPath, - localAddr: localAddr, - hostHeaderRewrite: hostHeaderRewrite, - headers: headers, - l: listener, + opts: opts, + l: listener, } tr := &http.Transport{ @@ -81,11 +55,11 @@ func NewHTTPS2HTTPSPlugin(params map[string]string) (Plugin, error) { rp := &httputil.ReverseProxy{ Director: func(req *http.Request) { req.URL.Scheme = "https" - req.URL.Host = p.localAddr - if p.hostHeaderRewrite != "" { - req.Host = p.hostHeaderRewrite + req.URL.Host = p.opts.LocalAddr + if p.opts.HostHeaderRewrite != "" { + req.Host = p.opts.HostHeaderRewrite } - for k, v := range p.headers { + for k, v := range p.opts.RequestHeaders.Set { req.Header.Set(k, v) } }, @@ -100,7 +74,7 @@ func NewHTTPS2HTTPSPlugin(params map[string]string) (Plugin, error) { tlsConfig *tls.Config err error ) - if crtPath != "" || keyPath != "" { + if opts.CrtPath != "" || opts.KeyPath != "" { tlsConfig, err = p.genTLSConfig() } else { tlsConfig, err = transport.NewServerTLSConfig("", "", "") @@ -118,7 +92,7 @@ func NewHTTPS2HTTPSPlugin(params map[string]string) (Plugin, error) { } func (p *HTTPS2HTTPSPlugin) genTLSConfig() (*tls.Config, error) { - cert, err := tls.LoadX509KeyPair(p.crtPath, p.keyPath) + cert, err := tls.LoadX509KeyPair(p.opts.CrtPath, p.opts.KeyPath) if err != nil { return nil, err } @@ -127,13 +101,13 @@ func (p *HTTPS2HTTPSPlugin) genTLSConfig() (*tls.Config, error) { return config, nil } -func (p *HTTPS2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ []byte) { +func (p *HTTPS2HTTPSPlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn) _ = p.l.PutConn(wrapConn) } func (p *HTTPS2HTTPSPlugin) Name() string { - return PluginHTTPS2HTTPS + return v1.PluginHTTPS2HTTPS } func (p *HTTPS2HTTPSPlugin) Close() error { diff --git a/pkg/plugin/client/plugin.go b/pkg/plugin/client/plugin.go index 6850919a3c1..520e379441f 100644 --- a/pkg/plugin/client/plugin.go +++ b/pkg/plugin/client/plugin.go @@ -21,32 +21,41 @@ import ( "sync" "github.com/fatedier/golib/errors" + pp "github.com/pires/go-proxyproto" + + v1 "github.com/fatedier/frp/pkg/config/v1" ) // Creators is used for create plugins to handle connections. var creators = make(map[string]CreatorFn) // params has prefix "plugin_" -type CreatorFn func(params map[string]string) (Plugin, error) +type CreatorFn func(options v1.ClientPluginOptions) (Plugin, error) func Register(name string, fn CreatorFn) { + if _, exist := creators[name]; exist { + panic(fmt.Sprintf("plugin [%s] is already registered", name)) + } creators[name] = fn } -func Create(name string, params map[string]string) (p Plugin, err error) { +func Create(name string, options v1.ClientPluginOptions) (p Plugin, err error) { if fn, ok := creators[name]; ok { - p, err = fn(params) + p, err = fn(options) } else { err = fmt.Errorf("plugin [%s] is not registered", name) } return } +type ExtraInfo struct { + ProxyProtocolHeader *pp.Header +} + type Plugin interface { Name() string - // extraBufToLocal will send to local connection first, then join conn with local connection - Handle(conn io.ReadWriteCloser, realConn net.Conn, extraBufToLocal []byte) + Handle(conn io.ReadWriteCloser, realConn net.Conn, extra *ExtraInfo) Close() error } diff --git a/pkg/plugin/client/socks5.go b/pkg/plugin/client/socks5.go index 805f956b907..c2e253d241f 100644 --- a/pkg/plugin/client/socks5.go +++ b/pkg/plugin/client/socks5.go @@ -21,28 +21,26 @@ import ( gosocks5 "github.com/armon/go-socks5" + v1 "github.com/fatedier/frp/pkg/config/v1" utilnet "github.com/fatedier/frp/pkg/util/net" ) -const PluginSocks5 = "socks5" - func init() { - Register(PluginSocks5, NewSocks5Plugin) + Register(v1.PluginSocks5, NewSocks5Plugin) } type Socks5Plugin struct { Server *gosocks5.Server } -func NewSocks5Plugin(params map[string]string) (p Plugin, err error) { - user := params["plugin_user"] - passwd := params["plugin_passwd"] +func NewSocks5Plugin(options v1.ClientPluginOptions) (p Plugin, err error) { + opts := options.(*v1.Socks5PluginOptions) cfg := &gosocks5.Config{ Logger: log.New(io.Discard, "", log.LstdFlags), } - if user != "" || passwd != "" { - cfg.Credentials = gosocks5.StaticCredentials(map[string]string{user: passwd}) + if opts.Username != "" || opts.Password != "" { + cfg.Credentials = gosocks5.StaticCredentials(map[string]string{opts.Username: opts.Password}) } sp := &Socks5Plugin{} sp.Server, err = gosocks5.New(cfg) @@ -50,14 +48,14 @@ func NewSocks5Plugin(params map[string]string) (p Plugin, err error) { return } -func (sp *Socks5Plugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ []byte) { +func (sp *Socks5Plugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { defer conn.Close() wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn) _ = sp.Server.ServeConn(wrapConn) } func (sp *Socks5Plugin) Name() string { - return PluginSocks5 + return v1.PluginSocks5 } func (sp *Socks5Plugin) Close() error { diff --git a/pkg/plugin/client/static_file.go b/pkg/plugin/client/static_file.go index 45f731bbf6e..20b79a099da 100644 --- a/pkg/plugin/client/static_file.go +++ b/pkg/plugin/client/static_file.go @@ -22,51 +22,41 @@ import ( "github.com/gorilla/mux" + v1 "github.com/fatedier/frp/pkg/config/v1" utilnet "github.com/fatedier/frp/pkg/util/net" ) -const PluginStaticFile = "static_file" - func init() { - Register(PluginStaticFile, NewStaticFilePlugin) + Register(v1.PluginStaticFile, NewStaticFilePlugin) } type StaticFilePlugin struct { - localPath string - stripPrefix string - httpUser string - httpPasswd string + opts *v1.StaticFilePluginOptions l *Listener s *http.Server } -func NewStaticFilePlugin(params map[string]string) (Plugin, error) { - localPath := params["plugin_local_path"] - stripPrefix := params["plugin_strip_prefix"] - httpUser := params["plugin_http_user"] - httpPasswd := params["plugin_http_passwd"] +func NewStaticFilePlugin(options v1.ClientPluginOptions) (Plugin, error) { + opts := options.(*v1.StaticFilePluginOptions) listener := NewProxyListener() sp := &StaticFilePlugin{ - localPath: localPath, - stripPrefix: stripPrefix, - httpUser: httpUser, - httpPasswd: httpPasswd, + opts: opts, l: listener, } var prefix string - if stripPrefix != "" { - prefix = "/" + stripPrefix + "/" + if opts.StripPrefix != "" { + prefix = "/" + opts.StripPrefix + "/" } else { prefix = "/" } router := mux.NewRouter() - router.Use(utilnet.NewHTTPAuthMiddleware(httpUser, httpPasswd).SetAuthFailDelay(200 * time.Millisecond).Middleware) - router.PathPrefix(prefix).Handler(utilnet.MakeHTTPGzipHandler(http.StripPrefix(prefix, http.FileServer(http.Dir(localPath))))).Methods("GET") + router.Use(utilnet.NewHTTPAuthMiddleware(opts.HTTPUser, opts.HTTPPassword).SetAuthFailDelay(200 * time.Millisecond).Middleware) + router.PathPrefix(prefix).Handler(utilnet.MakeHTTPGzipHandler(http.StripPrefix(prefix, http.FileServer(http.Dir(opts.LocalPath))))).Methods("GET") sp.s = &http.Server{ Handler: router, } @@ -76,13 +66,13 @@ func NewStaticFilePlugin(params map[string]string) (Plugin, error) { return sp, nil } -func (sp *StaticFilePlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ []byte) { +func (sp *StaticFilePlugin) Handle(conn io.ReadWriteCloser, realConn net.Conn, _ *ExtraInfo) { wrapConn := utilnet.WrapReadWriteCloserToConn(conn, realConn) _ = sp.l.PutConn(wrapConn) } func (sp *StaticFilePlugin) Name() string { - return PluginStaticFile + return v1.PluginStaticFile } func (sp *StaticFilePlugin) Close() error { diff --git a/pkg/plugin/client/unix_domain_socket.go b/pkg/plugin/client/unix_domain_socket.go index 03371356033..f186ec925ea 100644 --- a/pkg/plugin/client/unix_domain_socket.go +++ b/pkg/plugin/client/unix_domain_socket.go @@ -15,31 +15,26 @@ package plugin import ( - "fmt" "io" "net" libio "github.com/fatedier/golib/io" -) -const PluginUnixDomainSocket = "unix_domain_socket" + v1 "github.com/fatedier/frp/pkg/config/v1" +) func init() { - Register(PluginUnixDomainSocket, NewUnixDomainSocketPlugin) + Register(v1.PluginUnixDomainSocket, NewUnixDomainSocketPlugin) } type UnixDomainSocketPlugin struct { UnixAddr *net.UnixAddr } -func NewUnixDomainSocketPlugin(params map[string]string) (p Plugin, err error) { - unixPath, ok := params["plugin_unix_path"] - if !ok { - err = fmt.Errorf("plugin_unix_path not found") - return - } +func NewUnixDomainSocketPlugin(options v1.ClientPluginOptions) (p Plugin, err error) { + opts := options.(*v1.UnixDomainSocketPluginOptions) - unixAddr, errRet := net.ResolveUnixAddr("unix", unixPath) + unixAddr, errRet := net.ResolveUnixAddr("unix", opts.UnixPath) if errRet != nil { err = errRet return @@ -51,13 +46,13 @@ func NewUnixDomainSocketPlugin(params map[string]string) (p Plugin, err error) { return } -func (uds *UnixDomainSocketPlugin) Handle(conn io.ReadWriteCloser, _ net.Conn, extraBufToLocal []byte) { +func (uds *UnixDomainSocketPlugin) Handle(conn io.ReadWriteCloser, _ net.Conn, extra *ExtraInfo) { localConn, err := net.DialUnix("unix", nil, uds.UnixAddr) if err != nil { return } - if len(extraBufToLocal) > 0 { - if _, err := localConn.Write(extraBufToLocal); err != nil { + if extra.ProxyProtocolHeader != nil { + if _, err := extra.ProxyProtocolHeader.WriteTo(localConn); err != nil { return } } @@ -66,7 +61,7 @@ func (uds *UnixDomainSocketPlugin) Handle(conn io.ReadWriteCloser, _ net.Conn, e } func (uds *UnixDomainSocketPlugin) Name() string { - return PluginUnixDomainSocket + return v1.PluginUnixDomainSocket } func (uds *UnixDomainSocketPlugin) Close() error { diff --git a/pkg/plugin/server/http.go b/pkg/plugin/server/http.go index 0f561204ce4..7108b7fb185 100644 --- a/pkg/plugin/server/http.go +++ b/pkg/plugin/server/http.go @@ -25,24 +25,18 @@ import ( "net/url" "reflect" "strings" -) -type HTTPPluginOptions struct { - Name string `ini:"name"` - Addr string `ini:"addr"` - Path string `ini:"path"` - Ops []string `ini:"ops"` - TLSVerify bool `ini:"tls_verify"` -} + v1 "github.com/fatedier/frp/pkg/config/v1" +) type httpPlugin struct { - options HTTPPluginOptions + options v1.HTTPPluginOptions url string client *http.Client } -func NewHTTPPluginOptions(options HTTPPluginOptions) Plugin { +func NewHTTPPluginOptions(options v1.HTTPPluginOptions) Plugin { url := fmt.Sprintf("%s%s", options.Addr, options.Path) var client *http.Client diff --git a/test/e2e/pkg/sdk/client/client.go b/pkg/sdk/client/client.go similarity index 79% rename from test/e2e/pkg/sdk/client/client.go rename to pkg/sdk/client/client.go index 3d157073e77..c9657905f24 100644 --- a/test/e2e/pkg/sdk/client/client.go +++ b/pkg/sdk/client/client.go @@ -10,7 +10,7 @@ import ( "strings" "github.com/fatedier/frp/client" - "github.com/fatedier/frp/test/e2e/pkg/utils" + "github.com/fatedier/frp/pkg/util/util" ) type Client struct { @@ -53,6 +53,22 @@ func (c *Client) GetProxyStatus(name string) (*client.ProxyStatusResp, error) { return nil, fmt.Errorf("no proxy status found") } +func (c *Client) GetAllProxyStatus() (client.StatusResp, error) { + req, err := http.NewRequest("GET", "http://"+c.address+"/api/status", nil) + if err != nil { + return nil, err + } + content, err := c.do(req) + if err != nil { + return nil, err + } + allStatus := make(client.StatusResp) + if err = json.Unmarshal([]byte(content), &allStatus); err != nil { + return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content)) + } + return allStatus, nil +} + func (c *Client) Reload() error { req, err := http.NewRequest("GET", "http://"+c.address+"/api/reload", nil) if err != nil { @@ -90,7 +106,7 @@ func (c *Client) UpdateConfig(content string) error { func (c *Client) setAuthHeader(req *http.Request) { if c.authUser != "" || c.authPwd != "" { - req.Header.Set("Authorization", utils.BasicAuth(c.authUser, c.authPwd)) + req.Header.Set("Authorization", util.BasicAuth(c.authUser, c.authPwd)) } } diff --git a/pkg/util/log/log.go b/pkg/util/log/log.go index 01a454d595d..a1391b3260a 100644 --- a/pkg/util/log/log.go +++ b/pkg/util/log/log.go @@ -29,15 +29,14 @@ func init() { Log.SetLogFuncCallDepth(Log.GetLogFuncCallDepth() + 1) } -func InitLog(logWay string, logFile string, logLevel string, maxdays int64, disableLogColor bool) { - SetLogFile(logWay, logFile, maxdays, disableLogColor) +func InitLog(logFile string, logLevel string, maxdays int64, disableLogColor bool) { + SetLogFile(logFile, maxdays, disableLogColor) SetLogLevel(logLevel) } // SetLogFile to configure log params -// logWay: file or console -func SetLogFile(logWay string, logFile string, maxdays int64, disableLogColor bool) { - if logWay == "console" { +func SetLogFile(logFile string, maxdays int64, disableLogColor bool) { + if logFile == "console" { params := "" if disableLogColor { params = `{"color": false}` diff --git a/pkg/util/tcpmux/httpconnect.go b/pkg/util/tcpmux/httpconnect.go index 650891f40b1..17989adcad5 100644 --- a/pkg/util/tcpmux/httpconnect.go +++ b/pkg/util/tcpmux/httpconnect.go @@ -40,7 +40,8 @@ func NewHTTPConnectTCPMuxer(listener net.Listener, passthrough bool, timeout tim ret := &HTTPConnectTCPMuxer{passthrough: passthrough} mux, err := vhost.NewMuxer(listener, ret.getHostFromHTTPConnect, timeout) mux.SetCheckAuthFunc(ret.auth). - SetSuccessHookFunc(ret.sendConnectResponse) + SetSuccessHookFunc(ret.sendConnectResponse). + SetFailHookFunc(vhostFailed) ret.Muxer = mux return ret, err } @@ -92,6 +93,15 @@ func (muxer *HTTPConnectTCPMuxer) auth(c net.Conn, username, password string, re return false, nil } +func vhostFailed(c net.Conn) { + res := vhost.NotFoundResponse() + if res.Body != nil { + defer res.Body.Close() + } + _ = res.Write(c) + _ = c.Close() +} + func (muxer *HTTPConnectTCPMuxer) getHostFromHTTPConnect(c net.Conn) (net.Conn, map[string]string, error) { reqInfoMap := make(map[string]string, 0) sc, rd := libnet.NewSharedConn(c) diff --git a/pkg/util/util/http.go b/pkg/util/util/http.go index d89af08b76a..a6a25a4cbe1 100644 --- a/pkg/util/util/http.go +++ b/pkg/util/util/http.go @@ -1,4 +1,4 @@ -// Copyright 2020 guylewin, guy@lewin.co.il +// Copyright 2023 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. @@ -95,3 +95,8 @@ func ParseBasicAuth(auth string) (username, password string, ok bool) { } return cs[:s], cs[s+1:], true } + +func BasicAuth(username, passwd string) string { + auth := username + ":" + passwd + return "Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) +} diff --git a/pkg/util/util/types.go b/pkg/util/util/types.go new file mode 100644 index 00000000000..784b8f193ad --- /dev/null +++ b/pkg/util/util/types.go @@ -0,0 +1,23 @@ +// Copyright 2023 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 util + +func EmptyOr[T comparable](v T, fallback T) T { + var zero T + if zero == v { + return fallback + } + return v +} diff --git a/pkg/util/version/version.go b/pkg/util/version/version.go index 9a9135940c6..32b31837b6f 100644 --- a/pkg/util/version/version.go +++ b/pkg/util/version/version.go @@ -19,7 +19,7 @@ import ( "strings" ) -var version = "0.51.3" +var version = "0.52.0" func Full() string { return version @@ -45,38 +45,3 @@ func Major(v string) int64 { func Minor(v string) int64 { return getSubVersion(v, 2) } - -// add every case there if server will not accept client's protocol and return false -func Compat(client string) (ok bool, msg string) { - if LessThan(client, "0.18.0") { - return false, "Please upgrade your frpc version to at least 0.18.0" - } - return true, "" -} - -func LessThan(client string, server string) bool { - vc := Proto(client) - vs := Proto(server) - if vc > vs { - return false - } else if vc < vs { - return true - } - - vc = Major(client) - vs = Major(server) - if vc > vs { - return false - } else if vc < vs { - return true - } - - vc = Minor(client) - vs = Minor(server) - if vc > vs { - return false - } else if vc < vs { - return true - } - return false -} diff --git a/pkg/util/version/version_test.go b/pkg/util/version/version_test.go index a77bf421832..73b96a85f79 100644 --- a/pkg/util/version/version_test.go +++ b/pkg/util/version/version_test.go @@ -51,15 +51,3 @@ func TestVersion(t *testing.T) { version := Full() assert.Equal(parseVerion, version) } - -func TestCompact(t *testing.T) { - assert := assert.New(t) - ok, _ := Compat("0.9.0") - assert.False(ok) - - ok, _ = Compat("10.0.0") - assert.True(ok) - - ok, _ = Compat("0.10.0") - assert.False(ok) -} diff --git a/pkg/util/vhost/http.go b/pkg/util/vhost/http.go index af3a4ab5146..7b914ce9b74 100644 --- a/pkg/util/vhost/http.go +++ b/pkg/util/vhost/http.go @@ -251,7 +251,7 @@ func (rp *HTTPReverseProxy) connectHandler(rw http.ResponseWriter, req *http.Req remote, err := rp.CreateConnection(req.Context().Value(RouteInfoKey).(*RequestRouteInfo), false) if err != nil { - _ = notFoundResponse().Write(client) + _ = NotFoundResponse().Write(client) client.Close() return } diff --git a/pkg/util/vhost/https.go b/pkg/util/vhost/https.go index e15c1901b43..bcfdb81e387 100644 --- a/pkg/util/vhost/https.go +++ b/pkg/util/vhost/https.go @@ -29,6 +29,7 @@ type HTTPSMuxer struct { func NewHTTPSMuxer(listener net.Listener, timeout time.Duration) (*HTTPSMuxer, error) { mux, err := NewMuxer(listener, GetHTTPSHostname, timeout) + mux.SetFailHookFunc(vhostFailed) if err != nil { return nil, err } @@ -69,6 +70,12 @@ func readClientHello(reader io.Reader) (*tls.ClientHelloInfo, error) { return hello, nil } +func vhostFailed(c net.Conn) { + // Alert with alertUnrecognizedName + _ = tls.Server(c, &tls.Config{}).Handshake() + c.Close() +} + type readOnlyConn struct { reader io.Reader } diff --git a/pkg/util/vhost/resource.go b/pkg/util/vhost/resource.go index e09edf21ef7..d78082b24d0 100644 --- a/pkg/util/vhost/resource.go +++ b/pkg/util/vhost/resource.go @@ -67,7 +67,7 @@ func getNotFoundPageContent() []byte { return buf } -func notFoundResponse() *http.Response { +func NotFoundResponse() *http.Response { header := make(http.Header) header.Set("server", "frp/"+version.Full()) header.Set("Content-Type", "text/html") diff --git a/pkg/util/vhost/vhost.go b/pkg/util/vhost/vhost.go index 6051a217f5c..29123b695d9 100644 --- a/pkg/util/vhost/vhost.go +++ b/pkg/util/vhost/vhost.go @@ -46,6 +46,7 @@ type ( authFunc func(conn net.Conn, username, password string, reqInfoMap map[string]string) (bool, error) hostRewriteFunc func(net.Conn, string) (net.Conn, error) successHookFunc func(net.Conn, map[string]string) error + failHookFunc func(net.Conn) ) // Muxer is a functional component used for https and tcpmux proxies. @@ -58,6 +59,7 @@ type Muxer struct { vhostFunc muxFunc checkAuth authFunc successHook successHookFunc + failHook failHookFunc rewriteHost hostRewriteFunc registryRouter *Routers } @@ -87,6 +89,11 @@ func (v *Muxer) SetSuccessHookFunc(f successHookFunc) *Muxer { return v } +func (v *Muxer) SetFailHookFunc(f failHookFunc) *Muxer { + v.failHook = f + return v +} + func (v *Muxer) SetRewriteHostFunc(f hostRewriteFunc) *Muxer { v.rewriteHost = f return v @@ -206,13 +213,8 @@ func (v *Muxer) handle(c net.Conn) { httpUser := reqInfoMap["HTTPUser"] l, ok := v.getListener(name, path, httpUser) if !ok { - res := notFoundResponse() - if res.Body != nil { - defer res.Body.Close() - } - _ = res.Write(c) log.Debug("http request for host [%s] path [%s] httpUser [%s] not found", name, path, httpUser) - _ = c.Close() + v.failHook(sConn) return } diff --git a/server/control.go b/server/control.go index abf17ab3b6b..f2eaaa56ad7 100644 --- a/server/control.go +++ b/server/control.go @@ -26,9 +26,11 @@ import ( "github.com/fatedier/golib/control/shutdown" "github.com/fatedier/golib/crypto" "github.com/fatedier/golib/errors" + "github.com/samber/lo" "github.com/fatedier/frp/pkg/auth" "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" pkgerr "github.com/fatedier/frp/pkg/errors" "github.com/fatedier/frp/pkg/msg" plugin "github.com/fatedier/frp/pkg/plugin/server" @@ -151,7 +153,7 @@ type Control struct { mu sync.RWMutex // Server configuration information - serverCfg config.ServerCommonConf + serverCfg *v1.ServerConfig xl *xlog.Logger ctx context.Context @@ -165,11 +167,11 @@ func NewControl( authVerifier auth.Verifier, ctlConn net.Conn, loginMsg *msg.Login, - serverCfg config.ServerCommonConf, + serverCfg *v1.ServerConfig, ) *Control { poolCount := loginMsg.PoolCount - if poolCount > int(serverCfg.MaxPoolCount) { - poolCount = int(serverCfg.MaxPoolCount) + if poolCount > int(serverCfg.Transport.MaxPoolCount) { + poolCount = int(serverCfg.Transport.MaxPoolCount) } ctl := &Control{ rc: rc, @@ -320,7 +322,7 @@ func (ctl *Control) writer() { defer ctl.allShutdown.Start() defer ctl.writerShutdown.Done() - encWriter, err := crypto.NewWriter(ctl.conn, []byte(ctl.serverCfg.Token)) + encWriter, err := crypto.NewWriter(ctl.conn, []byte(ctl.serverCfg.Auth.Token)) if err != nil { xl.Error("crypto new writer error: %v", err) ctl.allShutdown.Start() @@ -352,7 +354,7 @@ func (ctl *Control) reader() { defer ctl.allShutdown.Start() defer ctl.readerShutdown.Done() - encReader := crypto.NewReader(ctl.conn, []byte(ctl.serverCfg.Token)) + encReader := crypto.NewReader(ctl.conn, []byte(ctl.serverCfg.Auth.Token)) for { m, err := msg.ReadMsg(encReader) if err != nil { @@ -400,7 +402,7 @@ func (ctl *Control) stoper() { for _, pxy := range ctl.proxies { pxy.Close() ctl.pxyManager.Del(pxy.GetName()) - metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConf().GetBaseConfig().ProxyType) + metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type) notifyContent := &plugin.CloseProxyContent{ User: plugin.UserInfo{ @@ -450,7 +452,7 @@ func (ctl *Control) manager() { var heartbeatCh <-chan time.Time // Don't need application heartbeat if TCPMux is enabled, // yamux will do same thing. - if !ctl.serverCfg.TCPMux && ctl.serverCfg.HeartbeatTimeout > 0 { + if !lo.FromPtr(ctl.serverCfg.Transport.TCPMux) && ctl.serverCfg.Transport.HeartbeatTimeout > 0 { heartbeat := time.NewTicker(time.Second) defer heartbeat.Stop() heartbeatCh = heartbeat.C @@ -459,7 +461,7 @@ func (ctl *Control) manager() { for { select { case <-heartbeatCh: - if time.Since(ctl.lastPing) > time.Duration(ctl.serverCfg.HeartbeatTimeout)*time.Second { + if time.Since(ctl.lastPing) > time.Duration(ctl.serverCfg.Transport.HeartbeatTimeout)*time.Second { xl.Warn("heartbeat timeout") return } @@ -491,7 +493,8 @@ func (ctl *Control) manager() { } if err != nil { xl.Warn("new proxy [%s] type [%s] error: %v", m.ProxyName, m.ProxyType, err) - resp.Error = util.GenerateResponseErrorString(fmt.Sprintf("new proxy [%s] error", m.ProxyName), err, ctl.serverCfg.DetailedErrorsToClient) + resp.Error = util.GenerateResponseErrorString(fmt.Sprintf("new proxy [%s] error", m.ProxyName), + err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient)) } else { resp.RemoteAddr = remoteAddr xl.Info("new proxy [%s] type [%s] success", m.ProxyName, m.ProxyType) @@ -524,7 +527,7 @@ func (ctl *Control) manager() { if err != nil { xl.Warn("received invalid ping: %v", err) ctl.sendCh <- &msg.Pong{ - Error: util.GenerateResponseErrorString("invalid ping", err, ctl.serverCfg.DetailedErrorsToClient), + Error: util.GenerateResponseErrorString("invalid ping", err, lo.FromPtr(ctl.serverCfg.DetailedErrorsToClient)), } return } @@ -549,9 +552,9 @@ func (ctl *Control) HandleNatHoleReport(m *msg.NatHoleReport) { } func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err error) { - var pxyConf config.ProxyConf + var pxyConf v1.ProxyConfigurer // Load configures from NewProxy message and validate. - pxyConf, err = config.NewProxyConfFromMsg(pxyMsg, ctl.serverCfg) + pxyConf, err = config.NewProxyConfigurerFromMsg(pxyMsg, ctl.serverCfg) if err != nil { return } @@ -565,7 +568,15 @@ func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err // NewProxy will return an interface Proxy. // In fact, it creates different proxies based on the proxy type. We just call run() here. - pxy, err := proxy.NewProxy(ctl.ctx, userInfo, ctl.rc, ctl.poolCount, ctl.GetWorkConn, pxyConf, ctl.serverCfg, ctl.loginMsg) + pxy, err := proxy.NewProxy(ctl.ctx, &proxy.Options{ + UserInfo: userInfo, + LoginMsg: ctl.loginMsg, + PoolCount: ctl.poolCount, + ResourceController: ctl.rc, + GetWorkConnFn: ctl.GetWorkConn, + Configurer: pxyConf, + ServerCfg: ctl.serverCfg, + }) if err != nil { return remoteAddr, err } @@ -632,7 +643,7 @@ func (ctl *Control) CloseProxy(closeMsg *msg.CloseProxy) (err error) { delete(ctl.proxies, closeMsg.ProxyName) ctl.mu.Unlock() - metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConf().GetBaseConfig().ProxyType) + metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type) notifyContent := &plugin.CloseProxyContent{ User: plugin.UserInfo{ diff --git a/server/dashboard.go b/server/dashboard.go index a5c46ba3133..1f290cf9a5a 100644 --- a/server/dashboard.go +++ b/server/dashboard.go @@ -39,7 +39,7 @@ func (svr *Service) RunDashboardServer(address string) (err error) { router.HandleFunc("/healthz", svr.Healthz) // debug - if svr.cfg.PprofEnable { + if svr.cfg.WebServer.PprofEnable { router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) router.HandleFunc("/debug/pprof/profile", pprof.Profile) router.HandleFunc("/debug/pprof/symbol", pprof.Symbol) @@ -49,7 +49,7 @@ func (svr *Service) RunDashboardServer(address string) (err error) { subRouter := router.NewRoute().Subrouter() - user, passwd := svr.cfg.DashboardUser, svr.cfg.DashboardPwd + user, passwd := svr.cfg.WebServer.User, svr.cfg.WebServer.Password subRouter.Use(utilnet.NewHTTPAuthMiddleware(user, passwd).SetAuthFailDelay(200 * time.Millisecond).Middleware) // metrics @@ -82,8 +82,8 @@ func (svr *Service) RunDashboardServer(address string) (err error) { return err } - if svr.cfg.DashboardTLSMode { - cert, err := tls.LoadX509KeyPair(svr.cfg.DashboardTLSCertFile, svr.cfg.DashboardTLSKeyFile) + if svr.cfg.WebServer.TLS != nil { + cert, err := tls.LoadX509KeyPair(svr.cfg.WebServer.TLS.CertFile, svr.cfg.WebServer.TLS.KeyFile) if err != nil { return err } diff --git a/server/dashboard_api.go b/server/dashboard_api.go index 198a111aa61..56faac30789 100644 --- a/server/dashboard_api.go +++ b/server/dashboard_api.go @@ -20,8 +20,8 @@ import ( "github.com/gorilla/mux" - "github.com/fatedier/frp/pkg/config" - "github.com/fatedier/frp/pkg/consts" + "github.com/fatedier/frp/pkg/config/types" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/metrics/mem" "github.com/fatedier/frp/pkg/util/log" "github.com/fatedier/frp/pkg/util/version" @@ -81,11 +81,11 @@ func (svr *Service) APIServerInfo(w http.ResponseWriter, r *http.Request) { KCPBindPort: svr.cfg.KCPBindPort, QUICBindPort: svr.cfg.QUICBindPort, SubdomainHost: svr.cfg.SubDomainHost, - MaxPoolCount: svr.cfg.MaxPoolCount, + MaxPoolCount: svr.cfg.Transport.MaxPoolCount, MaxPortsPerClient: svr.cfg.MaxPortsPerClient, - HeartBeatTimeout: svr.cfg.HeartbeatTimeout, - AllowPortsStr: svr.cfg.AllowPortsStr, - TLSOnly: svr.cfg.TLSOnly, + HeartBeatTimeout: svr.cfg.Transport.HeartbeatTimeout, + AllowPortsStr: types.PortsRangeSlice(svr.cfg.AllowPorts).String(), + TLSOnly: svr.cfg.Transport.TLS.Force, TotalTrafficIn: serverStats.TotalTrafficIn, TotalTrafficOut: serverStats.TotalTrafficOut, @@ -99,7 +99,7 @@ func (svr *Service) APIServerInfo(w http.ResponseWriter, r *http.Request) { } type BaseOutConf struct { - config.BaseProxyConf + v1.ProxyBaseConfig } type TCPOutConf struct { @@ -109,7 +109,7 @@ type TCPOutConf struct { type TCPMuxOutConf struct { BaseOutConf - config.DomainConf + v1.DomainConfig Multiplexer string `json:"multiplexer"` } @@ -120,14 +120,14 @@ type UDPOutConf struct { type HTTPOutConf struct { BaseOutConf - config.DomainConf + v1.DomainConfig Locations []string `json:"locations"` HostHeaderRewrite string `json:"host_header_rewrite"` } type HTTPSOutConf struct { BaseOutConf - config.DomainConf + v1.DomainConfig } type STCPOutConf struct { @@ -138,21 +138,21 @@ type XTCPOutConf struct { BaseOutConf } -func getConfByType(proxyType string) interface{} { - switch proxyType { - case consts.TCPProxy: +func getConfByType(proxyType string) any { + switch v1.ProxyType(proxyType) { + case v1.ProxyTypeTCP: return &TCPOutConf{} - case consts.TCPMuxProxy: + case v1.ProxyTypeTCPMUX: return &TCPMuxOutConf{} - case consts.UDPProxy: + case v1.ProxyTypeUDP: return &UDPOutConf{} - case consts.HTTPProxy: + case v1.ProxyTypeHTTP: return &HTTPOutConf{} - case consts.HTTPSProxy: + case v1.ProxyTypeHTTPS: return &HTTPSOutConf{} - case consts.STCPProxy: + case v1.ProxyTypeSTCP: return &STCPOutConf{} - case consts.XTCPProxy: + case v1.ProxyTypeXTCP: return &XTCPOutConf{} default: return nil @@ -204,7 +204,7 @@ func (svr *Service) getProxyStatsByType(proxyType string) (proxyInfos []*ProxySt for _, ps := range proxyStats { proxyInfo := &ProxyStatsInfo{} if pxy, ok := svr.pxyManager.GetByName(ps.Name); ok { - content, err := json.Marshal(pxy.GetConf()) + content, err := json.Marshal(pxy.GetConfigurer()) if err != nil { log.Warn("marshal proxy [%s] conf info error: %v", ps.Name, err) continue @@ -214,12 +214,12 @@ func (svr *Service) getProxyStatsByType(proxyType string) (proxyInfos []*ProxySt log.Warn("unmarshal proxy [%s] conf info error: %v", ps.Name, err) continue } - proxyInfo.Status = consts.Online + proxyInfo.Status = "online" if pxy.GetLoginMsg() != nil { proxyInfo.ClientVersion = pxy.GetLoginMsg().Version } } else { - proxyInfo.Status = consts.Offline + proxyInfo.Status = "offline" } proxyInfo.Name = ps.Name proxyInfo.TodayTrafficIn = ps.TodayTrafficIn @@ -278,7 +278,7 @@ func (svr *Service) getProxyStatsByTypeAndName(proxyType string, proxyName strin msg = "no proxy info found" } else { if pxy, ok := svr.pxyManager.GetByName(proxyName); ok { - content, err := json.Marshal(pxy.GetConf()) + content, err := json.Marshal(pxy.GetConfigurer()) if err != nil { log.Warn("marshal proxy [%s] conf info error: %v", ps.Name, err) code = 400 @@ -292,9 +292,9 @@ func (svr *Service) getProxyStatsByTypeAndName(proxyType string, proxyName strin msg = "parse conf error" return } - proxyInfo.Status = consts.Online + proxyInfo.Status = "online" } else { - proxyInfo.Status = consts.Offline + proxyInfo.Status = "offline" } proxyInfo.TodayTrafficIn = ps.TodayTrafficIn proxyInfo.TodayTrafficOut = ps.TodayTrafficOut diff --git a/server/group/tcpmux.go b/server/group/tcpmux.go index 0d9790eb88f..1712bc747ce 100644 --- a/server/group/tcpmux.go +++ b/server/group/tcpmux.go @@ -22,7 +22,7 @@ import ( gerr "github.com/fatedier/golib/errors" - "github.com/fatedier/frp/pkg/consts" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/util/tcpmux" "github.com/fatedier/frp/pkg/util/vhost" ) @@ -59,8 +59,8 @@ func (tmgc *TCPMuxGroupCtl) Listen( } tmgc.mu.Unlock() - switch multiplexer { - case consts.HTTPConnectTCPMultiplexer: + switch v1.TCPMultiplexerType(multiplexer) { + case v1.TCPMultiplexerHTTPConnect: return tcpMuxGroup.HTTPConnectListen(ctx, group, groupKey, routeConfig) default: err = fmt.Errorf("unknown multiplexer [%s]", multiplexer) diff --git a/server/ports/ports.go b/server/ports/ports.go index f852f843596..5a73fbc5b3e 100644 --- a/server/ports/ports.go +++ b/server/ports/ports.go @@ -6,6 +6,8 @@ import ( "strconv" "sync" "time" + + "github.com/fatedier/frp/pkg/config/types" ) const ( @@ -39,7 +41,7 @@ type Manager struct { mu sync.Mutex } -func NewManager(netType string, bindAddr string, allowPorts map[int]struct{}) *Manager { +func NewManager(netType string, bindAddr string, allowPorts []types.PortsRange) *Manager { pm := &Manager{ reservedPorts: make(map[string]*PortCtx), usedPorts: make(map[int]*PortCtx), @@ -48,8 +50,14 @@ func NewManager(netType string, bindAddr string, allowPorts map[int]struct{}) *M netType: netType, } if len(allowPorts) > 0 { - for port := range allowPorts { - pm.freePorts[port] = struct{}{} + for _, pair := range allowPorts { + if pair.Single > 0 { + pm.freePorts[pair.Single] = struct{}{} + } else { + for i := pair.Start; i <= pair.End; i++ { + pm.freePorts[i] = struct{}{} + } + } } } else { for i := MinPort; i <= MaxPort; i++ { diff --git a/server/proxy/http.go b/server/proxy/http.go index 432dbc101b3..cafaf8f3d9a 100644 --- a/server/proxy/http.go +++ b/server/proxy/http.go @@ -22,7 +22,7 @@ import ( libio "github.com/fatedier/golib/io" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/util/limit" utilnet "github.com/fatedier/frp/pkg/util/net" "github.com/fatedier/frp/pkg/util/util" @@ -31,18 +31,18 @@ import ( ) func init() { - RegisterProxyFactory(reflect.TypeOf(&config.HTTPProxyConf{}), NewHTTPProxy) + RegisterProxyFactory(reflect.TypeOf(&v1.HTTPProxyConfig{}), NewHTTPProxy) } type HTTPProxy struct { *BaseProxy - cfg *config.HTTPProxyConf + cfg *v1.HTTPProxyConfig closeFuncs []func() } -func NewHTTPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy { - unwrapped, ok := cfg.(*config.HTTPProxyConf) +func NewHTTPProxy(baseProxy *BaseProxy) Proxy { + unwrapped, ok := baseProxy.GetConfigurer().(*v1.HTTPProxyConfig) if !ok { return nil } @@ -57,9 +57,9 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) { routeConfig := vhost.RouteConfig{ RewriteHost: pxy.cfg.HostHeaderRewrite, RouteByHTTPUser: pxy.cfg.RouteByHTTPUser, - Headers: pxy.cfg.Headers, + Headers: pxy.cfg.RequestHeaders.Set, Username: pxy.cfg.HTTPUser, - Password: pxy.cfg.HTTPPwd, + Password: pxy.cfg.HTTPPassword, CreateConnFn: pxy.GetRealConn, } @@ -87,14 +87,14 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) { tmpRouteConfig := routeConfig // handle group - if pxy.cfg.Group != "" { - err = pxy.rc.HTTPGroupCtl.Register(pxy.name, pxy.cfg.Group, pxy.cfg.GroupKey, routeConfig) + if pxy.cfg.LoadBalancer.Group != "" { + err = pxy.rc.HTTPGroupCtl.Register(pxy.name, pxy.cfg.LoadBalancer.Group, pxy.cfg.LoadBalancer.GroupKey, routeConfig) if err != nil { return } pxy.closeFuncs = append(pxy.closeFuncs, func() { - pxy.rc.HTTPGroupCtl.UnRegister(pxy.name, pxy.cfg.Group, tmpRouteConfig) + pxy.rc.HTTPGroupCtl.UnRegister(pxy.name, pxy.cfg.LoadBalancer.Group, tmpRouteConfig) }) } else { // no group @@ -108,7 +108,7 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) { } addrs = append(addrs, util.CanonicalAddr(routeConfig.Domain, pxy.serverCfg.VhostHTTPPort)) xl.Info("http proxy listen for host [%s] location [%s] group [%s], routeByHTTPUser [%s]", - routeConfig.Domain, routeConfig.Location, pxy.cfg.Group, pxy.cfg.RouteByHTTPUser) + routeConfig.Domain, routeConfig.Location, pxy.cfg.LoadBalancer.Group, pxy.cfg.RouteByHTTPUser) } } @@ -120,14 +120,14 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) { tmpRouteConfig := routeConfig // handle group - if pxy.cfg.Group != "" { - err = pxy.rc.HTTPGroupCtl.Register(pxy.name, pxy.cfg.Group, pxy.cfg.GroupKey, routeConfig) + if pxy.cfg.LoadBalancer.Group != "" { + err = pxy.rc.HTTPGroupCtl.Register(pxy.name, pxy.cfg.LoadBalancer.Group, pxy.cfg.LoadBalancer.GroupKey, routeConfig) if err != nil { return } pxy.closeFuncs = append(pxy.closeFuncs, func() { - pxy.rc.HTTPGroupCtl.UnRegister(pxy.name, pxy.cfg.Group, tmpRouteConfig) + pxy.rc.HTTPGroupCtl.UnRegister(pxy.name, pxy.cfg.LoadBalancer.Group, tmpRouteConfig) }) } else { err = pxy.rc.HTTPReverseProxy.Register(routeConfig) @@ -141,17 +141,13 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) { addrs = append(addrs, util.CanonicalAddr(tmpRouteConfig.Domain, pxy.serverCfg.VhostHTTPPort)) xl.Info("http proxy listen for host [%s] location [%s] group [%s], routeByHTTPUser [%s]", - routeConfig.Domain, routeConfig.Location, pxy.cfg.Group, pxy.cfg.RouteByHTTPUser) + routeConfig.Domain, routeConfig.Location, pxy.cfg.LoadBalancer.Group, pxy.cfg.RouteByHTTPUser) } } remoteAddr = strings.Join(addrs, ",") return } -func (pxy *HTTPProxy) GetConf() config.ProxyConf { - return pxy.cfg -} - func (pxy *HTTPProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err error) { xl := pxy.xl rAddr, errRet := net.ResolveTCPAddr("tcp", remoteAddr) @@ -167,14 +163,14 @@ func (pxy *HTTPProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err err } var rwc io.ReadWriteCloser = tmpConn - if pxy.cfg.UseEncryption { - rwc, err = libio.WithEncryption(rwc, []byte(pxy.serverCfg.Token)) + if pxy.cfg.Transport.UseEncryption { + rwc, err = libio.WithEncryption(rwc, []byte(pxy.serverCfg.Auth.Token)) if err != nil { xl.Error("create encryption stream error: %v", err) return } } - if pxy.cfg.UseCompression { + if pxy.cfg.Transport.UseCompression { rwc = libio.WithCompression(rwc) } @@ -186,13 +182,13 @@ func (pxy *HTTPProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err err workConn = utilnet.WrapReadWriteCloserToConn(rwc, tmpConn) workConn = utilnet.WrapStatsConn(workConn, pxy.updateStatsAfterClosedConn) - metrics.Server.OpenConnection(pxy.GetName(), pxy.GetConf().GetBaseConfig().ProxyType) + metrics.Server.OpenConnection(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type) return } func (pxy *HTTPProxy) updateStatsAfterClosedConn(totalRead, totalWrite int64) { name := pxy.GetName() - proxyType := pxy.GetConf().GetBaseConfig().ProxyType + proxyType := pxy.GetConfigurer().GetBaseConfig().Type metrics.Server.CloseConnection(name, proxyType) metrics.Server.AddTrafficIn(name, proxyType, totalWrite) metrics.Server.AddTrafficOut(name, proxyType, totalRead) diff --git a/server/proxy/https.go b/server/proxy/https.go index 74f0e7ca26e..dc9b77ebe00 100644 --- a/server/proxy/https.go +++ b/server/proxy/https.go @@ -18,22 +18,22 @@ import ( "reflect" "strings" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/vhost" ) func init() { - RegisterProxyFactory(reflect.TypeOf(&config.HTTPSProxyConf{}), NewHTTPSProxy) + RegisterProxyFactory(reflect.TypeOf(&v1.HTTPSProxyConfig{}), NewHTTPSProxy) } type HTTPSProxy struct { *BaseProxy - cfg *config.HTTPSProxyConf + cfg *v1.HTTPSProxyConfig } -func NewHTTPSProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy { - unwrapped, ok := cfg.(*config.HTTPSProxyConf) +func NewHTTPSProxy(baseProxy *BaseProxy) Proxy { + unwrapped, ok := baseProxy.GetConfigurer().(*v1.HTTPSProxyConfig) if !ok { return nil } @@ -86,10 +86,6 @@ func (pxy *HTTPSProxy) Run() (remoteAddr string, err error) { return } -func (pxy *HTTPSProxy) GetConf() config.ProxyConf { - return pxy.cfg -} - func (pxy *HTTPSProxy) Close() { pxy.BaseProxy.Close() } diff --git a/server/proxy/proxy.go b/server/proxy/proxy.go index d1e6625523a..fe6f781b728 100644 --- a/server/proxy/proxy.go +++ b/server/proxy/proxy.go @@ -27,7 +27,8 @@ import ( libio "github.com/fatedier/golib/io" "golang.org/x/time/rate" - "github.com/fatedier/frp/pkg/config" + "github.com/fatedier/frp/pkg/config/types" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" plugin "github.com/fatedier/frp/pkg/plugin/server" "github.com/fatedier/frp/pkg/util/limit" @@ -37,9 +38,9 @@ import ( "github.com/fatedier/frp/server/metrics" ) -var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy, config.ProxyConf) Proxy{} +var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy) Proxy{} -func RegisterProxyFactory(proxyConfType reflect.Type, factory func(*BaseProxy, config.ProxyConf) Proxy) { +func RegisterProxyFactory(proxyConfType reflect.Type, factory func(*BaseProxy) Proxy) { proxyFactoryRegistry[proxyConfType] = factory } @@ -49,7 +50,7 @@ type Proxy interface { Context() context.Context Run() (remoteAddr string, err error) GetName() string - GetConf() config.ProxyConf + GetConfigurer() v1.ProxyConfigurer GetWorkConnFromPool(src, dst net.Addr) (workConn net.Conn, err error) GetUsedPortsNum() int GetResourceController() *controller.ResourceController @@ -66,11 +67,11 @@ type BaseProxy struct { usedPortsNum int poolCount int getWorkConnFn GetWorkConnFn - serverCfg config.ServerCommonConf + serverCfg *v1.ServerConfig limiter *rate.Limiter userInfo plugin.UserInfo loginMsg *msg.Login - pxyConf config.ProxyConf + configurer v1.ProxyConfigurer mu sync.RWMutex xl *xlog.Logger @@ -105,6 +106,10 @@ func (pxy *BaseProxy) GetLimiter() *rate.Limiter { return pxy.limiter } +func (pxy *BaseProxy) GetConfigurer() v1.ProxyConfigurer { + return pxy.configurer +} + func (pxy *BaseProxy) Close() { xl := xlog.FromContextSafe(pxy.ctx) xl.Info("proxy closing") @@ -209,13 +214,13 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) { defer userConn.Close() serverCfg := pxy.serverCfg - cfg := pxy.pxyConf.GetBaseConfig() + cfg := pxy.configurer.GetBaseConfig() // server plugin hook rc := pxy.GetResourceController() content := &plugin.NewUserConnContent{ User: pxy.GetUserInfo(), ProxyName: pxy.GetName(), - ProxyType: cfg.ProxyType, + ProxyType: cfg.Type, RemoteAddr: userConn.RemoteAddr().String(), } _, err := rc.PluginManager.NewUserConn(content) @@ -232,15 +237,16 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) { defer workConn.Close() var local io.ReadWriteCloser = workConn - xl.Trace("handler user tcp connection, use_encryption: %t, use_compression: %t", cfg.UseEncryption, cfg.UseCompression) - if cfg.UseEncryption { - local, err = libio.WithEncryption(local, []byte(serverCfg.Token)) + xl.Trace("handler user tcp connection, use_encryption: %t, use_compression: %t", + cfg.Transport.UseEncryption, cfg.Transport.UseCompression) + if cfg.Transport.UseEncryption { + local, err = libio.WithEncryption(local, []byte(serverCfg.Auth.Token)) if err != nil { xl.Error("create encryption stream error: %v", err) return } } - if cfg.UseCompression { + if cfg.Transport.UseCompression { var recycleFn func() local, recycleFn = libio.WithCompressionFromPool(local) defer recycleFn() @@ -256,7 +262,7 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) { workConn.RemoteAddr().String(), userConn.LocalAddr().String(), userConn.RemoteAddr().String()) name := pxy.GetName() - proxyType := cfg.ProxyType + proxyType := cfg.Type metrics.Server.OpenConnection(name, proxyType) inCount, outCount, _ := libio.Join(local, userConn) metrics.Server.CloseConnection(name, proxyType) @@ -265,37 +271,46 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) { xl.Debug("join connections closed") } -func NewProxy(ctx context.Context, userInfo plugin.UserInfo, rc *controller.ResourceController, poolCount int, - getWorkConnFn GetWorkConnFn, pxyConf config.ProxyConf, serverCfg config.ServerCommonConf, loginMsg *msg.Login, -) (pxy Proxy, err error) { - xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(pxyConf.GetBaseConfig().ProxyName) +type Options struct { + UserInfo plugin.UserInfo + LoginMsg *msg.Login + PoolCount int + ResourceController *controller.ResourceController + GetWorkConnFn GetWorkConnFn + Configurer v1.ProxyConfigurer + ServerCfg *v1.ServerConfig +} + +func NewProxy(ctx context.Context, options *Options) (pxy Proxy, err error) { + configurer := options.Configurer + xl := xlog.FromContextSafe(ctx).Spawn().AppendPrefix(configurer.GetBaseConfig().Name) var limiter *rate.Limiter - limitBytes := pxyConf.GetBaseConfig().BandwidthLimit.Bytes() - if limitBytes > 0 && pxyConf.GetBaseConfig().BandwidthLimitMode == config.BandwidthLimitModeServer { + limitBytes := configurer.GetBaseConfig().Transport.BandwidthLimit.Bytes() + if limitBytes > 0 && configurer.GetBaseConfig().Transport.BandwidthLimitMode == types.BandwidthLimitModeServer { limiter = rate.NewLimiter(rate.Limit(float64(limitBytes)), int(limitBytes)) } basePxy := BaseProxy{ - name: pxyConf.GetBaseConfig().ProxyName, - rc: rc, + name: configurer.GetBaseConfig().Name, + rc: options.ResourceController, listeners: make([]net.Listener, 0), - poolCount: poolCount, - getWorkConnFn: getWorkConnFn, - serverCfg: serverCfg, + poolCount: options.PoolCount, + getWorkConnFn: options.GetWorkConnFn, + serverCfg: options.ServerCfg, limiter: limiter, xl: xl, ctx: xlog.NewContext(ctx, xl), - userInfo: userInfo, - loginMsg: loginMsg, - pxyConf: pxyConf, + userInfo: options.UserInfo, + loginMsg: options.LoginMsg, + configurer: configurer, } - factory := proxyFactoryRegistry[reflect.TypeOf(pxyConf)] + factory := proxyFactoryRegistry[reflect.TypeOf(configurer)] if factory == nil { return pxy, fmt.Errorf("proxy type not support") } - pxy = factory(&basePxy, pxyConf) + pxy = factory(&basePxy) if pxy == nil { return nil, fmt.Errorf("proxy not created") } diff --git a/server/proxy/stcp.go b/server/proxy/stcp.go index 7c1511ab288..4f08a7c12c3 100644 --- a/server/proxy/stcp.go +++ b/server/proxy/stcp.go @@ -17,20 +17,20 @@ package proxy import ( "reflect" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" ) func init() { - RegisterProxyFactory(reflect.TypeOf(&config.STCPProxyConf{}), NewSTCPProxy) + RegisterProxyFactory(reflect.TypeOf(&v1.STCPProxyConfig{}), NewSTCPProxy) } type STCPProxy struct { *BaseProxy - cfg *config.STCPProxyConf + cfg *v1.STCPProxyConfig } -func NewSTCPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy { - unwrapped, ok := cfg.(*config.STCPProxyConf) +func NewSTCPProxy(baseProxy *BaseProxy) Proxy { + unwrapped, ok := baseProxy.GetConfigurer().(*v1.STCPProxyConfig) if !ok { return nil } @@ -47,7 +47,7 @@ func (pxy *STCPProxy) Run() (remoteAddr string, err error) { if len(allowUsers) == 0 { allowUsers = []string{pxy.GetUserInfo().User} } - listener, errRet := pxy.rc.VisitorManager.Listen(pxy.GetName(), pxy.cfg.Sk, allowUsers) + listener, errRet := pxy.rc.VisitorManager.Listen(pxy.GetName(), pxy.cfg.Secretkey, allowUsers) if errRet != nil { err = errRet return @@ -59,10 +59,6 @@ func (pxy *STCPProxy) Run() (remoteAddr string, err error) { return } -func (pxy *STCPProxy) GetConf() config.ProxyConf { - return pxy.cfg -} - func (pxy *STCPProxy) Close() { pxy.BaseProxy.Close() pxy.rc.VisitorManager.CloseListener(pxy.GetName()) diff --git a/server/proxy/sudp.go b/server/proxy/sudp.go index 492b09542bd..a6d7c798f1b 100644 --- a/server/proxy/sudp.go +++ b/server/proxy/sudp.go @@ -17,20 +17,20 @@ package proxy import ( "reflect" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" ) func init() { - RegisterProxyFactory(reflect.TypeOf(&config.SUDPProxyConf{}), NewSUDPProxy) + RegisterProxyFactory(reflect.TypeOf(&v1.SUDPProxyConfig{}), NewSUDPProxy) } type SUDPProxy struct { *BaseProxy - cfg *config.SUDPProxyConf + cfg *v1.SUDPProxyConfig } -func NewSUDPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy { - unwrapped, ok := cfg.(*config.SUDPProxyConf) +func NewSUDPProxy(baseProxy *BaseProxy) Proxy { + unwrapped, ok := baseProxy.GetConfigurer().(*v1.SUDPProxyConfig) if !ok { return nil } @@ -47,7 +47,7 @@ func (pxy *SUDPProxy) Run() (remoteAddr string, err error) { if len(allowUsers) == 0 { allowUsers = []string{pxy.GetUserInfo().User} } - listener, errRet := pxy.rc.VisitorManager.Listen(pxy.GetName(), pxy.cfg.Sk, allowUsers) + listener, errRet := pxy.rc.VisitorManager.Listen(pxy.GetName(), pxy.cfg.Secretkey, allowUsers) if errRet != nil { err = errRet return @@ -59,10 +59,6 @@ func (pxy *SUDPProxy) Run() (remoteAddr string, err error) { return } -func (pxy *SUDPProxy) GetConf() config.ProxyConf { - return pxy.cfg -} - func (pxy *SUDPProxy) Close() { pxy.BaseProxy.Close() pxy.rc.VisitorManager.CloseListener(pxy.GetName()) diff --git a/server/proxy/tcp.go b/server/proxy/tcp.go index 5bb5c1abfaf..4196ad4ae3a 100644 --- a/server/proxy/tcp.go +++ b/server/proxy/tcp.go @@ -20,22 +20,22 @@ import ( "reflect" "strconv" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" ) func init() { - RegisterProxyFactory(reflect.TypeOf(&config.TCPProxyConf{}), NewTCPProxy) + RegisterProxyFactory(reflect.TypeOf(&v1.TCPProxyConfig{}), NewTCPProxy) } type TCPProxy struct { *BaseProxy - cfg *config.TCPProxyConf + cfg *v1.TCPProxyConfig realBindPort int } -func NewTCPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy { - unwrapped, ok := cfg.(*config.TCPProxyConf) +func NewTCPProxy(baseProxy *BaseProxy) Proxy { + unwrapped, ok := baseProxy.GetConfigurer().(*v1.TCPProxyConfig) if !ok { return nil } @@ -48,8 +48,9 @@ func NewTCPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy { func (pxy *TCPProxy) Run() (remoteAddr string, err error) { xl := pxy.xl - if pxy.cfg.Group != "" { - l, realBindPort, errRet := pxy.rc.TCPGroupCtl.Listen(pxy.name, pxy.cfg.Group, pxy.cfg.GroupKey, pxy.serverCfg.ProxyBindAddr, pxy.cfg.RemotePort) + if pxy.cfg.LoadBalancer.Group != "" { + l, realBindPort, errRet := pxy.rc.TCPGroupCtl.Listen(pxy.name, pxy.cfg.LoadBalancer.Group, pxy.cfg.LoadBalancer.GroupKey, + pxy.serverCfg.ProxyBindAddr, pxy.cfg.RemotePort) if errRet != nil { err = errRet return @@ -61,7 +62,7 @@ func (pxy *TCPProxy) Run() (remoteAddr string, err error) { }() pxy.realBindPort = realBindPort pxy.listeners = append(pxy.listeners, l) - xl.Info("tcp proxy listen port [%d] in group [%s]", pxy.cfg.RemotePort, pxy.cfg.Group) + xl.Info("tcp proxy listen port [%d] in group [%s]", pxy.cfg.RemotePort, pxy.cfg.LoadBalancer.Group) } else { pxy.realBindPort, err = pxy.rc.TCPPortManager.Acquire(pxy.name, pxy.cfg.RemotePort) if err != nil { @@ -87,13 +88,9 @@ func (pxy *TCPProxy) Run() (remoteAddr string, err error) { return } -func (pxy *TCPProxy) GetConf() config.ProxyConf { - return pxy.cfg -} - func (pxy *TCPProxy) Close() { pxy.BaseProxy.Close() - if pxy.cfg.Group == "" { + if pxy.cfg.LoadBalancer.Group == "" { pxy.rc.TCPPortManager.Release(pxy.realBindPort) } } diff --git a/server/proxy/tcpmux.go b/server/proxy/tcpmux.go index 6d25a84ecb2..5b914c3983d 100644 --- a/server/proxy/tcpmux.go +++ b/server/proxy/tcpmux.go @@ -20,23 +20,22 @@ import ( "reflect" "strings" - "github.com/fatedier/frp/pkg/config" - "github.com/fatedier/frp/pkg/consts" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/pkg/util/vhost" ) func init() { - RegisterProxyFactory(reflect.TypeOf(&config.TCPMuxProxyConf{}), NewTCPMuxProxy) + RegisterProxyFactory(reflect.TypeOf(&v1.TCPMuxProxyConfig{}), NewTCPMuxProxy) } type TCPMuxProxy struct { *BaseProxy - cfg *config.TCPMuxProxyConf + cfg *v1.TCPMuxProxyConfig } -func NewTCPMuxProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy { - unwrapped, ok := cfg.(*config.TCPMuxProxyConf) +func NewTCPMuxProxy(baseProxy *BaseProxy) Proxy { + unwrapped, ok := baseProxy.GetConfigurer().(*v1.TCPMuxProxyConfig) if !ok { return nil } @@ -57,8 +56,9 @@ func (pxy *TCPMuxProxy) httpConnectListen( Username: httpUser, Password: httpPwd, } - if pxy.cfg.Group != "" { - l, err = pxy.rc.TCPMuxGroupCtl.Listen(pxy.ctx, pxy.cfg.Multiplexer, pxy.cfg.Group, pxy.cfg.GroupKey, *routeConfig) + if pxy.cfg.LoadBalancer.Group != "" { + l, err = pxy.rc.TCPMuxGroupCtl.Listen(pxy.ctx, pxy.cfg.Multiplexer, + pxy.cfg.LoadBalancer.Group, pxy.cfg.LoadBalancer.GroupKey, *routeConfig) } else { l, err = pxy.rc.TCPMuxHTTPConnectMuxer.Listen(pxy.ctx, routeConfig) } @@ -66,7 +66,7 @@ func (pxy *TCPMuxProxy) httpConnectListen( return nil, err } pxy.xl.Info("tcpmux httpconnect multiplexer listens for host [%s], group [%s] routeByHTTPUser [%s]", - domain, pxy.cfg.Group, pxy.cfg.RouteByHTTPUser) + domain, pxy.cfg.LoadBalancer.Group, pxy.cfg.RouteByHTTPUser) pxy.listeners = append(pxy.listeners, l) return append(addrs, util.CanonicalAddr(domain, pxy.serverCfg.TCPMuxHTTPConnectPort)), nil } @@ -78,7 +78,7 @@ func (pxy *TCPMuxProxy) httpConnectRun() (remoteAddr string, err error) { continue } - addrs, err = pxy.httpConnectListen(domain, pxy.cfg.RouteByHTTPUser, pxy.cfg.HTTPUser, pxy.cfg.HTTPPwd, addrs) + addrs, err = pxy.httpConnectListen(domain, pxy.cfg.RouteByHTTPUser, pxy.cfg.HTTPUser, pxy.cfg.HTTPPassword, addrs) if err != nil { return "", err } @@ -86,7 +86,7 @@ func (pxy *TCPMuxProxy) httpConnectRun() (remoteAddr string, err error) { if pxy.cfg.SubDomain != "" { addrs, err = pxy.httpConnectListen(pxy.cfg.SubDomain+"."+pxy.serverCfg.SubDomainHost, - pxy.cfg.RouteByHTTPUser, pxy.cfg.HTTPUser, pxy.cfg.HTTPPwd, addrs) + pxy.cfg.RouteByHTTPUser, pxy.cfg.HTTPUser, pxy.cfg.HTTPPassword, addrs) if err != nil { return "", err } @@ -98,8 +98,8 @@ func (pxy *TCPMuxProxy) httpConnectRun() (remoteAddr string, err error) { } func (pxy *TCPMuxProxy) Run() (remoteAddr string, err error) { - switch pxy.cfg.Multiplexer { - case consts.HTTPConnectTCPMultiplexer: + switch v1.TCPMultiplexerType(pxy.cfg.Multiplexer) { + case v1.TCPMultiplexerHTTPConnect: remoteAddr, err = pxy.httpConnectRun() default: err = fmt.Errorf("unknown multiplexer [%s]", pxy.cfg.Multiplexer) @@ -111,10 +111,6 @@ func (pxy *TCPMuxProxy) Run() (remoteAddr string, err error) { return remoteAddr, err } -func (pxy *TCPMuxProxy) GetConf() config.ProxyConf { - return pxy.cfg -} - func (pxy *TCPMuxProxy) Close() { pxy.BaseProxy.Close() } diff --git a/server/proxy/udp.go b/server/proxy/udp.go index 20108bf3933..772c3f0d1b9 100644 --- a/server/proxy/udp.go +++ b/server/proxy/udp.go @@ -26,7 +26,7 @@ import ( "github.com/fatedier/golib/errors" libio "github.com/fatedier/golib/io" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/proto/udp" "github.com/fatedier/frp/pkg/util/limit" @@ -35,12 +35,12 @@ import ( ) func init() { - RegisterProxyFactory(reflect.TypeOf(&config.UDPProxyConf{}), NewUDPProxy) + RegisterProxyFactory(reflect.TypeOf(&v1.UDPProxyConfig{}), NewUDPProxy) } type UDPProxy struct { *BaseProxy - cfg *config.UDPProxyConf + cfg *v1.UDPProxyConfig realBindPort int @@ -63,8 +63,8 @@ type UDPProxy struct { isClosed bool } -func NewUDPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy { - unwrapped, ok := cfg.(*config.UDPProxyConf) +func NewUDPProxy(baseProxy *BaseProxy) Proxy { + unwrapped, ok := baseProxy.GetConfigurer().(*v1.UDPProxyConfig) if !ok { return nil } @@ -140,7 +140,7 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) { pxy.readCh <- m metrics.Server.AddTrafficOut( pxy.GetName(), - pxy.GetConf().GetBaseConfig().ProxyType, + pxy.GetConfigurer().GetBaseConfig().Type, int64(len(m.Content)), ) }); errRet != nil { @@ -170,7 +170,7 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) { xl.Trace("send message to udp workConn: %s", udpMsg.Content) metrics.Server.AddTrafficIn( pxy.GetName(), - pxy.GetConf().GetBaseConfig().ProxyType, + pxy.GetConfigurer().GetBaseConfig().Type, int64(len(udpMsg.Content)), ) continue @@ -204,15 +204,15 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) { } var rwc io.ReadWriteCloser = workConn - if pxy.cfg.UseEncryption { - rwc, err = libio.WithEncryption(rwc, []byte(pxy.serverCfg.Token)) + if pxy.cfg.Transport.UseEncryption { + rwc, err = libio.WithEncryption(rwc, []byte(pxy.serverCfg.Auth.Token)) if err != nil { xl.Error("create encryption stream error: %v", err) workConn.Close() continue } } - if pxy.cfg.UseCompression { + if pxy.cfg.Transport.UseCompression { rwc = libio.WithCompression(rwc) } @@ -245,10 +245,6 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) { return remoteAddr, nil } -func (pxy *UDPProxy) GetConf() config.ProxyConf { - return pxy.cfg -} - func (pxy *UDPProxy) Close() { pxy.mu.Lock() defer pxy.mu.Unlock() diff --git a/server/proxy/xtcp.go b/server/proxy/xtcp.go index acf127d7f26..fe60c630c57 100644 --- a/server/proxy/xtcp.go +++ b/server/proxy/xtcp.go @@ -20,23 +20,23 @@ import ( "github.com/fatedier/golib/errors" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/msg" ) func init() { - RegisterProxyFactory(reflect.TypeOf(&config.XTCPProxyConf{}), NewXTCPProxy) + RegisterProxyFactory(reflect.TypeOf(&v1.XTCPProxyConfig{}), NewXTCPProxy) } type XTCPProxy struct { *BaseProxy - cfg *config.XTCPProxyConf + cfg *v1.XTCPProxyConfig closeCh chan struct{} } -func NewXTCPProxy(baseProxy *BaseProxy, cfg config.ProxyConf) Proxy { - unwrapped, ok := cfg.(*config.XTCPProxyConf) +func NewXTCPProxy(baseProxy *BaseProxy) Proxy { + unwrapped, ok := baseProxy.GetConfigurer().(*v1.XTCPProxyConfig) if !ok { return nil } @@ -58,7 +58,7 @@ func (pxy *XTCPProxy) Run() (remoteAddr string, err error) { if len(allowUsers) == 0 { allowUsers = []string{pxy.GetUserInfo().User} } - sidCh, err := pxy.rc.NatHoleController.ListenClient(pxy.GetName(), pxy.cfg.Sk, allowUsers) + sidCh, err := pxy.rc.NatHoleController.ListenClient(pxy.GetName(), pxy.cfg.Secretkey, allowUsers) if err != nil { return "", err } @@ -86,10 +86,6 @@ func (pxy *XTCPProxy) Run() (remoteAddr string, err error) { return } -func (pxy *XTCPProxy) GetConf() config.ProxyConf { - return pxy.cfg -} - func (pxy *XTCPProxy) Close() { pxy.BaseProxy.Close() pxy.rc.NatHoleController.CloseClient(pxy.GetName()) diff --git a/server/service.go b/server/service.go index 002468814d1..9deffa020f0 100644 --- a/server/service.go +++ b/server/service.go @@ -22,17 +22,17 @@ import ( "io" "net" "net/http" - "sort" "strconv" "time" "github.com/fatedier/golib/net/mux" fmux "github.com/hashicorp/yamux" quic "github.com/quic-go/quic-go" + "github.com/samber/lo" "github.com/fatedier/frp/assets" "github.com/fatedier/frp/pkg/auth" - "github.com/fatedier/frp/pkg/config" + v1 "github.com/fatedier/frp/pkg/config/v1" modelmetrics "github.com/fatedier/frp/pkg/metrics" "github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/nathole" @@ -98,7 +98,7 @@ type Service struct { tlsConfig *tls.Config - cfg config.ServerCommonConf + cfg *v1.ServerConfig // service context ctx context.Context @@ -106,11 +106,11 @@ type Service struct { cancel context.CancelFunc } -func NewService(cfg config.ServerCommonConf) (svr *Service, err error) { +func NewService(cfg *v1.ServerConfig) (svr *Service, err error) { tlsConfig, err := transport.NewServerTLSConfig( - cfg.TLSCertFile, - cfg.TLSKeyFile, - cfg.TLSTrustedCaFile) + cfg.Transport.TLS.CertFile, + cfg.Transport.TLS.KeyFile, + cfg.Transport.TLS.TrustedCaFile) if err != nil { return } @@ -125,7 +125,7 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) { UDPPortManager: ports.NewManager("udp", cfg.ProxyBindAddr, cfg.AllowPorts), }, httpVhostRouter: vhost.NewRouters(), - authVerifier: auth.NewAuthVerifier(cfg.ServerConfig), + authVerifier: auth.NewAuthVerifier(cfg.Auth), tlsConfig: tlsConfig, cfg: cfg, ctx: context.Background(), @@ -150,15 +150,9 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) { } // Init all plugins - pluginNames := make([]string, 0, len(cfg.HTTPPlugins)) - for n := range cfg.HTTPPlugins { - pluginNames = append(pluginNames, n) - } - sort.Strings(pluginNames) - - for _, name := range pluginNames { - svr.pluginManager.Register(plugin.NewHTTPPluginOptions(cfg.HTTPPlugins[name])) - log.Info("plugin [%s] has been registered", name) + for _, p := range cfg.HTTPPlugins { + svr.pluginManager.Register(plugin.NewHTTPPluginOptions(p)) + log.Info("plugin [%s] has been registered", p.Name) } svr.rc.PluginManager = svr.pluginManager @@ -196,7 +190,7 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) { } svr.muxer = mux.NewMux(ln) - svr.muxer.SetKeepAlive(time.Duration(cfg.TCPKeepAlive) * time.Second) + svr.muxer.SetKeepAlive(time.Duration(cfg.Transport.TCPKeepAlive) * time.Second) go func() { _ = svr.muxer.Serve() }() @@ -221,9 +215,9 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) { quicTLSCfg := tlsConfig.Clone() quicTLSCfg.NextProtos = []string{"frp"} svr.quicListener, err = quic.ListenAddr(address, quicTLSCfg, &quic.Config{ - MaxIdleTimeout: time.Duration(cfg.QUICMaxIdleTimeout) * time.Second, - MaxIncomingStreams: int64(cfg.QUICMaxIncomingStreams), - KeepAlivePeriod: time.Duration(cfg.QUICKeepalivePeriod) * time.Second, + MaxIdleTimeout: time.Duration(cfg.Transport.QUIC.MaxIdleTimeout) * time.Second, + MaxIncomingStreams: int64(cfg.Transport.QUIC.MaxIncomingStreams), + KeepAlivePeriod: time.Duration(cfg.Transport.QUIC.KeepalivePeriod) * time.Second, }) if err != nil { err = fmt.Errorf("listen on quic udp address %s error: %v", address, err) @@ -305,11 +299,11 @@ func NewService(cfg config.ServerCommonConf) (svr *Service, err error) { var statsEnable bool // Create dashboard web server. - if cfg.DashboardPort > 0 { + if cfg.WebServer.Port > 0 { // Init dashboard assets - assets.Load(cfg.AssetsDir) + assets.Load(cfg.WebServer.AssetsDir) - address := net.JoinHostPort(cfg.DashboardAddr, strconv.Itoa(cfg.DashboardPort)) + address := net.JoinHostPort(cfg.WebServer.Addr, strconv.Itoa(cfg.WebServer.Port)) err = svr.RunDashboardServer(address) if err != nil { err = fmt.Errorf("create dashboard web server error, %v", err) @@ -416,7 +410,7 @@ func (svr *Service) handleConnection(ctx context.Context, conn net.Conn) { xl.Warn("register control error: %v", err) _ = msg.WriteMsg(conn, &msg.LoginResp{ Version: version.Full(), - Error: util.GenerateResponseErrorString("register control error", err, svr.cfg.DetailedErrorsToClient), + Error: util.GenerateResponseErrorString("register control error", err, lo.FromPtr(svr.cfg.DetailedErrorsToClient)), }) conn.Close() } @@ -429,7 +423,7 @@ func (svr *Service) handleConnection(ctx context.Context, conn net.Conn) { xl.Warn("register visitor conn error: %v", err) _ = msg.WriteMsg(conn, &msg.NewVisitorConnResp{ ProxyName: m.ProxyName, - Error: util.GenerateResponseErrorString("register visitor conn error", err, svr.cfg.DetailedErrorsToClient), + Error: util.GenerateResponseErrorString("register visitor conn error", err, lo.FromPtr(svr.cfg.DetailedErrorsToClient)), }) conn.Close() } else { @@ -461,7 +455,7 @@ func (svr *Service) HandleListener(l net.Listener) { log.Trace("start check TLS connection...") originConn := c var isTLS, custom bool - c, isTLS, custom, err = utilnet.CheckAndEnableTLSServerConnWithTimeout(c, svr.tlsConfig, svr.cfg.TLSOnly, connReadTimeout) + c, isTLS, custom, err = utilnet.CheckAndEnableTLSServerConnWithTimeout(c, svr.tlsConfig, svr.cfg.Transport.TLS.Force, connReadTimeout) if err != nil { log.Warn("CheckAndEnableTLSServerConnWithTimeout error: %v", err) originConn.Close() @@ -471,9 +465,9 @@ func (svr *Service) HandleListener(l net.Listener) { // Start a new goroutine to handle connection. go func(ctx context.Context, frpConn net.Conn) { - if svr.cfg.TCPMux { + if lo.FromPtr(svr.cfg.Transport.TCPMux) { fmuxCfg := fmux.DefaultConfig() - fmuxCfg.KeepAliveInterval = time.Duration(svr.cfg.TCPMuxKeepaliveInterval) * time.Second + fmuxCfg.KeepAliveInterval = time.Duration(svr.cfg.Transport.TCPMuxKeepaliveInterval) * time.Second fmuxCfg.LogOutput = io.Discard fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024 session, err := fmux.Server(frpConn, fmuxCfg) @@ -539,12 +533,6 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login) (err xl.Info("client login info: ip [%s] version [%s] hostname [%s] os [%s] arch [%s]", ctlConn.RemoteAddr().String(), loginMsg.Version, loginMsg.Hostname, loginMsg.Os, loginMsg.Arch) - // Check client version. - if ok, msg := version.Compat(loginMsg.Version); !ok { - err = fmt.Errorf("%s", msg) - return - } - // Check auth. if err = svr.authVerifier.VerifyLogin(loginMsg); err != nil { return @@ -594,7 +582,7 @@ func (svr *Service) RegisterWorkConn(workConn net.Conn, newMsg *msg.NewWorkConn) if err != nil { xl.Warn("invalid NewWorkConn with run id [%s]", newMsg.RunID) _ = msg.WriteMsg(workConn, &msg.StartWorkConn{ - Error: util.GenerateResponseErrorString("invalid NewWorkConn", err, ctl.serverCfg.DetailedErrorsToClient), + Error: util.GenerateResponseErrorString("invalid NewWorkConn", err, lo.FromPtr(svr.cfg.DetailedErrorsToClient)), }) return fmt.Errorf("invalid NewWorkConn with run id [%s]", newMsg.RunID) } @@ -603,7 +591,8 @@ func (svr *Service) RegisterWorkConn(workConn net.Conn, newMsg *msg.NewWorkConn) func (svr *Service) RegisterVisitorConn(visitorConn net.Conn, newMsg *msg.NewVisitorConn) error { visitorUser := "" - // TODO: Compatible with old versions, can be without runID, user is empty. In later versions, it will be mandatory to include runID. + // TODO(deprecation): Compatible with old versions, can be without runID, user is empty. In later versions, it will be mandatory to include runID. + // If runID is required, it is not compatible with versions prior to v0.50.0. if newMsg.RunID != "" { ctl, exist := svr.ctlManager.GetByID(newMsg.RunID) if !exist { diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index f809288e1ca..27c36e0c809 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -10,10 +10,13 @@ import ( "github.com/fatedier/frp/pkg/util/log" // test source - _ "github.com/fatedier/frp/test/e2e/basic" - _ "github.com/fatedier/frp/test/e2e/features" "github.com/fatedier/frp/test/e2e/framework" - _ "github.com/fatedier/frp/test/e2e/plugin" + _ "github.com/fatedier/frp/test/e2e/legacy/basic" + _ "github.com/fatedier/frp/test/e2e/legacy/features" + _ "github.com/fatedier/frp/test/e2e/legacy/plugin" + _ "github.com/fatedier/frp/test/e2e/v1/basic" + _ "github.com/fatedier/frp/test/e2e/v1/features" + _ "github.com/fatedier/frp/test/e2e/v1/plugin" ) // handleFlags sets up all flags and parses the command line. @@ -31,7 +34,7 @@ func TestMain(m *testing.M) { os.Exit(1) } - log.InitLog("console", "", framework.TestContext.LogLevel, 0, true) + log.InitLog("console", framework.TestContext.LogLevel, 0, true) os.Exit(m.Run()) } diff --git a/test/e2e/examples.go b/test/e2e/examples.go index 9accdf9cdf1..135ce706918 100644 --- a/test/e2e/examples.go +++ b/test/e2e/examples.go @@ -19,10 +19,11 @@ var _ = ginkgo.Describe("[Feature: Example]", func() { remotePort := f.AllocPort() clientConf += fmt.Sprintf(` - [tcp] - type = tcp - local_port = {{ .%s }} - remote_port = %d + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d `, framework.TCPEchoServerPort, remotePort) f.RunProcesses([]string{serverConf}, []string{clientConf}) diff --git a/test/e2e/framework/consts/consts.go b/test/e2e/framework/consts/consts.go index 622eba924b0..1e5ca500401 100644 --- a/test/e2e/framework/consts/consts.go +++ b/test/e2e/framework/consts/consts.go @@ -18,12 +18,23 @@ var ( PortClientAdmin string DefaultServerConfig = ` +bindPort = {{ .%s }} +log.level = "trace" +` + + DefaultClientConfig = ` +serverAddr = "127.0.0.1" +serverPort = {{ .%s }} +log.level = "trace" +` + + LegacyDefaultServerConfig = ` [common] bind_port = {{ .%s }} log_level = trace ` - DefaultClientConfig = ` + LegacyDefaultClientConfig = ` [common] server_addr = 127.0.0.1 server_port = {{ .%s }} @@ -34,6 +45,9 @@ var ( func init() { PortServerName = port.GenName("Server") PortClientAdmin = port.GenName("ClientAdmin") + LegacyDefaultServerConfig = fmt.Sprintf(LegacyDefaultServerConfig, port.GenName("Server")) + LegacyDefaultClientConfig = fmt.Sprintf(LegacyDefaultClientConfig, port.GenName("Server")) + DefaultServerConfig = fmt.Sprintf(DefaultServerConfig, port.GenName("Server")) DefaultClientConfig = fmt.Sprintf(DefaultClientConfig, port.GenName("Server")) } diff --git a/test/e2e/framework/process.go b/test/e2e/framework/process.go index ca717e258b3..437c8997ca5 100644 --- a/test/e2e/framework/process.go +++ b/test/e2e/framework/process.go @@ -29,7 +29,10 @@ func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []str path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-server-%d", i)) err = os.WriteFile(path, []byte(outs[i]), 0o666) ExpectNoError(err) - flog.Trace("[%s] %s", path, outs[i]) + + if TestContext.Debug { + flog.Debug("[%s] %s", path, outs[i]) + } p := process.NewWithEnvs(TestContext.FRPServerPath, []string{"-c", path}, f.osEnvs) f.serverConfPaths = append(f.serverConfPaths, path) @@ -46,7 +49,10 @@ func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []str path := filepath.Join(f.TempDirectory, fmt.Sprintf("frp-e2e-client-%d", i)) err = os.WriteFile(path, []byte(outs[index]), 0o666) ExpectNoError(err) - flog.Trace("[%s] %s", path, outs[index]) + + if TestContext.Debug { + flog.Debug("[%s] %s", path, outs[index]) + } p := process.NewWithEnvs(TestContext.FRPClientPath, []string{"-c", path}, f.osEnvs) f.clientConfPaths = append(f.clientConfPaths, path) diff --git a/test/e2e/basic/basic.go b/test/e2e/legacy/basic/basic.go similarity index 95% rename from test/e2e/basic/basic.go rename to test/e2e/legacy/basic/basic.go index 805b46010b2..763d3353a34 100644 --- a/test/e2e/basic/basic.go +++ b/test/e2e/legacy/basic/basic.go @@ -25,8 +25,8 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { for _, t := range types { proxyType := t ginkgo.It(fmt.Sprintf("Expose a %s echo server", strings.ToUpper(proxyType)), func() { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + serverConf := consts.LegacyDefaultServerConfig + clientConf := consts.LegacyDefaultClientConfig localPortName := "" protocol := "tcp" @@ -96,13 +96,13 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { ginkgo.Describe("HTTP", func() { ginkgo.It("proxy to HTTP server", func() { - serverConf := consts.DefaultServerConfig + serverConf := consts.LegacyDefaultServerConfig vhostHTTPPort := f.AllocPort() serverConf += fmt.Sprintf(` vhost_http_port = %d `, vhostHTTPPort) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig getProxyConf := func(proxyName string, customDomains string, extra string) string { return fmt.Sprintf(` @@ -178,14 +178,14 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { ginkgo.Describe("HTTPS", func() { ginkgo.It("proxy to HTTPS server", func() { - serverConf := consts.DefaultServerConfig + serverConf := consts.LegacyDefaultServerConfig vhostHTTPSPort := f.AllocPort() serverConf += fmt.Sprintf(` vhost_https_port = %d `, vhostHTTPSPort) localPort := f.AllocPort() - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig getProxyConf := func(proxyName string, customDomains string, extra string) string { return fmt.Sprintf(` [%s] @@ -281,10 +281,10 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { for _, t := range types { proxyType := t ginkgo.It(fmt.Sprintf("Expose echo server with %s", strings.ToUpper(proxyType)), func() { - serverConf := consts.DefaultServerConfig - clientServerConf := consts.DefaultClientConfig + "\nuser = user1" - clientVisitorConf := consts.DefaultClientConfig + "\nuser = user1" - clientUser2VisitorConf := consts.DefaultClientConfig + "\nuser = user2" + serverConf := consts.LegacyDefaultServerConfig + clientServerConf := consts.LegacyDefaultClientConfig + "\nuser = user1" + clientVisitorConf := consts.LegacyDefaultClientConfig + "\nuser = user1" + clientUser2VisitorConf := consts.LegacyDefaultClientConfig + "\nuser = user2" localPortName := "" protocol := "tcp" @@ -439,8 +439,8 @@ var _ = ginkgo.Describe("[Feature: Basic]", func() { ginkgo.Describe("TCPMUX", func() { ginkgo.It("Type tcpmux", func() { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + serverConf := consts.LegacyDefaultServerConfig + clientConf := consts.LegacyDefaultClientConfig tcpmuxHTTPConnectPortName := port.GenName("TCPMUX") serverConf += fmt.Sprintf(` diff --git a/test/e2e/basic/client.go b/test/e2e/legacy/basic/client.go similarity index 88% rename from test/e2e/basic/client.go rename to test/e2e/legacy/basic/client.go index fb9b16b573a..da23db9c1fe 100644 --- a/test/e2e/basic/client.go +++ b/test/e2e/legacy/basic/client.go @@ -8,17 +8,17 @@ import ( "github.com/onsi/ginkgo/v2" + clientsdk "github.com/fatedier/frp/pkg/sdk/client" "github.com/fatedier/frp/test/e2e/framework" "github.com/fatedier/frp/test/e2e/framework/consts" "github.com/fatedier/frp/test/e2e/pkg/request" - clientsdk "github.com/fatedier/frp/test/e2e/pkg/sdk/client" ) var _ = ginkgo.Describe("[Feature: ClientManage]", func() { f := framework.NewDefaultFramework() ginkgo.It("Update && Reload API", func() { - serverConf := consts.DefaultServerConfig + serverConf := consts.LegacyDefaultServerConfig adminPort := f.AllocPort() @@ -26,7 +26,7 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() { p2Port := f.AllocPort() p3Port := f.AllocPort() - clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + clientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(` admin_port = %d [p1] @@ -80,10 +80,10 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() { }) ginkgo.It("healthz", func() { - serverConf := consts.DefaultServerConfig + serverConf := consts.LegacyDefaultServerConfig dashboardPort := f.AllocPort() - clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + clientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(` admin_addr = 0.0.0.0 admin_port = %d admin_user = admin @@ -103,11 +103,11 @@ var _ = ginkgo.Describe("[Feature: ClientManage]", func() { }) ginkgo.It("stop", func() { - serverConf := consts.DefaultServerConfig + serverConf := consts.LegacyDefaultServerConfig adminPort := f.AllocPort() testPort := f.AllocPort() - clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + clientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(` admin_port = %d [test] diff --git a/test/e2e/basic/client_server.go b/test/e2e/legacy/basic/client_server.go similarity index 98% rename from test/e2e/basic/client_server.go rename to test/e2e/legacy/basic/client_server.go index e7730f459ce..03bca0c54e2 100644 --- a/test/e2e/basic/client_server.go +++ b/test/e2e/legacy/basic/client_server.go @@ -33,8 +33,8 @@ func renderBindPortConfig(protocol string) string { } func runClientServerTest(f *framework.Framework, configures *generalTestConfigures) { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + serverConf := consts.LegacyDefaultServerConfig + clientConf := consts.LegacyDefaultClientConfig if configures.clientPrefix != "" { clientConf = configures.clientPrefix } @@ -64,7 +64,7 @@ func runClientServerTest(f *framework.Framework, configures *generalTestConfigur clientConfs := []string{clientConf} if configures.client2 != "" { - client2Conf := consts.DefaultClientConfig + client2Conf := consts.LegacyDefaultClientConfig if configures.client2Prefix != "" { client2Conf = configures.client2Prefix } diff --git a/test/e2e/basic/cmd.go b/test/e2e/legacy/basic/cmd.go similarity index 90% rename from test/e2e/basic/cmd.go rename to test/e2e/legacy/basic/cmd.go index 89f2435dcf8..7d86233a0ad 100644 --- a/test/e2e/basic/cmd.go +++ b/test/e2e/legacy/basic/cmd.go @@ -1,7 +1,6 @@ package basic import ( - "fmt" "strconv" "strings" @@ -70,7 +69,7 @@ var _ = ginkgo.Describe("[Feature: Cmd]", func() { localPort := f.PortByName(framework.TCPEchoServerPort) remotePort := f.AllocPort() - _, _, err = f.RunFrpc("tcp", "-s", fmt.Sprintf("127.0.0.1:%d", serverPort), "-t", "123", "-u", "test", + _, _, err = f.RunFrpc("tcp", "-s", "127.0.0.1", "-P", strconv.Itoa(serverPort), "-t", "123", "-u", "test", "-l", strconv.Itoa(localPort), "-r", strconv.Itoa(remotePort), "-n", "tcp_test") framework.ExpectNoError(err) @@ -84,7 +83,7 @@ var _ = ginkgo.Describe("[Feature: Cmd]", func() { localPort := f.PortByName(framework.UDPEchoServerPort) remotePort := f.AllocPort() - _, _, err = f.RunFrpc("udp", "-s", fmt.Sprintf("127.0.0.1:%d", serverPort), "-t", "123", "-u", "test", + _, _, err = f.RunFrpc("udp", "-s", "127.0.0.1", "-P", strconv.Itoa(serverPort), "-t", "123", "-u", "test", "-l", strconv.Itoa(localPort), "-r", strconv.Itoa(remotePort), "-n", "udp_test") framework.ExpectNoError(err) @@ -98,7 +97,7 @@ var _ = ginkgo.Describe("[Feature: Cmd]", func() { _, _, err := f.RunFrps("-t", "123", "-p", strconv.Itoa(serverPort), "--vhost_http_port", strconv.Itoa(vhostHTTPPort)) framework.ExpectNoError(err) - _, _, err = f.RunFrpc("http", "-s", "127.0.0.1:"+strconv.Itoa(serverPort), "-t", "123", "-u", "test", + _, _, err = f.RunFrpc("http", "-s", "127.0.0.1", "-P", strconv.Itoa(serverPort), "-t", "123", "-u", "test", "-n", "udp_test", "-l", strconv.Itoa(f.PortByName(framework.HTTPSimpleServerPort)), "--custom_domain", "test.example.com") framework.ExpectNoError(err) diff --git a/test/e2e/basic/config.go b/test/e2e/legacy/basic/config.go similarity index 95% rename from test/e2e/basic/config.go rename to test/e2e/legacy/basic/config.go index acee2c560ec..6e9f35643ea 100644 --- a/test/e2e/basic/config.go +++ b/test/e2e/legacy/basic/config.go @@ -15,8 +15,8 @@ var _ = ginkgo.Describe("[Feature: Config]", func() { ginkgo.Describe("Template", func() { ginkgo.It("render by env", func() { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + serverConf := consts.LegacyDefaultServerConfig + clientConf := consts.LegacyDefaultClientConfig portName := port.GenName("TCP") serverConf += fmt.Sprintf(` diff --git a/test/e2e/basic/http.go b/test/e2e/legacy/basic/http.go similarity index 95% rename from test/e2e/basic/http.go rename to test/e2e/legacy/basic/http.go index f8b173879e3..77ba105264a 100644 --- a/test/e2e/basic/http.go +++ b/test/e2e/legacy/basic/http.go @@ -19,7 +19,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { f := framework.NewDefaultFramework() getDefaultServerConf := func(vhostHTTPPort int) string { - conf := consts.DefaultServerConfig + ` + conf := consts.LegacyDefaultServerConfig + ` vhost_http_port = %d ` return fmt.Sprintf(conf, vhostHTTPPort) @@ -41,7 +41,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { barPort := f.AllocPort() f.RunServer("", newHTTPServer(barPort, "bar")) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [foo] type = http @@ -91,7 +91,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { otherPort := f.AllocPort() f.RunServer("", newHTTPServer(otherPort, "other")) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [foo] type = http @@ -142,7 +142,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { vhostHTTPPort := f.AllocPort() serverConf := getDefaultServerConf(vhostHTTPPort) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [test] type = http @@ -180,7 +180,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { vhostHTTPPort := f.AllocPort() serverConf := getDefaultServerConf(vhostHTTPPort) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [test] type = http @@ -225,7 +225,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { barPort := f.AllocPort() f.RunServer("", newHTTPServer(barPort, "bar")) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [foo] type = http @@ -270,7 +270,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { ) f.RunServer("", localServer) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [test] type = http @@ -303,7 +303,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { ) f.RunServer("", localServer) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [test] type = http @@ -352,7 +352,7 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() { f.RunServer("", localServer) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [test] type = http diff --git a/test/e2e/basic/server.go b/test/e2e/legacy/basic/server.go similarity index 90% rename from test/e2e/basic/server.go rename to test/e2e/legacy/basic/server.go index bfa2f2eb87e..f3c2a22824e 100644 --- a/test/e2e/basic/server.go +++ b/test/e2e/legacy/basic/server.go @@ -7,19 +7,19 @@ import ( "github.com/onsi/ginkgo/v2" + clientsdk "github.com/fatedier/frp/pkg/sdk/client" "github.com/fatedier/frp/test/e2e/framework" "github.com/fatedier/frp/test/e2e/framework/consts" "github.com/fatedier/frp/test/e2e/pkg/port" "github.com/fatedier/frp/test/e2e/pkg/request" - clientsdk "github.com/fatedier/frp/test/e2e/pkg/sdk/client" ) var _ = ginkgo.Describe("[Feature: Server Manager]", func() { f := framework.NewDefaultFramework() ginkgo.It("Ports Whitelist", func() { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + serverConf := consts.LegacyDefaultServerConfig + clientConf := consts.LegacyDefaultClientConfig serverConf += ` allow_ports = 20000-25000,25002,30000-50000 @@ -81,8 +81,8 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() { }) ginkgo.It("Alloc Random Port", func() { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + serverConf := consts.LegacyDefaultServerConfig + clientConf := consts.LegacyDefaultClientConfig adminPort := f.AllocPort() clientConf += fmt.Sprintf(` @@ -125,13 +125,13 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() { }) ginkgo.It("Port Reuse", func() { - serverConf := consts.DefaultServerConfig + serverConf := consts.LegacyDefaultServerConfig // Use same port as PortServer serverConf += fmt.Sprintf(` vhost_http_port = {{ .%s }} `, consts.PortServerName) - clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + clientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(` [http] type = http local_port = {{ .%s }} @@ -146,7 +146,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() { }) ginkgo.It("healthz", func() { - serverConf := consts.DefaultServerConfig + serverConf := consts.LegacyDefaultServerConfig dashboardPort := f.AllocPort() // Use same port as PortServer @@ -158,7 +158,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() { dashboard_pwd = admin `, consts.PortServerName, dashboardPort) - clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + clientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(` [http] type = http local_port = {{ .%s }} diff --git a/test/e2e/basic/tcpmux.go b/test/e2e/legacy/basic/tcpmux.go similarity index 96% rename from test/e2e/basic/tcpmux.go rename to test/e2e/legacy/basic/tcpmux.go index a1106b958ba..5bb742bc8e3 100644 --- a/test/e2e/basic/tcpmux.go +++ b/test/e2e/legacy/basic/tcpmux.go @@ -20,7 +20,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() { f := framework.NewDefaultFramework() getDefaultServerConf := func(httpconnectPort int) string { - conf := consts.DefaultServerConfig + ` + conf := consts.LegacyDefaultServerConfig + ` tcpmux_httpconnect_port = %d ` return fmt.Sprintf(conf, httpconnectPort) @@ -53,7 +53,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() { otherPort := f.AllocPort() f.RunServer("", newServer(otherPort, "other")) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [foo] type = tcpmux @@ -110,7 +110,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() { fooPort := f.AllocPort() f.RunServer("", newServer(fooPort, "foo")) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [test] type = tcpmux @@ -195,7 +195,7 @@ var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() { localPort := f.AllocPort() f.RunServer("", newServer(localPort)) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [test] type = tcpmux diff --git a/test/e2e/basic/xtcp.go b/test/e2e/legacy/basic/xtcp.go similarity index 91% rename from test/e2e/basic/xtcp.go rename to test/e2e/legacy/basic/xtcp.go index a501d7935c5..3c47f577808 100644 --- a/test/e2e/basic/xtcp.go +++ b/test/e2e/legacy/basic/xtcp.go @@ -16,8 +16,8 @@ var _ = ginkgo.Describe("[Feature: XTCP]", func() { f := framework.NewDefaultFramework() ginkgo.It("Fallback To STCP", func() { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + serverConf := consts.LegacyDefaultServerConfig + clientConf := consts.LegacyDefaultClientConfig bindPortName := port.GenName("XTCP") clientConf += fmt.Sprintf(` diff --git a/test/e2e/features/bandwidth_limit.go b/test/e2e/legacy/features/bandwidth_limit.go similarity index 88% rename from test/e2e/features/bandwidth_limit.go rename to test/e2e/legacy/features/bandwidth_limit.go index 96d236462d6..c94e473f063 100644 --- a/test/e2e/features/bandwidth_limit.go +++ b/test/e2e/legacy/features/bandwidth_limit.go @@ -11,16 +11,16 @@ import ( "github.com/fatedier/frp/test/e2e/framework" "github.com/fatedier/frp/test/e2e/framework/consts" "github.com/fatedier/frp/test/e2e/mock/server/streamserver" + pluginpkg "github.com/fatedier/frp/test/e2e/pkg/plugin" "github.com/fatedier/frp/test/e2e/pkg/request" - plugintest "github.com/fatedier/frp/test/e2e/plugin" ) var _ = ginkgo.Describe("[Feature: Bandwidth Limit]", func() { f := framework.NewDefaultFramework() ginkgo.It("Proxy Bandwidth Limit by Client", func() { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + serverConf := consts.LegacyDefaultServerConfig + clientConf := consts.LegacyDefaultClientConfig localPort := f.AllocPort() localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort)) @@ -65,17 +65,17 @@ var _ = ginkgo.Describe("[Feature: Bandwidth Limit]", func() { ret.Content = content return &ret } - pluginServer := plugintest.NewHTTPPluginServer(pluginPort, newFunc, handler, nil) + pluginServer := pluginpkg.NewHTTPPluginServer(pluginPort, newFunc, handler, nil) f.RunServer("", pluginServer) - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` [plugin.test] addr = 127.0.0.1:%d path = /handler ops = NewProxy `, pluginPort) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig localPort := f.AllocPort() localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort)) diff --git a/test/e2e/features/chaos.go b/test/e2e/legacy/features/chaos.go similarity index 100% rename from test/e2e/features/chaos.go rename to test/e2e/legacy/features/chaos.go diff --git a/test/e2e/features/group.go b/test/e2e/legacy/features/group.go similarity index 95% rename from test/e2e/features/group.go rename to test/e2e/legacy/features/group.go index 8ec6112549e..dc15f75c5da 100644 --- a/test/e2e/features/group.go +++ b/test/e2e/legacy/features/group.go @@ -62,8 +62,8 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { ginkgo.Describe("Load Balancing", func() { ginkgo.It("TCP", func() { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + serverConf := consts.LegacyDefaultServerConfig + clientConf := consts.LegacyDefaultClientConfig fooPort := f.AllocPort() fooServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(fooPort), streamserver.WithRespContent([]byte("foo"))) @@ -114,8 +114,8 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { ginkgo.Describe("Health Check", func() { ginkgo.It("TCP", func() { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + serverConf := consts.LegacyDefaultServerConfig + clientConf := consts.LegacyDefaultClientConfig fooPort := f.AllocPort() fooServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(fooPort), streamserver.WithRespContent([]byte("foo"))) @@ -180,10 +180,10 @@ var _ = ginkgo.Describe("[Feature: Group]", func() { ginkgo.It("HTTP", func() { vhostPort := f.AllocPort() - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` vhost_http_port = %d `, vhostPort) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig fooPort := f.AllocPort() fooServer := newHTTPServer(fooPort, "foo") diff --git a/test/e2e/features/heartbeat.go b/test/e2e/legacy/features/heartbeat.go similarity index 100% rename from test/e2e/features/heartbeat.go rename to test/e2e/legacy/features/heartbeat.go diff --git a/test/e2e/features/monitor.go b/test/e2e/legacy/features/monitor.go similarity index 91% rename from test/e2e/features/monitor.go rename to test/e2e/legacy/features/monitor.go index 74996d2cad5..8968df9f01b 100644 --- a/test/e2e/features/monitor.go +++ b/test/e2e/legacy/features/monitor.go @@ -18,13 +18,13 @@ var _ = ginkgo.Describe("[Feature: Monitor]", func() { ginkgo.It("Prometheus metrics", func() { dashboardPort := f.AllocPort() - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` enable_prometheus = true dashboard_addr = 0.0.0.0 dashboard_port = %d `, dashboardPort) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig remotePort := f.AllocPort() clientConf += fmt.Sprintf(` [tcp] diff --git a/test/e2e/features/real_ip.go b/test/e2e/legacy/features/real_ip.go similarity index 92% rename from test/e2e/features/real_ip.go rename to test/e2e/legacy/features/real_ip.go index 31f02fe2db3..082df8c6260 100644 --- a/test/e2e/features/real_ip.go +++ b/test/e2e/legacy/features/real_ip.go @@ -23,7 +23,7 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() { ginkgo.It("HTTP X-Forwarded-For", func() { vhostHTTPPort := f.AllocPort() - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` vhost_http_port = %d `, vhostHTTPPort) @@ -36,7 +36,7 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() { ) f.RunServer("", localServer) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [test] type = http @@ -56,8 +56,8 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() { ginkgo.Describe("Proxy Protocol", func() { ginkgo.It("TCP", func() { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + serverConf := consts.LegacyDefaultServerConfig + clientConf := consts.LegacyDefaultClientConfig localPort := f.AllocPort() localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort), @@ -107,11 +107,11 @@ var _ = ginkgo.Describe("[Feature: Real IP]", func() { ginkgo.It("HTTP", func() { vhostHTTPPort := f.AllocPort() - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` vhost_http_port = %d `, vhostHTTPPort) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig localPort := f.AllocPort() var srcAddrRecord string diff --git a/test/e2e/plugin/client.go b/test/e2e/legacy/plugin/client.go similarity index 91% rename from test/e2e/plugin/client.go rename to test/e2e/legacy/plugin/client.go index 3046b900c93..b69c6aed780 100644 --- a/test/e2e/plugin/client.go +++ b/test/e2e/legacy/plugin/client.go @@ -21,8 +21,8 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { ginkgo.Describe("UnixDomainSocket", func() { ginkgo.It("Expose a unix domain socket echo server", func() { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + serverConf := consts.LegacyDefaultServerConfig + clientConf := consts.LegacyDefaultClientConfig getProxyConf := func(proxyName string, portName string, extra string) string { return fmt.Sprintf(` @@ -77,8 +77,8 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { }) ginkgo.It("http_proxy", func() { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + serverConf := consts.LegacyDefaultServerConfig + clientConf := consts.LegacyDefaultClientConfig remotePort := f.AllocPort() clientConf += fmt.Sprintf(` @@ -109,8 +109,8 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { }) ginkgo.It("socks5 proxy", func() { - serverConf := consts.DefaultServerConfig - clientConf := consts.DefaultClientConfig + serverConf := consts.LegacyDefaultServerConfig + clientConf := consts.LegacyDefaultClientConfig remotePort := f.AllocPort() clientConf += fmt.Sprintf(` @@ -137,10 +137,10 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { ginkgo.It("static_file", func() { vhostPort := f.AllocPort() - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` vhost_http_port = %d `, vhostPort) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig remotePort := f.AllocPort() f.WriteTempFile("test_static_file", "foo") @@ -185,14 +185,14 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { }) ginkgo.It("http2https", func() { - serverConf := consts.DefaultServerConfig + serverConf := consts.LegacyDefaultServerConfig vhostHTTPPort := f.AllocPort() serverConf += fmt.Sprintf(` vhost_http_port = %d `, vhostHTTPPort) localPort := f.AllocPort() - clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + clientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(` [http2https] type = http custom_domains = example.com @@ -227,14 +227,14 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { crtPath := f.WriteTempFile("server.crt", string(artifacts.Cert)) keyPath := f.WriteTempFile("server.key", string(artifacts.Key)) - serverConf := consts.DefaultServerConfig + serverConf := consts.LegacyDefaultServerConfig vhostHTTPSPort := f.AllocPort() serverConf += fmt.Sprintf(` vhost_https_port = %d `, vhostHTTPSPort) localPort := f.AllocPort() - clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + clientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(` [https2http] type = https custom_domains = example.com @@ -271,14 +271,14 @@ var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { crtPath := f.WriteTempFile("server.crt", string(artifacts.Cert)) keyPath := f.WriteTempFile("server.key", string(artifacts.Key)) - serverConf := consts.DefaultServerConfig + serverConf := consts.LegacyDefaultServerConfig vhostHTTPSPort := f.AllocPort() serverConf += fmt.Sprintf(` vhost_https_port = %d `, vhostHTTPSPort) localPort := f.AllocPort() - clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + clientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(` [https2https] type = https custom_domains = example.com diff --git a/test/e2e/plugin/server.go b/test/e2e/legacy/plugin/server.go similarity index 83% rename from test/e2e/plugin/server.go rename to test/e2e/legacy/plugin/server.go index 955e3b5477f..3f14a42dcf6 100644 --- a/test/e2e/plugin/server.go +++ b/test/e2e/legacy/plugin/server.go @@ -10,6 +10,7 @@ import ( "github.com/fatedier/frp/pkg/transport" "github.com/fatedier/frp/test/e2e/framework" "github.com/fatedier/frp/test/e2e/framework/consts" + pluginpkg "github.com/fatedier/frp/test/e2e/pkg/plugin" ) var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { @@ -40,17 +41,17 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { } return &ret } - pluginServer := NewHTTPPluginServer(localPort, newFunc, handler, nil) + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) f.RunServer("", pluginServer) - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` [plugin.user-manager] addr = 127.0.0.1:%d path = /handler ops = Login `, localPort) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig remotePort := f.AllocPort() clientConf += fmt.Sprintf(` @@ -63,7 +64,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { `, framework.TCPEchoServerPort, remotePort) remotePort2 := f.AllocPort() - invalidTokenClientConf := consts.DefaultClientConfig + fmt.Sprintf(` + invalidTokenClientConf := consts.LegacyDefaultClientConfig + fmt.Sprintf(` [tcp2] type = tcp local_port = {{ .%s }} @@ -98,17 +99,17 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { } return &ret } - pluginServer := NewHTTPPluginServer(localPort, newFunc, handler, nil) + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) f.RunServer("", pluginServer) - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` [plugin.test] addr = 127.0.0.1:%d path = /handler ops = NewProxy `, localPort) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig remotePort := f.AllocPort() clientConf += fmt.Sprintf(` @@ -133,17 +134,17 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { ret.Content = content return &ret } - pluginServer := NewHTTPPluginServer(localPort, newFunc, handler, nil) + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) f.RunServer("", pluginServer) - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` [plugin.test] addr = 127.0.0.1:%d path = /handler ops = NewProxy `, localPort) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [tcp] @@ -174,17 +175,17 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { recordProxyName = content.ProxyName return &ret } - pluginServer := NewHTTPPluginServer(localPort, newFunc, handler, nil) + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) f.RunServer("", pluginServer) - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` [plugin.test] addr = 127.0.0.1:%d path = /handler ops = CloseProxy `, localPort) - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig remotePort := f.AllocPort() clientConf += fmt.Sprintf(` @@ -226,11 +227,11 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { ret.Unchange = true return &ret } - pluginServer := NewHTTPPluginServer(localPort, newFunc, handler, nil) + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) f.RunServer("", pluginServer) - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` [plugin.test] addr = 127.0.0.1:%d path = /handler @@ -238,7 +239,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { `, localPort) remotePort := f.AllocPort() - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` heartbeat_interval = 1 authenticate_heartbeats = true @@ -276,11 +277,11 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { ret.Unchange = true return &ret } - pluginServer := NewHTTPPluginServer(localPort, newFunc, handler, nil) + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) f.RunServer("", pluginServer) - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` [plugin.test] addr = 127.0.0.1:%d path = /handler @@ -288,7 +289,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { `, localPort) remotePort := f.AllocPort() - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [tcp] type = tcp @@ -321,11 +322,11 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { ret.Unchange = true return &ret } - pluginServer := NewHTTPPluginServer(localPort, newFunc, handler, nil) + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) f.RunServer("", pluginServer) - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` [plugin.test] addr = 127.0.0.1:%d path = /handler @@ -333,7 +334,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { `, localPort) remotePort := f.AllocPort() - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [tcp] type = tcp @@ -368,11 +369,11 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { } tlsConfig, err := transport.NewServerTLSConfig("", "", "") framework.ExpectNoError(err) - pluginServer := NewHTTPPluginServer(localPort, newFunc, handler, tlsConfig) + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, tlsConfig) f.RunServer("", pluginServer) - serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + serverConf := consts.LegacyDefaultServerConfig + fmt.Sprintf(` [plugin.test] addr = https://127.0.0.1:%d path = /handler @@ -380,7 +381,7 @@ var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { `, localPort) remotePort := f.AllocPort() - clientConf := consts.DefaultClientConfig + clientConf := consts.LegacyDefaultClientConfig clientConf += fmt.Sprintf(` [tcp] type = tcp diff --git a/test/e2e/plugin/utils.go b/test/e2e/pkg/plugin/plugin.go similarity index 100% rename from test/e2e/plugin/utils.go rename to test/e2e/pkg/plugin/plugin.go diff --git a/test/e2e/pkg/request/request.go b/test/e2e/pkg/request/request.go index 44bc0d0ccb6..50deb3bf31a 100644 --- a/test/e2e/pkg/request/request.go +++ b/test/e2e/pkg/request/request.go @@ -14,8 +14,8 @@ import ( libdial "github.com/fatedier/golib/net/dial" + "github.com/fatedier/frp/pkg/util/util" "github.com/fatedier/frp/test/e2e/pkg/rpc" - "github.com/fatedier/frp/test/e2e/pkg/utils" ) type Request struct { @@ -115,7 +115,7 @@ func (r *Request) HTTPHeaders(headers map[string]string) *Request { } func (r *Request) HTTPAuth(user, password string) *Request { - r.authValue = utils.BasicAuth(user, password) + r.authValue = util.BasicAuth(user, password) return r } diff --git a/test/e2e/pkg/utils/utils.go b/test/e2e/pkg/utils/utils.go deleted file mode 100644 index 7b177e14bb0..00000000000 --- a/test/e2e/pkg/utils/utils.go +++ /dev/null @@ -1,10 +0,0 @@ -package utils - -import ( - "encoding/base64" -) - -func BasicAuth(username, passwd string) string { - auth := username + ":" + passwd - return "Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) -} diff --git a/test/e2e/v1/basic/basic.go b/test/e2e/v1/basic/basic.go new file mode 100644 index 00000000000..9d9d34cb39d --- /dev/null +++ b/test/e2e/v1/basic/basic.go @@ -0,0 +1,524 @@ +package basic + +import ( + "crypto/tls" + "fmt" + "strings" + "time" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/pkg/transport" + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/mock/server/httpserver" + "github.com/fatedier/frp/test/e2e/mock/server/streamserver" + "github.com/fatedier/frp/test/e2e/pkg/port" + "github.com/fatedier/frp/test/e2e/pkg/request" +) + +var _ = ginkgo.Describe("[Feature: Basic]", func() { + f := framework.NewDefaultFramework() + + ginkgo.Describe("TCP && UDP", func() { + types := []string{"tcp", "udp"} + for _, t := range types { + proxyType := t + ginkgo.It(fmt.Sprintf("Expose a %s echo server", strings.ToUpper(proxyType)), func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + localPortName := "" + protocol := "tcp" + switch proxyType { + case "tcp": + localPortName = framework.TCPEchoServerPort + protocol = "tcp" + case "udp": + localPortName = framework.UDPEchoServerPort + protocol = "udp" + } + getProxyConf := func(proxyName string, portName string, extra string) string { + return fmt.Sprintf(` + [[proxies]] + name = "%s" + type = "%s" + localPort = {{ .%s }} + remotePort = {{ .%s }} + `+extra, proxyName, proxyType, localPortName, portName) + } + + tests := []struct { + proxyName string + portName string + extraConfig string + }{ + { + proxyName: "normal", + portName: port.GenName("Normal"), + }, + { + proxyName: "with-encryption", + portName: port.GenName("WithEncryption"), + extraConfig: "transport.useEncryption = true", + }, + { + proxyName: "with-compression", + portName: port.GenName("WithCompression"), + extraConfig: "transport.useCompression = true", + }, + { + proxyName: "with-encryption-and-compression", + portName: port.GenName("WithEncryptionAndCompression"), + extraConfig: ` + transport.useEncryption = true + transport.useCompression = true + `, + }, + } + + // build all client config + for _, test := range tests { + clientConf += getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n" + } + // run frps and frpc + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + for _, test := range tests { + framework.NewRequestExpect(f). + Protocol(protocol). + PortName(test.portName). + Explain(test.proxyName). + Ensure() + } + }) + } + }) + + ginkgo.Describe("HTTP", func() { + ginkgo.It("proxy to HTTP server", func() { + serverConf := consts.DefaultServerConfig + vhostHTTPPort := f.AllocPort() + serverConf += fmt.Sprintf(` + vhostHTTPPort = %d + `, vhostHTTPPort) + + clientConf := consts.DefaultClientConfig + + getProxyConf := func(proxyName string, customDomains string, extra string) string { + return fmt.Sprintf(` + [[proxies]] + name = "%s" + type = "http" + localPort = {{ .%s }} + customDomains = %s + `+extra, proxyName, framework.HTTPSimpleServerPort, customDomains) + } + + tests := []struct { + proxyName string + customDomains string + extraConfig string + }{ + { + proxyName: "normal", + }, + { + proxyName: "with-encryption", + extraConfig: "transport.useEncryption = true", + }, + { + proxyName: "with-compression", + extraConfig: "transport.useCompression = true", + }, + { + proxyName: "with-encryption-and-compression", + extraConfig: ` + transport.useEncryption = true + transport.useCompression = true + `, + }, + { + proxyName: "multiple-custom-domains", + customDomains: `["a.example.com", "b.example.com"]`, + }, + } + + // build all client config + for i, test := range tests { + if tests[i].customDomains == "" { + tests[i].customDomains = fmt.Sprintf(`["%s"]`, test.proxyName+".example.com") + } + clientConf += getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n" + } + // run frps and frpc + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + for _, test := range tests { + for _, domain := range strings.Split(test.customDomains, ",") { + domain = strings.TrimSpace(domain) + domain = strings.TrimLeft(domain, "[\"") + domain = strings.TrimRight(domain, "]\"") + framework.NewRequestExpect(f). + Explain(test.proxyName + "-" + domain). + Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost(domain) + }). + Ensure() + } + } + + // not exist host + framework.NewRequestExpect(f). + Explain("not exist host"). + Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("not-exist.example.com") + }). + Ensure(framework.ExpectResponseCode(404)) + }) + }) + + ginkgo.Describe("HTTPS", func() { + ginkgo.It("proxy to HTTPS server", func() { + serverConf := consts.DefaultServerConfig + vhostHTTPSPort := f.AllocPort() + serverConf += fmt.Sprintf(` + vhostHTTPSPort = %d + `, vhostHTTPSPort) + + localPort := f.AllocPort() + clientConf := consts.DefaultClientConfig + getProxyConf := func(proxyName string, customDomains string, extra string) string { + return fmt.Sprintf(` + [[proxies]] + name = "%s" + type = "https" + localPort = %d + customDomains = %s + `+extra, proxyName, localPort, customDomains) + } + + tests := []struct { + proxyName string + customDomains string + extraConfig string + }{ + { + proxyName: "normal", + }, + { + proxyName: "with-encryption", + extraConfig: "transport.useEncryption = true", + }, + { + proxyName: "with-compression", + extraConfig: "transport.useCompression = true", + }, + { + proxyName: "with-encryption-and-compression", + extraConfig: ` + transport.useEncryption = true + transport.useCompression = true + `, + }, + { + proxyName: "multiple-custom-domains", + customDomains: `["a.example.com", "b.example.com"]`, + }, + } + + // build all client config + for i, test := range tests { + if tests[i].customDomains == "" { + tests[i].customDomains = fmt.Sprintf(`["%s"]`, test.proxyName+".example.com") + } + clientConf += getProxyConf(test.proxyName, tests[i].customDomains, test.extraConfig) + "\n" + } + // run frps and frpc + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + tlsConfig, err := transport.NewServerTLSConfig("", "", "") + framework.ExpectNoError(err) + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithTLSConfig(tlsConfig), + httpserver.WithResponse([]byte("test")), + ) + f.RunServer("", localServer) + + for _, test := range tests { + for _, domain := range strings.Split(test.customDomains, ",") { + domain = strings.TrimSpace(domain) + domain = strings.TrimLeft(domain, "[\"") + domain = strings.TrimRight(domain, "]\"") + framework.NewRequestExpect(f). + Explain(test.proxyName + "-" + domain). + Port(vhostHTTPSPort). + RequestModify(func(r *request.Request) { + r.HTTPS().HTTPHost(domain).TLSConfig(&tls.Config{ + ServerName: domain, + InsecureSkipVerify: true, + }) + }). + ExpectResp([]byte("test")). + Ensure() + } + } + + // not exist host + notExistDomain := "not-exist.example.com" + framework.NewRequestExpect(f). + Explain("not exist host"). + Port(vhostHTTPSPort). + RequestModify(func(r *request.Request) { + r.HTTPS().HTTPHost(notExistDomain).TLSConfig(&tls.Config{ + ServerName: notExistDomain, + InsecureSkipVerify: true, + }) + }). + ExpectError(true). + Ensure() + }) + }) + + ginkgo.Describe("STCP && SUDP && XTCP", func() { + types := []string{"stcp", "sudp", "xtcp"} + for _, t := range types { + proxyType := t + ginkgo.It(fmt.Sprintf("Expose echo server with %s", strings.ToUpper(proxyType)), func() { + serverConf := consts.DefaultServerConfig + clientServerConf := consts.DefaultClientConfig + "\nuser = \"user1\"" + clientVisitorConf := consts.DefaultClientConfig + "\nuser = \"user1\"" + clientUser2VisitorConf := consts.DefaultClientConfig + "\nuser = \"user2\"" + + localPortName := "" + protocol := "tcp" + switch proxyType { + case "stcp": + localPortName = framework.TCPEchoServerPort + protocol = "tcp" + case "sudp": + localPortName = framework.UDPEchoServerPort + protocol = "udp" + case "xtcp": + localPortName = framework.TCPEchoServerPort + protocol = "tcp" + ginkgo.Skip("stun server is not stable") + } + + correctSK := "abc" + wrongSK := "123" + + getProxyServerConf := func(proxyName string, extra string) string { + return fmt.Sprintf(` + [[proxies]] + name = "%s" + type = "%s" + secretKey = "%s" + localPort = {{ .%s }} + `+extra, proxyName, proxyType, correctSK, localPortName) + } + getProxyVisitorConf := func(proxyName string, portName, visitorSK, extra string) string { + return fmt.Sprintf(` + [[visitors]] + name = "%s" + type = "%s" + serverName = "%s" + secretKey = "%s" + bindPort = {{ .%s }} + `+extra, proxyName, proxyType, proxyName, visitorSK, portName) + } + + tests := []struct { + proxyName string + bindPortName string + visitorSK string + commonExtraConfig string + proxyExtraConfig string + visitorExtraConfig string + expectError bool + deployUser2Client bool + // skipXTCP is used to skip xtcp test case + skipXTCP bool + }{ + { + proxyName: "normal", + bindPortName: port.GenName("Normal"), + visitorSK: correctSK, + skipXTCP: true, + }, + { + proxyName: "with-encryption", + bindPortName: port.GenName("WithEncryption"), + visitorSK: correctSK, + commonExtraConfig: "transport.useEncryption = true", + skipXTCP: true, + }, + { + proxyName: "with-compression", + bindPortName: port.GenName("WithCompression"), + visitorSK: correctSK, + commonExtraConfig: "transport.useCompression = true", + skipXTCP: true, + }, + { + proxyName: "with-encryption-and-compression", + bindPortName: port.GenName("WithEncryptionAndCompression"), + visitorSK: correctSK, + commonExtraConfig: ` + transport.useEncryption = true + transport.useCompression = true + `, + skipXTCP: true, + }, + { + proxyName: "with-error-sk", + bindPortName: port.GenName("WithErrorSK"), + visitorSK: wrongSK, + expectError: true, + }, + { + proxyName: "allowed-user", + bindPortName: port.GenName("AllowedUser"), + visitorSK: correctSK, + proxyExtraConfig: `allowUsers = ["another", "user2"]`, + visitorExtraConfig: `serverUser = "user1"`, + deployUser2Client: true, + }, + { + proxyName: "not-allowed-user", + bindPortName: port.GenName("NotAllowedUser"), + visitorSK: correctSK, + proxyExtraConfig: `allowUsers = ["invalid"]`, + visitorExtraConfig: `serverUser = "user1"`, + expectError: true, + }, + { + proxyName: "allow-all", + bindPortName: port.GenName("AllowAll"), + visitorSK: correctSK, + proxyExtraConfig: `allowUsers = ["*"]`, + visitorExtraConfig: `serverUser = "user1"`, + deployUser2Client: true, + }, + } + + // build all client config + for _, test := range tests { + clientServerConf += getProxyServerConf(test.proxyName, test.commonExtraConfig+"\n"+test.proxyExtraConfig) + "\n" + } + for _, test := range tests { + config := getProxyVisitorConf( + test.proxyName, test.bindPortName, test.visitorSK, test.commonExtraConfig+"\n"+test.visitorExtraConfig, + ) + "\n" + if test.deployUser2Client { + clientUser2VisitorConf += config + } else { + clientVisitorConf += config + } + } + // run frps and frpc + f.RunProcesses([]string{serverConf}, []string{clientServerConf, clientVisitorConf, clientUser2VisitorConf}) + + for _, test := range tests { + timeout := time.Second + if t == "xtcp" { + if test.skipXTCP { + continue + } + timeout = 10 * time.Second + } + framework.NewRequestExpect(f). + RequestModify(func(r *request.Request) { + r.Timeout(timeout) + }). + Protocol(protocol). + PortName(test.bindPortName). + Explain(test.proxyName). + ExpectError(test.expectError). + Ensure() + } + }) + } + }) + + ginkgo.Describe("TCPMUX", func() { + ginkgo.It("Type tcpmux", func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + tcpmuxHTTPConnectPortName := port.GenName("TCPMUX") + serverConf += fmt.Sprintf(` + tcpmuxHTTPConnectPort = {{ .%s }} + `, tcpmuxHTTPConnectPortName) + + getProxyConf := func(proxyName string, extra string) string { + return fmt.Sprintf(` + [[proxies]] + name = "%s" + type = "tcpmux" + multiplexer = "httpconnect" + localPort = {{ .%s }} + customDomains = ["%s"] + `+extra, proxyName, port.GenName(proxyName), proxyName) + } + + tests := []struct { + proxyName string + extraConfig string + }{ + { + proxyName: "normal", + }, + { + proxyName: "with-encryption", + extraConfig: "transport.useEncryption = true", + }, + { + proxyName: "with-compression", + extraConfig: "transport.useCompression = true", + }, + { + proxyName: "with-encryption-and-compression", + extraConfig: ` + transport.useEncryption = true + transport.useCompression = true + `, + }, + } + + // build all client config + for _, test := range tests { + clientConf += getProxyConf(test.proxyName, test.extraConfig) + "\n" + + localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(f.AllocPort()), streamserver.WithRespContent([]byte(test.proxyName))) + f.RunServer(port.GenName(test.proxyName), localServer) + } + + // run frps and frpc + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + // Request without HTTP connect should get error + framework.NewRequestExpect(f). + PortName(tcpmuxHTTPConnectPortName). + ExpectError(true). + Explain("request without HTTP connect expect error"). + Ensure() + + proxyURL := fmt.Sprintf("http://127.0.0.1:%d", f.PortByName(tcpmuxHTTPConnectPortName)) + // Request with incorrect connect hostname + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.Addr("invalid").Proxy(proxyURL) + }).ExpectError(true).Explain("request without HTTP connect expect error").Ensure() + + // Request with correct connect hostname + for _, test := range tests { + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.Addr(test.proxyName).Proxy(proxyURL) + }).ExpectResp([]byte(test.proxyName)).Explain(test.proxyName).Ensure() + } + }) + }) +}) diff --git a/test/e2e/v1/basic/client.go b/test/e2e/v1/basic/client.go new file mode 100644 index 00000000000..25b99424101 --- /dev/null +++ b/test/e2e/v1/basic/client.go @@ -0,0 +1,136 @@ +package basic + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/onsi/ginkgo/v2" + + clientsdk "github.com/fatedier/frp/pkg/sdk/client" + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/pkg/request" +) + +var _ = ginkgo.Describe("[Feature: ClientManage]", func() { + f := framework.NewDefaultFramework() + + ginkgo.It("Update && Reload API", func() { + serverConf := consts.DefaultServerConfig + + adminPort := f.AllocPort() + + p1Port := f.AllocPort() + p2Port := f.AllocPort() + p3Port := f.AllocPort() + + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + webServer.port = %d + + [[proxies]] + name = "p1" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + + [[proxies]] + name = "p2" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + + [[proxies]] + name = "p3" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + `, adminPort, + framework.TCPEchoServerPort, p1Port, + framework.TCPEchoServerPort, p2Port, + framework.TCPEchoServerPort, p3Port) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + framework.NewRequestExpect(f).Port(p1Port).Ensure() + framework.NewRequestExpect(f).Port(p2Port).Ensure() + framework.NewRequestExpect(f).Port(p3Port).Ensure() + + client := clientsdk.New("127.0.0.1", adminPort) + conf, err := client.GetConfig() + framework.ExpectNoError(err) + + newP2Port := f.AllocPort() + // change p2 port and remove p3 proxy + newClientConf := strings.ReplaceAll(conf, strconv.Itoa(p2Port), strconv.Itoa(newP2Port)) + p3Index := strings.LastIndex(newClientConf, "[[proxies]]") + if p3Index >= 0 { + newClientConf = newClientConf[:p3Index] + } + + err = client.UpdateConfig(newClientConf) + framework.ExpectNoError(err) + + err = client.Reload() + framework.ExpectNoError(err) + time.Sleep(time.Second) + + framework.NewRequestExpect(f).Port(p1Port).Explain("p1 port").Ensure() + framework.NewRequestExpect(f).Port(p2Port).Explain("original p2 port").ExpectError(true).Ensure() + framework.NewRequestExpect(f).Port(newP2Port).Explain("new p2 port").Ensure() + framework.NewRequestExpect(f).Port(p3Port).Explain("p3 port").ExpectError(true).Ensure() + }) + + ginkgo.It("healthz", func() { + serverConf := consts.DefaultServerConfig + + dashboardPort := f.AllocPort() + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + webServer.addr = "0.0.0.0" + webServer.port = %d + webServer.user = "admin" + webServer.password = "admin" + `, dashboardPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().HTTPPath("/healthz") + }).Port(dashboardPort).ExpectResp([]byte("")).Ensure() + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().HTTPPath("/") + }).Port(dashboardPort). + Ensure(framework.ExpectResponseCode(401)) + }) + + ginkgo.It("stop", func() { + serverConf := consts.DefaultServerConfig + + adminPort := f.AllocPort() + testPort := f.AllocPort() + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + webServer.port = %d + + [[proxies]] + name = "test" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + `, adminPort, framework.TCPEchoServerPort, testPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + framework.NewRequestExpect(f).Port(testPort).Ensure() + + client := clientsdk.New("127.0.0.1", adminPort) + err := client.Stop() + framework.ExpectNoError(err) + + time.Sleep(3 * time.Second) + + // frpc stopped so the port is not listened, expect error + framework.NewRequestExpect(f).Port(testPort).ExpectError(true).Ensure() + }) +}) diff --git a/test/e2e/v1/basic/client_server.go b/test/e2e/v1/basic/client_server.go new file mode 100644 index 00000000000..082c0de591a --- /dev/null +++ b/test/e2e/v1/basic/client_server.go @@ -0,0 +1,325 @@ +package basic + +import ( + "fmt" + "strings" + "time" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/pkg/cert" + "github.com/fatedier/frp/test/e2e/pkg/port" +) + +type generalTestConfigures struct { + server string + client string + clientPrefix string + client2 string + client2Prefix string + testDelay time.Duration + expectError bool +} + +func renderBindPortConfig(protocol string) string { + if protocol == "kcp" { + return fmt.Sprintf(`kcpBindPort = {{ .%s }}`, consts.PortServerName) + } else if protocol == "quic" { + return fmt.Sprintf(`quicBindPort = {{ .%s }}`, consts.PortServerName) + } + return "" +} + +func runClientServerTest(f *framework.Framework, configures *generalTestConfigures) { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + if configures.clientPrefix != "" { + clientConf = configures.clientPrefix + } + + serverConf += fmt.Sprintf(` + %s + `, configures.server) + + tcpPortName := port.GenName("TCP") + udpPortName := port.GenName("UDP") + clientConf += fmt.Sprintf(` + %s + + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = {{ .%s }} + + [[proxies]] + name = "udp" + type = "udp" + localPort = {{ .%s }} + remotePort = {{ .%s }} + `, configures.client, + framework.TCPEchoServerPort, tcpPortName, + framework.UDPEchoServerPort, udpPortName, + ) + + clientConfs := []string{clientConf} + if configures.client2 != "" { + client2Conf := consts.DefaultClientConfig + if configures.client2Prefix != "" { + client2Conf = configures.client2Prefix + } + client2Conf += fmt.Sprintf(` + %s + `, configures.client2) + clientConfs = append(clientConfs, client2Conf) + } + + f.RunProcesses([]string{serverConf}, clientConfs) + + if configures.testDelay > 0 { + time.Sleep(configures.testDelay) + } + + framework.NewRequestExpect(f).PortName(tcpPortName).ExpectError(configures.expectError).Explain("tcp proxy").Ensure() + framework.NewRequestExpect(f).Protocol("udp"). + PortName(udpPortName).ExpectError(configures.expectError).Explain("udp proxy").Ensure() +} + +// defineClientServerTest test a normal tcp and udp proxy with specified TestConfigures. +func defineClientServerTest(desc string, f *framework.Framework, configures *generalTestConfigures) { + ginkgo.It(desc, func() { + runClientServerTest(f, configures) + }) +} + +var _ = ginkgo.Describe("[Feature: Client-Server]", func() { + f := framework.NewDefaultFramework() + + ginkgo.Describe("Protocol", func() { + supportProtocols := []string{"tcp", "kcp", "quic", "websocket"} + for _, protocol := range supportProtocols { + configures := &generalTestConfigures{ + server: fmt.Sprintf(` + %s + `, renderBindPortConfig(protocol)), + client: fmt.Sprintf(`transport.protocol = "%s"`, protocol), + } + defineClientServerTest(protocol, f, configures) + } + }) + + // wss is special, it needs to be tested separately. + // frps only supports ws, so there should be a proxy to terminate TLS before frps. + ginkgo.Describe("Protocol wss", func() { + wssPort := f.AllocPort() + configures := &generalTestConfigures{ + clientPrefix: fmt.Sprintf(` + serverAddr = "127.0.0.1" + serverPort = %d + loginFailExit = false + transport.protocol = "wss" + log.level = "trace" + `, wssPort), + // Due to the fact that frps cannot directly accept wss connections, we use the https2http plugin of another frpc to terminate TLS. + client2: fmt.Sprintf(` + [[proxies]] + name = "wss2ws" + type = "tcp" + remotePort = %d + [proxies.plugin] + type = "https2http" + localAddr = "127.0.0.1:{{ .%s }}" + `, wssPort, consts.PortServerName), + testDelay: 10 * time.Second, + } + + defineClientServerTest("wss", f, configures) + }) + + ginkgo.Describe("Authentication", func() { + defineClientServerTest("Token Correct", f, &generalTestConfigures{ + server: `auth.token = "123456"`, + client: `auth.token = "123456"`, + }) + + defineClientServerTest("Token Incorrect", f, &generalTestConfigures{ + server: `auth.token = "123456"`, + client: `auth.token = "invalid"`, + expectError: true, + }) + }) + + ginkgo.Describe("TLS", func() { + supportProtocols := []string{"tcp", "kcp", "quic", "websocket"} + for _, protocol := range supportProtocols { + tmp := protocol + // Since v0.50.0, the default value of tls_enable has been changed to true. + // Therefore, here it needs to be set as false to test the scenario of turning it off. + defineClientServerTest("Disable TLS over "+strings.ToUpper(tmp), f, &generalTestConfigures{ + server: fmt.Sprintf(` + %s + `, renderBindPortConfig(protocol)), + client: fmt.Sprintf(`transport.tls.enable = false + transport.protocol = "%s" + `, protocol), + }) + } + + defineClientServerTest("enable tls force, client with TLS", f, &generalTestConfigures{ + server: "transport.tls.force = true", + }) + defineClientServerTest("enable tls force, client without TLS", f, &generalTestConfigures{ + server: "transport.tls.force = true", + client: "transport.tls.enable = false", + expectError: true, + }) + }) + + ginkgo.Describe("TLS with custom certificate", func() { + supportProtocols := []string{"tcp", "kcp", "quic", "websocket"} + + var ( + caCrtPath string + serverCrtPath, serverKeyPath string + clientCrtPath, clientKeyPath string + ) + ginkgo.JustBeforeEach(func() { + generator := &cert.SelfSignedCertGenerator{} + artifacts, err := generator.Generate("127.0.0.1") + framework.ExpectNoError(err) + + caCrtPath = f.WriteTempFile("ca.crt", string(artifacts.CACert)) + serverCrtPath = f.WriteTempFile("server.crt", string(artifacts.Cert)) + serverKeyPath = f.WriteTempFile("server.key", string(artifacts.Key)) + generator.SetCA(artifacts.CACert, artifacts.CAKey) + _, err = generator.Generate("127.0.0.1") + framework.ExpectNoError(err) + clientCrtPath = f.WriteTempFile("client.crt", string(artifacts.Cert)) + clientKeyPath = f.WriteTempFile("client.key", string(artifacts.Key)) + }) + + for _, protocol := range supportProtocols { + tmp := protocol + + ginkgo.It("one-way authentication: "+tmp, func() { + runClientServerTest(f, &generalTestConfigures{ + server: fmt.Sprintf(` + %s + transport.tls.trustedCaFile = "%s" + `, renderBindPortConfig(tmp), caCrtPath), + client: fmt.Sprintf(` + transport.protocol = "%s" + transport.tls.certFile = "%s" + transport.tls.keyFile = "%s" + `, tmp, clientCrtPath, clientKeyPath), + }) + }) + + ginkgo.It("mutual authentication: "+tmp, func() { + runClientServerTest(f, &generalTestConfigures{ + server: fmt.Sprintf(` + %s + transport.tls.certFile = "%s" + transport.tls.keyFile = "%s" + transport.tls.trustedCaFile = "%s" + `, renderBindPortConfig(tmp), serverCrtPath, serverKeyPath, caCrtPath), + client: fmt.Sprintf(` + transport.protocol = "%s" + transport.tls.certFile = "%s" + transport.tls.keyFile = "%s" + transport.tls.trustedCaFile = "%s" + `, tmp, clientCrtPath, clientKeyPath, caCrtPath), + }) + }) + } + }) + + ginkgo.Describe("TLS with custom certificate and specified server name", func() { + var ( + caCrtPath string + serverCrtPath, serverKeyPath string + clientCrtPath, clientKeyPath string + ) + ginkgo.JustBeforeEach(func() { + generator := &cert.SelfSignedCertGenerator{} + artifacts, err := generator.Generate("example.com") + framework.ExpectNoError(err) + + caCrtPath = f.WriteTempFile("ca.crt", string(artifacts.CACert)) + serverCrtPath = f.WriteTempFile("server.crt", string(artifacts.Cert)) + serverKeyPath = f.WriteTempFile("server.key", string(artifacts.Key)) + generator.SetCA(artifacts.CACert, artifacts.CAKey) + _, err = generator.Generate("example.com") + framework.ExpectNoError(err) + clientCrtPath = f.WriteTempFile("client.crt", string(artifacts.Cert)) + clientKeyPath = f.WriteTempFile("client.key", string(artifacts.Key)) + }) + + ginkgo.It("mutual authentication", func() { + runClientServerTest(f, &generalTestConfigures{ + server: fmt.Sprintf(` + transport.tls.certFile = "%s" + transport.tls.keyFile = "%s" + transport.tls.trustedCaFile = "%s" + `, serverCrtPath, serverKeyPath, caCrtPath), + client: fmt.Sprintf(` + transport.tls.serverName = "example.com" + transport.tls.certFile = "%s" + transport.tls.keyFile = "%s" + transport.tls.trustedCaFile = "%s" + `, clientCrtPath, clientKeyPath, caCrtPath), + }) + }) + + ginkgo.It("mutual authentication with incorrect server name", func() { + runClientServerTest(f, &generalTestConfigures{ + server: fmt.Sprintf(` + transport.tls.certFile = "%s" + transport.tls.keyFile = "%s" + transport.tls.trustedCaFile = "%s" + `, serverCrtPath, serverKeyPath, caCrtPath), + client: fmt.Sprintf(` + transport.tls.serverName = "invalid.com" + transport.tls.certFile = "%s" + transport.tls.keyFile = "%s" + transport.tls.trustedCaFile = "%s" + `, clientCrtPath, clientKeyPath, caCrtPath), + expectError: true, + }) + }) + }) + + ginkgo.Describe("TLS with disable_custom_tls_first_byte set to false", func() { + supportProtocols := []string{"tcp", "kcp", "quic", "websocket"} + for _, protocol := range supportProtocols { + tmp := protocol + defineClientServerTest("TLS over "+strings.ToUpper(tmp), f, &generalTestConfigures{ + server: fmt.Sprintf(` + %s + `, renderBindPortConfig(protocol)), + client: fmt.Sprintf(` + transport.protocol = "%s" + transport.tls.disableCustomTLSFirstByte = false + `, protocol), + }) + } + }) + + ginkgo.Describe("IPv6 bind address", func() { + supportProtocols := []string{"tcp", "kcp", "quic", "websocket"} + for _, protocol := range supportProtocols { + tmp := protocol + defineClientServerTest("IPv6 bind address: "+strings.ToUpper(tmp), f, &generalTestConfigures{ + server: fmt.Sprintf(` + bindAddr = "::" + %s + `, renderBindPortConfig(protocol)), + client: fmt.Sprintf(` + transport.protocol = "%s" + `, protocol), + }) + } + }) +}) diff --git a/test/e2e/v1/basic/cmd.go b/test/e2e/v1/basic/cmd.go new file mode 100644 index 00000000000..08a9ea943b9 --- /dev/null +++ b/test/e2e/v1/basic/cmd.go @@ -0,0 +1,108 @@ +package basic + +import ( + "strconv" + "strings" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/pkg/request" +) + +const ( + ConfigValidStr = "syntax is ok" +) + +var _ = ginkgo.Describe("[Feature: Cmd]", func() { + f := framework.NewDefaultFramework() + + ginkgo.Describe("Verify", func() { + ginkgo.It("frps valid", func() { + path := f.GenerateConfigFile(` + bindAddr = "0.0.0.0" + bindPort = 7000 + `) + _, output, err := f.RunFrps("verify", "-c", path) + framework.ExpectNoError(err) + framework.ExpectTrue(strings.Contains(output, ConfigValidStr), "output: %s", output) + }) + ginkgo.It("frps invalid", func() { + path := f.GenerateConfigFile(` + bindAddr = "0.0.0.0" + bindPort = 70000 + `) + _, output, err := f.RunFrps("verify", "-c", path) + framework.ExpectNoError(err) + framework.ExpectTrue(!strings.Contains(output, ConfigValidStr), "output: %s", output) + }) + ginkgo.It("frpc valid", func() { + path := f.GenerateConfigFile(` + serverAddr = "0.0.0.0" + serverPort = 7000 + `) + _, output, err := f.RunFrpc("verify", "-c", path) + framework.ExpectNoError(err) + framework.ExpectTrue(strings.Contains(output, ConfigValidStr), "output: %s", output) + }) + ginkgo.It("frpc invalid", func() { + path := f.GenerateConfigFile(` + serverAddr = "0.0.0.0" + serverPort = 7000 + transport.protocol = "invalid" + `) + _, output, err := f.RunFrpc("verify", "-c", path) + framework.ExpectNoError(err) + framework.ExpectTrue(!strings.Contains(output, ConfigValidStr), "output: %s", output) + }) + }) + + ginkgo.Describe("Single proxy", func() { + ginkgo.It("TCP", func() { + serverPort := f.AllocPort() + _, _, err := f.RunFrps("-t", "123", "-p", strconv.Itoa(serverPort)) + framework.ExpectNoError(err) + + localPort := f.PortByName(framework.TCPEchoServerPort) + remotePort := f.AllocPort() + _, _, err = f.RunFrpc("tcp", "-s", "127.0.0.1", "-P", strconv.Itoa(serverPort), "-t", "123", "-u", "test", + "-l", strconv.Itoa(localPort), "-r", strconv.Itoa(remotePort), "-n", "tcp_test") + framework.ExpectNoError(err) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + }) + + ginkgo.It("UDP", func() { + serverPort := f.AllocPort() + _, _, err := f.RunFrps("-t", "123", "-p", strconv.Itoa(serverPort)) + framework.ExpectNoError(err) + + localPort := f.PortByName(framework.UDPEchoServerPort) + remotePort := f.AllocPort() + _, _, err = f.RunFrpc("udp", "-s", "127.0.0.1", "-P", strconv.Itoa(serverPort), "-t", "123", "-u", "test", + "-l", strconv.Itoa(localPort), "-r", strconv.Itoa(remotePort), "-n", "udp_test") + framework.ExpectNoError(err) + + framework.NewRequestExpect(f).Protocol("udp"). + Port(remotePort).Ensure() + }) + + ginkgo.It("HTTP", func() { + serverPort := f.AllocPort() + vhostHTTPPort := f.AllocPort() + _, _, err := f.RunFrps("-t", "123", "-p", strconv.Itoa(serverPort), "--vhost_http_port", strconv.Itoa(vhostHTTPPort)) + framework.ExpectNoError(err) + + _, _, err = f.RunFrpc("http", "-s", "127.0.0.1", "-P", strconv.Itoa(serverPort), "-t", "123", "-u", "test", + "-n", "udp_test", "-l", strconv.Itoa(f.PortByName(framework.HTTPSimpleServerPort)), + "--custom_domain", "test.example.com") + framework.ExpectNoError(err) + + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("test.example.com") + }). + Ensure() + }) + }) +}) diff --git a/test/e2e/v1/basic/config.go b/test/e2e/v1/basic/config.go new file mode 100644 index 00000000000..686b3c5c498 --- /dev/null +++ b/test/e2e/v1/basic/config.go @@ -0,0 +1,122 @@ +package basic + +import ( + "fmt" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/pkg/port" +) + +var _ = ginkgo.Describe("[Feature: Config]", func() { + f := framework.NewDefaultFramework() + + ginkgo.Describe("Template", func() { + ginkgo.It("render by env", func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + portName := port.GenName("TCP") + serverConf += fmt.Sprintf(` + auth.token = "{{ %s{{ .Envs.FRP_TOKEN }}%s }}" + `, "`", "`") + + clientConf += fmt.Sprintf(` + auth.token = "{{ %s{{ .Envs.FRP_TOKEN }}%s }}" + + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = {{ .%s }} + `, "`", "`", framework.TCPEchoServerPort, portName) + + f.SetEnvs([]string{"FRP_TOKEN=123"}) + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + framework.NewRequestExpect(f).PortName(portName).Ensure() + }) + }) + + ginkgo.Describe("Includes", func() { + ginkgo.It("split tcp proxies into different files", func() { + serverPort := f.AllocPort() + serverConfigPath := f.GenerateConfigFile(fmt.Sprintf(` + bindAddr = "0.0.0.0" + bindPort = %d + `, serverPort)) + + remotePort := f.AllocPort() + proxyConfigPath := f.GenerateConfigFile(fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + localPort = %d + remotePort = %d + `, f.PortByName(framework.TCPEchoServerPort), remotePort)) + + remotePort2 := f.AllocPort() + proxyConfigPath2 := f.GenerateConfigFile(fmt.Sprintf(` + [[proxies]] + name = "tcp2" + type = "tcp" + localPort = %d + remotePort = %d + `, f.PortByName(framework.TCPEchoServerPort), remotePort2)) + + clientConfigPath := f.GenerateConfigFile(fmt.Sprintf(` + serverPort = %d + includes = ["%s","%s"] + `, serverPort, proxyConfigPath, proxyConfigPath2)) + + _, _, err := f.RunFrps("-c", serverConfigPath) + framework.ExpectNoError(err) + + _, _, err = f.RunFrpc("-c", clientConfigPath) + framework.ExpectNoError(err) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + framework.NewRequestExpect(f).Port(remotePort2).Ensure() + }) + }) + + ginkgo.Describe("Support Formats", func() { + ginkgo.It("YAML", func() { + serverConf := fmt.Sprintf(` +bindPort: {{ .%s }} +log: + level: trace +`, port.GenName("Server")) + + remotePort := f.AllocPort() + clientConf := fmt.Sprintf(` +serverPort: {{ .%s }} +log: + level: trace + +proxies: +- name: tcp + type: tcp + localPort: {{ .%s }} + remotePort: %d +`, port.GenName("Server"), framework.TCPEchoServerPort, remotePort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + framework.NewRequestExpect(f).Port(remotePort).Ensure() + }) + + ginkgo.It("JSON", func() { + serverConf := fmt.Sprintf(`{"bindPort": {{ .%s }}, "log": {"level": "trace"}}`, port.GenName("Server")) + + remotePort := f.AllocPort() + clientConf := fmt.Sprintf(`{"serverPort": {{ .%s }}, "log": {"level": "trace"}, +"proxies": [{"name": "tcp", "type": "tcp", "localPort": {{ .%s }}, "remotePort": %d}]}`, + port.GenName("Server"), framework.TCPEchoServerPort, remotePort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + framework.NewRequestExpect(f).Port(remotePort).Ensure() + }) + }) +}) diff --git a/test/e2e/v1/basic/http.go b/test/e2e/v1/basic/http.go new file mode 100644 index 00000000000..e649dbfbe47 --- /dev/null +++ b/test/e2e/v1/basic/http.go @@ -0,0 +1,388 @@ +package basic + +import ( + "fmt" + "net/http" + "net/url" + "strconv" + + "github.com/gorilla/websocket" + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/mock/server/httpserver" + "github.com/fatedier/frp/test/e2e/pkg/request" +) + +var _ = ginkgo.Describe("[Feature: HTTP]", func() { + f := framework.NewDefaultFramework() + + getDefaultServerConf := func(vhostHTTPPort int) string { + conf := consts.DefaultServerConfig + ` + vhostHTTPPort = %d + ` + return fmt.Sprintf(conf, vhostHTTPPort) + } + newHTTPServer := func(port int, respContent string) *httpserver.Server { + return httpserver.New( + httpserver.WithBindPort(port), + httpserver.WithHandler(framework.SpecifiedHTTPBodyHandler([]byte(respContent))), + ) + } + + ginkgo.It("HTTP route by locations", func() { + vhostHTTPPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostHTTPPort) + + fooPort := f.AllocPort() + f.RunServer("", newHTTPServer(fooPort, "foo")) + + barPort := f.AllocPort() + f.RunServer("", newHTTPServer(barPort, "bar")) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "foo" + type = "http" + localPort = %d + customDomains = ["normal.example.com"] + locations = ["/","/foo"] + + [[proxies]] + name = "bar" + type = "http" + localPort = %d + customDomains = ["normal.example.com"] + locations = ["/bar"] + `, fooPort, barPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + tests := []struct { + path string + expectResp string + desc string + }{ + {path: "/foo", expectResp: "foo", desc: "foo path"}, + {path: "/bar", expectResp: "bar", desc: "bar path"}, + {path: "/other", expectResp: "foo", desc: "other path"}, + } + + for _, test := range tests { + framework.NewRequestExpect(f).Explain(test.desc).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com").HTTPPath(test.path) + }). + ExpectResp([]byte(test.expectResp)). + Ensure() + } + }) + + ginkgo.It("HTTP route by HTTP user", func() { + vhostHTTPPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostHTTPPort) + + fooPort := f.AllocPort() + f.RunServer("", newHTTPServer(fooPort, "foo")) + + barPort := f.AllocPort() + f.RunServer("", newHTTPServer(barPort, "bar")) + + otherPort := f.AllocPort() + f.RunServer("", newHTTPServer(otherPort, "other")) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "foo" + type = "http" + localPort = %d + customDomains = ["normal.example.com"] + routeByHTTPUser = "user1" + + [[proxies]] + name = "bar" + type = "http" + localPort = %d + customDomains = ["normal.example.com"] + routeByHTTPUser = "user2" + + [[proxies]] + name = "catchAll" + type = "http" + localPort = %d + customDomains = ["normal.example.com"] + `, fooPort, barPort, otherPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + // user1 + framework.NewRequestExpect(f).Explain("user1").Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com").HTTPAuth("user1", "") + }). + ExpectResp([]byte("foo")). + Ensure() + + // user2 + framework.NewRequestExpect(f).Explain("user2").Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com").HTTPAuth("user2", "") + }). + ExpectResp([]byte("bar")). + Ensure() + + // other user + framework.NewRequestExpect(f).Explain("other user").Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com").HTTPAuth("user3", "") + }). + ExpectResp([]byte("other")). + Ensure() + }) + + ginkgo.It("HTTP Basic Auth", func() { + vhostHTTPPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostHTTPPort) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "test" + type = "http" + localPort = {{ .%s }} + customDomains = ["normal.example.com"] + httpUser = "test" + httpPassword = "test" + `, framework.HTTPSimpleServerPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + // not set auth header + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com") + }). + Ensure(framework.ExpectResponseCode(401)) + + // set incorrect auth header + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com").HTTPAuth("test", "invalid") + }). + Ensure(framework.ExpectResponseCode(401)) + + // set correct auth header + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com").HTTPAuth("test", "test") + }). + Ensure() + }) + + ginkgo.It("Wildcard domain", func() { + vhostHTTPPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostHTTPPort) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "test" + type = "http" + localPort = {{ .%s }} + customDomains = ["*.example.com"] + `, framework.HTTPSimpleServerPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + // not match host + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("not-match.test.com") + }). + Ensure(framework.ExpectResponseCode(404)) + + // test.example.com match *.example.com + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("test.example.com") + }). + Ensure() + + // sub.test.example.com match *.example.com + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("sub.test.example.com") + }). + Ensure() + }) + + ginkgo.It("Subdomain", func() { + vhostHTTPPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostHTTPPort) + serverConf += ` + subdomainHost = "example.com" + ` + + fooPort := f.AllocPort() + f.RunServer("", newHTTPServer(fooPort, "foo")) + + barPort := f.AllocPort() + f.RunServer("", newHTTPServer(barPort, "bar")) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "foo" + type = "http" + localPort = %d + subdomain = "foo" + + [[proxies]] + name = "bar" + type = "http" + localPort = %d + subdomain = "bar" + `, fooPort, barPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + // foo + framework.NewRequestExpect(f).Explain("foo subdomain").Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("foo.example.com") + }). + ExpectResp([]byte("foo")). + Ensure() + + // bar + framework.NewRequestExpect(f).Explain("bar subdomain").Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("bar.example.com") + }). + ExpectResp([]byte("bar")). + Ensure() + }) + + ginkgo.It("Modify headers", func() { + vhostHTTPPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostHTTPPort) + + localPort := f.AllocPort() + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + _, _ = w.Write([]byte(req.Header.Get("X-From-Where"))) + })), + ) + f.RunServer("", localServer) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "test" + type = "http" + localPort = %d + customDomains = ["normal.example.com"] + requestHeaders.set.x-from-where = "frp" + `, localPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + // not set auth header + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com") + }). + ExpectResp([]byte("frp")). // local http server will write this X-From-Where header to response body + Ensure() + }) + + ginkgo.It("Host Header Rewrite", func() { + vhostHTTPPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostHTTPPort) + + localPort := f.AllocPort() + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + _, _ = w.Write([]byte(req.Host)) + })), + ) + f.RunServer("", localServer) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "test" + type = "http" + localPort = %d + customDomains = ["normal.example.com"] + hostHeaderRewrite = "rewrite.example.com" + `, localPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com") + }). + ExpectResp([]byte("rewrite.example.com")). // local http server will write host header to response body + Ensure() + }) + + ginkgo.It("Websocket protocol", func() { + vhostHTTPPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostHTTPPort) + + upgrader := websocket.Upgrader{} + + localPort := f.AllocPort() + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + c, err := upgrader.Upgrade(w, req, nil) + if err != nil { + return + } + defer c.Close() + for { + mt, message, err := c.ReadMessage() + if err != nil { + break + } + err = c.WriteMessage(mt, message) + if err != nil { + break + } + } + })), + ) + + f.RunServer("", localServer) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "test" + type = "http" + localPort = %d + customDomains = ["127.0.0.1"] + `, localPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + u := url.URL{Scheme: "ws", Host: "127.0.0.1:" + strconv.Itoa(vhostHTTPPort)} + c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) + framework.ExpectNoError(err) + + err = c.WriteMessage(websocket.TextMessage, []byte(consts.TestString)) + framework.ExpectNoError(err) + + _, msg, err := c.ReadMessage() + framework.ExpectNoError(err) + framework.ExpectEqualValues(consts.TestString, string(msg)) + }) +}) diff --git a/test/e2e/v1/basic/server.go b/test/e2e/v1/basic/server.go new file mode 100644 index 00000000000..eed1ddaa3f1 --- /dev/null +++ b/test/e2e/v1/basic/server.go @@ -0,0 +1,192 @@ +package basic + +import ( + "fmt" + "net" + "strconv" + + "github.com/onsi/ginkgo/v2" + + clientsdk "github.com/fatedier/frp/pkg/sdk/client" + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/pkg/port" + "github.com/fatedier/frp/test/e2e/pkg/request" +) + +var _ = ginkgo.Describe("[Feature: Server Manager]", func() { + f := framework.NewDefaultFramework() + + ginkgo.It("Ports Whitelist", func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + serverConf += ` + allowPorts = [ + { start = 20000, end = 25000 }, + { single = 25002 }, + { start = 30000, end = 50000 }, + ] + ` + + tcpPortName := port.GenName("TCP", port.WithRangePorts(20000, 25000)) + udpPortName := port.GenName("UDP", port.WithRangePorts(30000, 50000)) + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp-allowded-in-range" + type = "tcp" + localPort = {{ .%s }} + remotePort = {{ .%s }} + `, framework.TCPEchoServerPort, tcpPortName) + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp-port-not-allowed" + type = "tcp" + localPort = {{ .%s }} + remotePort = 25001 + `, framework.TCPEchoServerPort) + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp-port-unavailable" + type = "tcp" + localPort = {{ .%s }} + remotePort = {{ .%s }} + `, framework.TCPEchoServerPort, consts.PortServerName) + clientConf += fmt.Sprintf(` + [[proxies]] + name = "udp-allowed-in-range" + type = "udp" + localPort = {{ .%s }} + remotePort = {{ .%s }} + `, framework.UDPEchoServerPort, udpPortName) + clientConf += fmt.Sprintf(` + [[proxies]] + name = "udp-port-not-allowed" + type = "udp" + localPort = {{ .%s }} + remotePort = 25003 + `, framework.UDPEchoServerPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + // TCP + // Allowed in range + framework.NewRequestExpect(f).PortName(tcpPortName).Ensure() + + // Not Allowed + framework.NewRequestExpect(f).Port(25001).ExpectError(true).Ensure() + + // Unavailable, already bind by frps + framework.NewRequestExpect(f).PortName(consts.PortServerName).ExpectError(true).Ensure() + + // UDP + // Allowed in range + framework.NewRequestExpect(f).Protocol("udp").PortName(udpPortName).Ensure() + + // Not Allowed + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.UDP().Port(25003) + }).ExpectError(true).Ensure() + }) + + ginkgo.It("Alloc Random Port", func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + adminPort := f.AllocPort() + clientConf += fmt.Sprintf(` + webServer.port = %d + + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + + [[proxies]] + name = "udp" + type = "udp" + localPort = {{ .%s }} + `, adminPort, framework.TCPEchoServerPort, framework.UDPEchoServerPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + client := clientsdk.New("127.0.0.1", adminPort) + + // tcp random port + status, err := client.GetProxyStatus("tcp") + framework.ExpectNoError(err) + + _, portStr, err := net.SplitHostPort(status.RemoteAddr) + framework.ExpectNoError(err) + port, err := strconv.Atoi(portStr) + framework.ExpectNoError(err) + + framework.NewRequestExpect(f).Port(port).Ensure() + + // udp random port + status, err = client.GetProxyStatus("udp") + framework.ExpectNoError(err) + + _, portStr, err = net.SplitHostPort(status.RemoteAddr) + framework.ExpectNoError(err) + port, err = strconv.Atoi(portStr) + framework.ExpectNoError(err) + + framework.NewRequestExpect(f).Protocol("udp").Port(port).Ensure() + }) + + ginkgo.It("Port Reuse", func() { + serverConf := consts.DefaultServerConfig + // Use same port as PortServer + serverConf += fmt.Sprintf(` + vhostHTTPPort = {{ .%s }} + `, consts.PortServerName) + + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + [[proxies]] + name = "http" + type = "http" + localPort = {{ .%s }} + customDomains = ["example.com"] + `, framework.HTTPSimpleServerPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("example.com") + }).PortName(consts.PortServerName).Ensure() + }) + + ginkgo.It("healthz", func() { + serverConf := consts.DefaultServerConfig + dashboardPort := f.AllocPort() + + // Use same port as PortServer + serverConf += fmt.Sprintf(` + vhostHTTPPort = {{ .%s }} + webServer.addr = "0.0.0.0" + webServer.port = %d + webServer.user = "admin" + webServer.password = "admin" + `, consts.PortServerName, dashboardPort) + + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + [[proxies]] + name = "http" + type = "http" + localPort = {{ .%s }} + customDomains = ["example.com"] + `, framework.HTTPSimpleServerPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().HTTPPath("/healthz") + }).Port(dashboardPort).ExpectResp([]byte("")).Ensure() + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().HTTPPath("/") + }).Port(dashboardPort). + Ensure(framework.ExpectResponseCode(401)) + }) +}) diff --git a/test/e2e/v1/basic/tcpmux.go b/test/e2e/v1/basic/tcpmux.go new file mode 100644 index 00000000000..356a18be978 --- /dev/null +++ b/test/e2e/v1/basic/tcpmux.go @@ -0,0 +1,223 @@ +package basic + +import ( + "bufio" + "fmt" + "net" + "net/http" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/pkg/util/util" + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/mock/server/streamserver" + "github.com/fatedier/frp/test/e2e/pkg/request" + "github.com/fatedier/frp/test/e2e/pkg/rpc" +) + +var _ = ginkgo.Describe("[Feature: TCPMUX httpconnect]", func() { + f := framework.NewDefaultFramework() + + getDefaultServerConf := func(httpconnectPort int) string { + conf := consts.DefaultServerConfig + ` + tcpmuxHTTPConnectPort = %d + ` + return fmt.Sprintf(conf, httpconnectPort) + } + newServer := func(port int, respContent string) *streamserver.Server { + return streamserver.New( + streamserver.TCP, + streamserver.WithBindPort(port), + streamserver.WithRespContent([]byte(respContent)), + ) + } + + proxyURLWithAuth := func(username, password string, port int) string { + if username == "" { + return fmt.Sprintf("http://127.0.0.1:%d", port) + } + return fmt.Sprintf("http://%s:%s@127.0.0.1:%d", username, password, port) + } + + ginkgo.It("Route by HTTP user", func() { + vhostPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostPort) + + fooPort := f.AllocPort() + f.RunServer("", newServer(fooPort, "foo")) + + barPort := f.AllocPort() + f.RunServer("", newServer(barPort, "bar")) + + otherPort := f.AllocPort() + f.RunServer("", newServer(otherPort, "other")) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "foo" + type = "tcpmux" + multiplexer = "httpconnect" + localPort = %d + customDomains = ["normal.example.com"] + routeByHTTPUser = "user1" + + [[proxies]] + name = "bar" + type = "tcpmux" + multiplexer = "httpconnect" + localPort = %d + customDomains = ["normal.example.com"] + routeByHTTPUser = "user2" + + [[proxies]] + name = "catchAll" + type = "tcpmux" + multiplexer = "httpconnect" + localPort = %d + customDomains = ["normal.example.com"] + `, fooPort, barPort, otherPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + // user1 + framework.NewRequestExpect(f).Explain("user1"). + RequestModify(func(r *request.Request) { + r.Addr("normal.example.com").Proxy(proxyURLWithAuth("user1", "", vhostPort)) + }). + ExpectResp([]byte("foo")). + Ensure() + + // user2 + framework.NewRequestExpect(f).Explain("user2"). + RequestModify(func(r *request.Request) { + r.Addr("normal.example.com").Proxy(proxyURLWithAuth("user2", "", vhostPort)) + }). + ExpectResp([]byte("bar")). + Ensure() + + // other user + framework.NewRequestExpect(f).Explain("other user"). + RequestModify(func(r *request.Request) { + r.Addr("normal.example.com").Proxy(proxyURLWithAuth("user3", "", vhostPort)) + }). + ExpectResp([]byte("other")). + Ensure() + }) + + ginkgo.It("Proxy auth", func() { + vhostPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostPort) + + fooPort := f.AllocPort() + f.RunServer("", newServer(fooPort, "foo")) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "test" + type = "tcpmux" + multiplexer = "httpconnect" + localPort = %d + customDomains = ["normal.example.com"] + httpUser = "test" + httpPassword = "test" + `, fooPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + // not set auth header + framework.NewRequestExpect(f).Explain("no auth"). + RequestModify(func(r *request.Request) { + r.Addr("normal.example.com").Proxy(proxyURLWithAuth("", "", vhostPort)) + }). + ExpectError(true). + Ensure() + + // set incorrect auth header + framework.NewRequestExpect(f).Explain("incorrect auth"). + RequestModify(func(r *request.Request) { + r.Addr("normal.example.com").Proxy(proxyURLWithAuth("test", "invalid", vhostPort)) + }). + ExpectError(true). + Ensure() + + // set correct auth header + framework.NewRequestExpect(f).Explain("correct auth"). + RequestModify(func(r *request.Request) { + r.Addr("normal.example.com").Proxy(proxyURLWithAuth("test", "test", vhostPort)) + }). + ExpectResp([]byte("foo")). + Ensure() + }) + + ginkgo.It("TCPMux Passthrough", func() { + vhostPort := f.AllocPort() + serverConf := getDefaultServerConf(vhostPort) + serverConf += ` + tcpmuxPassthrough = true + ` + + var ( + respErr error + connectRequestHost string + ) + newServer := func(port int) *streamserver.Server { + return streamserver.New( + streamserver.TCP, + streamserver.WithBindPort(port), + streamserver.WithCustomHandler(func(conn net.Conn) { + defer conn.Close() + + // read HTTP CONNECT request + bufioReader := bufio.NewReader(conn) + req, err := http.ReadRequest(bufioReader) + if err != nil { + respErr = err + return + } + connectRequestHost = req.Host + + // return ok response + res := util.OkResponse() + if res.Body != nil { + defer res.Body.Close() + } + _ = res.Write(conn) + + buf, err := rpc.ReadBytes(conn) + if err != nil { + respErr = err + return + } + _, _ = rpc.WriteBytes(conn, buf) + }), + ) + } + + localPort := f.AllocPort() + f.RunServer("", newServer(localPort)) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "test" + type = "tcpmux" + multiplexer = "httpconnect" + localPort = %d + customDomains = ["normal.example.com"] + `, localPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + framework.NewRequestExpect(f). + RequestModify(func(r *request.Request) { + r.Addr("normal.example.com").Proxy(proxyURLWithAuth("", "", vhostPort)).Body([]byte("frp")) + }). + ExpectResp([]byte("frp")). + Ensure() + framework.ExpectNoError(respErr) + framework.ExpectEqualValues(connectRequestHost, "normal.example.com") + }) +}) diff --git a/test/e2e/v1/basic/xtcp.go b/test/e2e/v1/basic/xtcp.go new file mode 100644 index 00000000000..a5aaf67b81f --- /dev/null +++ b/test/e2e/v1/basic/xtcp.go @@ -0,0 +1,53 @@ +package basic + +import ( + "fmt" + "time" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/pkg/port" + "github.com/fatedier/frp/test/e2e/pkg/request" +) + +var _ = ginkgo.Describe("[Feature: XTCP]", func() { + f := framework.NewDefaultFramework() + + ginkgo.It("Fallback To STCP", func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + bindPortName := port.GenName("XTCP") + clientConf += fmt.Sprintf(` + [[proxies]] + name = "foo" + type = "stcp" + localPort = {{ .%s }} + + [[visitors]] + name = "foo-visitor" + type = "stcp" + serverName = "foo" + bindPort = -1 + + [[visitors]] + name = "bar-visitor" + type = "xtcp" + serverName = "bar" + bindPort = {{ .%s }} + keepTunnelOpen = true + fallbackTo = "foo-visitor" + fallbackTimeoutMs = 200 + `, framework.TCPEchoServerPort, bindPortName) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + framework.NewRequestExpect(f). + RequestModify(func(r *request.Request) { + r.Timeout(time.Second) + }). + PortName(bindPortName). + Ensure() + }) +}) diff --git a/test/e2e/v1/features/bandwidth_limit.go b/test/e2e/v1/features/bandwidth_limit.go new file mode 100644 index 00000000000..efcf38ed3e7 --- /dev/null +++ b/test/e2e/v1/features/bandwidth_limit.go @@ -0,0 +1,108 @@ +package features + +import ( + "fmt" + "strings" + "time" + + "github.com/onsi/ginkgo/v2" + + plugin "github.com/fatedier/frp/pkg/plugin/server" + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/mock/server/streamserver" + pluginpkg "github.com/fatedier/frp/test/e2e/pkg/plugin" + "github.com/fatedier/frp/test/e2e/pkg/request" +) + +var _ = ginkgo.Describe("[Feature: Bandwidth Limit]", func() { + f := framework.NewDefaultFramework() + + ginkgo.It("Proxy Bandwidth Limit by Client", func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + localPort := f.AllocPort() + localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort)) + f.RunServer("", localServer) + + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + localPort = %d + remotePort = %d + transport.bandwidthLimit = "10KB" + `, localPort, remotePort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + content := strings.Repeat("a", 50*1024) // 5KB + start := time.Now() + framework.NewRequestExpect(f).Port(remotePort).RequestModify(func(r *request.Request) { + r.Body([]byte(content)).Timeout(30 * time.Second) + }).ExpectResp([]byte(content)).Ensure() + + duration := time.Since(start) + framework.Logf("request duration: %s", duration.String()) + + framework.ExpectTrue(duration.Seconds() > 8, "100Kb with 10KB limit, want > 8 seconds, but got %s", duration.String()) + }) + + ginkgo.It("Proxy Bandwidth Limit by Server", func() { + // new test plugin server + newFunc := func() *plugin.Request { + var r plugin.Request + r.Content = &plugin.NewProxyContent{} + return &r + } + pluginPort := f.AllocPort() + handler := func(req *plugin.Request) *plugin.Response { + var ret plugin.Response + content := req.Content.(*plugin.NewProxyContent) + content.BandwidthLimit = "10KB" + content.BandwidthLimitMode = "server" + ret.Content = content + return &ret + } + pluginServer := pluginpkg.NewHTTPPluginServer(pluginPort, newFunc, handler, nil) + + f.RunServer("", pluginServer) + + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + [[httpPlugins]] + name = "test" + addr = "127.0.0.1:%d" + path = "/handler" + ops = ["NewProxy"] + `, pluginPort) + clientConf := consts.DefaultClientConfig + + localPort := f.AllocPort() + localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort)) + f.RunServer("", localServer) + + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + localPort = %d + remotePort = %d + `, localPort, remotePort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + content := strings.Repeat("a", 50*1024) // 5KB + start := time.Now() + framework.NewRequestExpect(f).Port(remotePort).RequestModify(func(r *request.Request) { + r.Body([]byte(content)).Timeout(30 * time.Second) + }).ExpectResp([]byte(content)).Ensure() + + duration := time.Since(start) + framework.Logf("request duration: %s", duration.String()) + + framework.ExpectTrue(duration.Seconds() > 8, "100Kb with 10KB limit, want > 8 seconds, but got %s", duration.String()) + }) +}) diff --git a/test/e2e/v1/features/chaos.go b/test/e2e/v1/features/chaos.go new file mode 100644 index 00000000000..f5f8b388c7f --- /dev/null +++ b/test/e2e/v1/features/chaos.go @@ -0,0 +1,64 @@ +package features + +import ( + "fmt" + "time" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" +) + +var _ = ginkgo.Describe("[Feature: Chaos]", func() { + f := framework.NewDefaultFramework() + + ginkgo.It("reconnect after frps restart", func() { + serverPort := f.AllocPort() + serverConfigPath := f.GenerateConfigFile(fmt.Sprintf(` + bindAddr = "0.0.0.0" + bindPort = %d + `, serverPort)) + + remotePort := f.AllocPort() + clientConfigPath := f.GenerateConfigFile(fmt.Sprintf(` + serverPort = %d + log.level = "trace" + + [[proxies]] + name = "tcp" + type = "tcp" + localPort = %d + remotePort = %d + `, serverPort, f.PortByName(framework.TCPEchoServerPort), remotePort)) + + // 1. start frps and frpc, expect request success + ps, _, err := f.RunFrps("-c", serverConfigPath) + framework.ExpectNoError(err) + + pc, _, err := f.RunFrpc("-c", clientConfigPath) + framework.ExpectNoError(err) + framework.NewRequestExpect(f).Port(remotePort).Ensure() + + // 2. stop frps, expect request failed + _ = ps.Stop() + time.Sleep(200 * time.Millisecond) + framework.NewRequestExpect(f).Port(remotePort).ExpectError(true).Ensure() + + // 3. restart frps, expect request success + _, _, err = f.RunFrps("-c", serverConfigPath) + framework.ExpectNoError(err) + time.Sleep(2 * time.Second) + framework.NewRequestExpect(f).Port(remotePort).Ensure() + + // 4. stop frpc, expect request failed + _ = pc.Stop() + time.Sleep(200 * time.Millisecond) + framework.NewRequestExpect(f).Port(remotePort).ExpectError(true).Ensure() + + // 5. restart frpc, expect request success + _, _, err = f.RunFrpc("-c", clientConfigPath) + framework.ExpectNoError(err) + time.Sleep(time.Second) + framework.NewRequestExpect(f).Port(remotePort).Ensure() + }) +}) diff --git a/test/e2e/v1/features/group.go b/test/e2e/v1/features/group.go new file mode 100644 index 00000000000..fe0c957ba8b --- /dev/null +++ b/test/e2e/v1/features/group.go @@ -0,0 +1,267 @@ +package features + +import ( + "fmt" + "strconv" + "sync" + "time" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/mock/server/httpserver" + "github.com/fatedier/frp/test/e2e/mock/server/streamserver" + "github.com/fatedier/frp/test/e2e/pkg/request" +) + +var _ = ginkgo.Describe("[Feature: Group]", func() { + f := framework.NewDefaultFramework() + + newHTTPServer := func(port int, respContent string) *httpserver.Server { + return httpserver.New( + httpserver.WithBindPort(port), + httpserver.WithHandler(framework.SpecifiedHTTPBodyHandler([]byte(respContent))), + ) + } + + validateFooBarResponse := func(resp *request.Response) bool { + if string(resp.Content) == "foo" || string(resp.Content) == "bar" { + return true + } + return false + } + + doFooBarHTTPRequest := func(vhostPort int, host string) []string { + results := []string{} + var wait sync.WaitGroup + var mu sync.Mutex + expectFn := func() { + framework.NewRequestExpect(f).Port(vhostPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost(host) + }). + Ensure(validateFooBarResponse, func(resp *request.Response) bool { + mu.Lock() + defer mu.Unlock() + results = append(results, string(resp.Content)) + return true + }) + } + for i := 0; i < 10; i++ { + wait.Add(1) + go func() { + defer wait.Done() + expectFn() + }() + } + + wait.Wait() + return results + } + + ginkgo.Describe("Load Balancing", func() { + ginkgo.It("TCP", func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + fooPort := f.AllocPort() + fooServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(fooPort), streamserver.WithRespContent([]byte("foo"))) + f.RunServer("", fooServer) + + barPort := f.AllocPort() + barServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(barPort), streamserver.WithRespContent([]byte("bar"))) + f.RunServer("", barServer) + + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + [[proxies]] + name = "foo" + type = "tcp" + localPort = %d + remotePort = %d + loadBalancer.group = "test" + loadBalancer.groupKey = "123" + + [[proxies]] + name = "bar" + type = "tcp" + localPort = %d + remotePort = %d + loadBalancer.group = "test" + loadBalancer.groupKey = "123" + `, fooPort, remotePort, barPort, remotePort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + fooCount := 0 + barCount := 0 + for i := 0; i < 10; i++ { + framework.NewRequestExpect(f).Explain("times " + strconv.Itoa(i)).Port(remotePort).Ensure(func(resp *request.Response) bool { + switch string(resp.Content) { + case "foo": + fooCount++ + case "bar": + barCount++ + default: + return false + } + return true + }) + } + + framework.ExpectTrue(fooCount > 1 && barCount > 1, "fooCount: %d, barCount: %d", fooCount, barCount) + }) + }) + + ginkgo.Describe("Health Check", func() { + ginkgo.It("TCP", func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + fooPort := f.AllocPort() + fooServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(fooPort), streamserver.WithRespContent([]byte("foo"))) + f.RunServer("", fooServer) + + barPort := f.AllocPort() + barServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(barPort), streamserver.WithRespContent([]byte("bar"))) + f.RunServer("", barServer) + + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + [[proxies]] + name = "foo" + type = "tcp" + localPort = %d + remotePort = %d + loadBalancer.group = "test" + loadBalancer.groupKey = "123" + healthCheck.type = "tcp" + healthCheck.intervalSeconds = 1 + + [[proxies]] + name = "bar" + type = "tcp" + localPort = %d + remotePort = %d + loadBalancer.group = "test" + loadBalancer.groupKey = "123" + healthCheck.type = "tcp" + healthCheck.intervalSeconds = 1 + `, fooPort, remotePort, barPort, remotePort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + // check foo and bar is ok + results := []string{} + for i := 0; i < 10; i++ { + framework.NewRequestExpect(f).Port(remotePort).Ensure(validateFooBarResponse, func(resp *request.Response) bool { + results = append(results, string(resp.Content)) + return true + }) + } + framework.ExpectContainElements(results, []string{"foo", "bar"}) + + // close bar server, check foo is ok + barServer.Close() + time.Sleep(2 * time.Second) + for i := 0; i < 10; i++ { + framework.NewRequestExpect(f).Port(remotePort).ExpectResp([]byte("foo")).Ensure() + } + + // resume bar server, check foo and bar is ok + f.RunServer("", barServer) + time.Sleep(2 * time.Second) + results = []string{} + for i := 0; i < 10; i++ { + framework.NewRequestExpect(f).Port(remotePort).Ensure(validateFooBarResponse, func(resp *request.Response) bool { + results = append(results, string(resp.Content)) + return true + }) + } + framework.ExpectContainElements(results, []string{"foo", "bar"}) + }) + + ginkgo.It("HTTP", func() { + vhostPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + vhostHTTPPort = %d + `, vhostPort) + clientConf := consts.DefaultClientConfig + + fooPort := f.AllocPort() + fooServer := newHTTPServer(fooPort, "foo") + f.RunServer("", fooServer) + + barPort := f.AllocPort() + barServer := newHTTPServer(barPort, "bar") + f.RunServer("", barServer) + + clientConf += fmt.Sprintf(` + [[proxies]] + name = "foo" + type = "http" + localPort = %d + customDomains = ["example.com"] + loadBalancer.group = "test" + loadBalancer.groupKey = "123" + healthCheck.type = "http" + healthCheck.intervalSeconds = 1 + healthCheck.path = "/healthz" + + [[proxies]] + name = "bar" + type = "http" + localPort = %d + customDomains = ["example.com"] + loadBalancer.group = "test" + loadBalancer.groupKey = "123" + healthCheck.type = "http" + healthCheck.intervalSeconds = 1 + healthCheck.path = "/healthz" + `, fooPort, barPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + // send first HTTP request + var contents []string + framework.NewRequestExpect(f).Port(vhostPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("example.com") + }). + Ensure(func(resp *request.Response) bool { + contents = append(contents, string(resp.Content)) + return true + }) + + // send second HTTP request, should be forwarded to another service + framework.NewRequestExpect(f).Port(vhostPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("example.com") + }). + Ensure(func(resp *request.Response) bool { + contents = append(contents, string(resp.Content)) + return true + }) + + framework.ExpectContainElements(contents, []string{"foo", "bar"}) + + // check foo and bar is ok + results := doFooBarHTTPRequest(vhostPort, "example.com") + framework.ExpectContainElements(results, []string{"foo", "bar"}) + + // close bar server, check foo is ok + barServer.Close() + time.Sleep(2 * time.Second) + results = doFooBarHTTPRequest(vhostPort, "example.com") + framework.ExpectContainElements(results, []string{"foo"}) + framework.ExpectNotContainElements(results, []string{"bar"}) + + // resume bar server, check foo and bar is ok + f.RunServer("", barServer) + time.Sleep(2 * time.Second) + results = doFooBarHTTPRequest(vhostPort, "example.com") + framework.ExpectContainElements(results, []string{"foo", "bar"}) + }) + }) +}) diff --git a/test/e2e/v1/features/heartbeat.go b/test/e2e/v1/features/heartbeat.go new file mode 100644 index 00000000000..4f5409fe7ce --- /dev/null +++ b/test/e2e/v1/features/heartbeat.go @@ -0,0 +1,47 @@ +package features + +import ( + "fmt" + "time" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/test/e2e/framework" +) + +var _ = ginkgo.Describe("[Feature: Heartbeat]", func() { + f := framework.NewDefaultFramework() + + ginkgo.It("disable application layer heartbeat", func() { + serverPort := f.AllocPort() + serverConf := fmt.Sprintf(` + bindAddr = "0.0.0.0" + bindPort = %d + transport.heartbeatTimeout = -1 + transport.tcpMuxKeepaliveInterval = 2 + `, serverPort) + + remotePort := f.AllocPort() + clientConf := fmt.Sprintf(` + serverPort = %d + log.level = "trace" + transport.heartbeatInterval = -1 + transport.heartbeatTimeout = -1 + transport.tcpMuxKeepaliveInterval = 2 + + [[proxies]] + name = "tcp" + type = "tcp" + localPort = %d + remotePort = %d + `, serverPort, f.PortByName(framework.TCPEchoServerPort), remotePort) + + // run frps and frpc + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + framework.NewRequestExpect(f).Protocol("tcp").Port(remotePort).Ensure() + + time.Sleep(5 * time.Second) + framework.NewRequestExpect(f).Protocol("tcp").Port(remotePort).Ensure() + }) +}) diff --git a/test/e2e/v1/features/monitor.go b/test/e2e/v1/features/monitor.go new file mode 100644 index 00000000000..5d4b8f9f908 --- /dev/null +++ b/test/e2e/v1/features/monitor.go @@ -0,0 +1,55 @@ +package features + +import ( + "fmt" + "strings" + "time" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/pkg/util/log" + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/pkg/request" +) + +var _ = ginkgo.Describe("[Feature: Monitor]", func() { + f := framework.NewDefaultFramework() + + ginkgo.It("Prometheus metrics", func() { + dashboardPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + enablePrometheus = true + webServer.addr = "0.0.0.0" + webServer.port = %d + `, dashboardPort) + + clientConf := consts.DefaultClientConfig + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + `, framework.TCPEchoServerPort, remotePort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + time.Sleep(500 * time.Millisecond) + + framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { + r.HTTP().Port(dashboardPort).HTTPPath("/metrics") + }).Ensure(func(resp *request.Response) bool { + log.Trace("prometheus metrics response: \n%s", resp.Content) + if resp.Code != 200 { + return false + } + if !strings.Contains(string(resp.Content), "traffic_in") { + return false + } + return true + }) + }) +}) diff --git a/test/e2e/v1/features/real_ip.go b/test/e2e/v1/features/real_ip.go new file mode 100644 index 00000000000..85338c62072 --- /dev/null +++ b/test/e2e/v1/features/real_ip.go @@ -0,0 +1,154 @@ +package features + +import ( + "bufio" + "fmt" + "net" + "net/http" + + "github.com/onsi/ginkgo/v2" + pp "github.com/pires/go-proxyproto" + + "github.com/fatedier/frp/pkg/util/log" + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/mock/server/httpserver" + "github.com/fatedier/frp/test/e2e/mock/server/streamserver" + "github.com/fatedier/frp/test/e2e/pkg/request" + "github.com/fatedier/frp/test/e2e/pkg/rpc" +) + +var _ = ginkgo.Describe("[Feature: Real IP]", func() { + f := framework.NewDefaultFramework() + + ginkgo.It("HTTP X-Forwarded-For", func() { + vhostHTTPPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + vhostHTTPPort = %d + `, vhostHTTPPort) + + localPort := f.AllocPort() + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + _, _ = w.Write([]byte(req.Header.Get("X-Forwarded-For"))) + })), + ) + f.RunServer("", localServer) + + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "test" + type = "http" + localPort = %d + customDomains = ["normal.example.com"] + `, localPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + framework.NewRequestExpect(f).Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com") + }). + ExpectResp([]byte("127.0.0.1")). + Ensure() + }) + + ginkgo.Describe("Proxy Protocol", func() { + ginkgo.It("TCP", func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + localPort := f.AllocPort() + localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort), + streamserver.WithCustomHandler(func(c net.Conn) { + defer c.Close() + rd := bufio.NewReader(c) + ppHeader, err := pp.Read(rd) + if err != nil { + log.Error("read proxy protocol error: %v", err) + return + } + + for { + if _, err := rpc.ReadBytes(rd); err != nil { + return + } + + buf := []byte(ppHeader.SourceAddr.String()) + _, _ = rpc.WriteBytes(c, buf) + } + })) + f.RunServer("", localServer) + + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + localPort = %d + remotePort = %d + transport.proxyProtocolVersion = "v2" + `, localPort, remotePort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + framework.NewRequestExpect(f).Port(remotePort).Ensure(func(resp *request.Response) bool { + log.Trace("ProxyProtocol get SourceAddr: %s", string(resp.Content)) + addr, err := net.ResolveTCPAddr("tcp", string(resp.Content)) + if err != nil { + return false + } + if addr.IP.String() != "127.0.0.1" { + return false + } + return true + }) + }) + + ginkgo.It("HTTP", func() { + vhostHTTPPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + vhostHTTPPort = %d + `, vhostHTTPPort) + + clientConf := consts.DefaultClientConfig + + localPort := f.AllocPort() + var srcAddrRecord string + localServer := streamserver.New(streamserver.TCP, streamserver.WithBindPort(localPort), + streamserver.WithCustomHandler(func(c net.Conn) { + defer c.Close() + rd := bufio.NewReader(c) + ppHeader, err := pp.Read(rd) + if err != nil { + log.Error("read proxy protocol error: %v", err) + return + } + srcAddrRecord = ppHeader.SourceAddr.String() + })) + f.RunServer("", localServer) + + clientConf += fmt.Sprintf(` + [[proxies]] + name = "test" + type = "http" + localPort = %d + customDomains = ["normal.example.com"] + transport.proxyProtocolVersion = "v2" + `, localPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + framework.NewRequestExpect(f).Port(vhostHTTPPort).RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("normal.example.com") + }).Ensure(framework.ExpectResponseCode(404)) + + log.Trace("ProxyProtocol get SourceAddr: %s", srcAddrRecord) + addr, err := net.ResolveTCPAddr("tcp", srcAddrRecord) + framework.ExpectNoError(err, srcAddrRecord) + framework.ExpectEqualValues("127.0.0.1", addr.IP.String()) + }) + }) +}) diff --git a/test/e2e/v1/plugin/client.go b/test/e2e/v1/plugin/client.go new file mode 100644 index 00000000000..f544570ae7e --- /dev/null +++ b/test/e2e/v1/plugin/client.go @@ -0,0 +1,331 @@ +package plugin + +import ( + "crypto/tls" + "fmt" + "strconv" + + "github.com/onsi/ginkgo/v2" + + "github.com/fatedier/frp/pkg/transport" + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + "github.com/fatedier/frp/test/e2e/mock/server/httpserver" + "github.com/fatedier/frp/test/e2e/pkg/cert" + "github.com/fatedier/frp/test/e2e/pkg/port" + "github.com/fatedier/frp/test/e2e/pkg/request" +) + +var _ = ginkgo.Describe("[Feature: Client-Plugins]", func() { + f := framework.NewDefaultFramework() + + ginkgo.Describe("UnixDomainSocket", func() { + ginkgo.It("Expose a unix domain socket echo server", func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + getProxyConf := func(proxyName string, portName string, extra string) string { + return fmt.Sprintf(` + [[proxies]] + name = "%s" + type = "tcp" + remotePort = {{ .%s }} + [proxies.plugin] + type = "unix_domain_socket" + unixPath = "{{ .%s }}" + `+extra, proxyName, portName, framework.UDSEchoServerAddr) + } + + tests := []struct { + proxyName string + portName string + extraConfig string + }{ + { + proxyName: "normal", + portName: port.GenName("Normal"), + }, + { + proxyName: "with-encryption", + portName: port.GenName("WithEncryption"), + extraConfig: "transport.useEncryption = true", + }, + { + proxyName: "with-compression", + portName: port.GenName("WithCompression"), + extraConfig: "transport.useCompression = true", + }, + { + proxyName: "with-encryption-and-compression", + portName: port.GenName("WithEncryptionAndCompression"), + extraConfig: ` + transport.useEncryption = true + transport.useCompression = true + `, + }, + } + + // build all client config + for _, test := range tests { + clientConf += getProxyConf(test.proxyName, test.portName, test.extraConfig) + "\n" + } + // run frps and frpc + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + for _, test := range tests { + framework.NewRequestExpect(f).Port(f.PortByName(test.portName)).Ensure() + } + }) + }) + + ginkgo.It("http_proxy", func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + remotePort = %d + [proxies.plugin] + type = "http_proxy" + httpUser = "abc" + httpPassword = "123" + `, remotePort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + // http proxy, no auth info + framework.NewRequestExpect(f).PortName(framework.HTTPSimpleServerPort).RequestModify(func(r *request.Request) { + r.HTTP().Proxy("http://127.0.0.1:" + strconv.Itoa(remotePort)) + }).Ensure(framework.ExpectResponseCode(407)) + + // http proxy, correct auth + framework.NewRequestExpect(f).PortName(framework.HTTPSimpleServerPort).RequestModify(func(r *request.Request) { + r.HTTP().Proxy("http://abc:123@127.0.0.1:" + strconv.Itoa(remotePort)) + }).Ensure() + + // connect TCP server by CONNECT method + framework.NewRequestExpect(f).PortName(framework.TCPEchoServerPort).RequestModify(func(r *request.Request) { + r.TCP().Proxy("http://abc:123@127.0.0.1:" + strconv.Itoa(remotePort)) + }) + }) + + ginkgo.It("socks5 proxy", func() { + serverConf := consts.DefaultServerConfig + clientConf := consts.DefaultClientConfig + + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + remotePort = %d + [proxies.plugin] + type = "socks5" + username = "abc" + password = "123" + `, remotePort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + // http proxy, no auth info + framework.NewRequestExpect(f).PortName(framework.TCPEchoServerPort).RequestModify(func(r *request.Request) { + r.TCP().Proxy("socks5://127.0.0.1:" + strconv.Itoa(remotePort)) + }).ExpectError(true).Ensure() + + // http proxy, correct auth + framework.NewRequestExpect(f).PortName(framework.TCPEchoServerPort).RequestModify(func(r *request.Request) { + r.TCP().Proxy("socks5://abc:123@127.0.0.1:" + strconv.Itoa(remotePort)) + }).Ensure() + }) + + ginkgo.It("static_file", func() { + vhostPort := f.AllocPort() + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + vhostHTTPPort = %d + `, vhostPort) + clientConf := consts.DefaultClientConfig + + remotePort := f.AllocPort() + f.WriteTempFile("test_static_file", "foo") + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + remotePort = %d + [proxies.plugin] + type = "static_file" + localPath = "%s" + + [[proxies]] + name = "http" + type = "http" + customDomains = ["example.com"] + [proxies.plugin] + type = "static_file" + localPath = "%s" + + [[proxies]] + name = "http-with-auth" + type = "http" + customDomains = ["other.example.com"] + [proxies.plugin] + type = "static_file" + localPath = "%s" + httpUser = "abc" + httpPassword = "123" + `, remotePort, f.TempDirectory, f.TempDirectory, f.TempDirectory) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + // from tcp proxy + framework.NewRequestExpect(f).Request( + framework.NewHTTPRequest().HTTPPath("/test_static_file").Port(remotePort), + ).ExpectResp([]byte("foo")).Ensure() + + // from http proxy without auth + framework.NewRequestExpect(f).Request( + framework.NewHTTPRequest().HTTPHost("example.com").HTTPPath("/test_static_file").Port(vhostPort), + ).ExpectResp([]byte("foo")).Ensure() + + // from http proxy with auth + framework.NewRequestExpect(f).Request( + framework.NewHTTPRequest().HTTPHost("other.example.com").HTTPPath("/test_static_file").Port(vhostPort).HTTPAuth("abc", "123"), + ).ExpectResp([]byte("foo")).Ensure() + }) + + ginkgo.It("http2https", func() { + serverConf := consts.DefaultServerConfig + vhostHTTPPort := f.AllocPort() + serverConf += fmt.Sprintf(` + vhostHTTPPort = %d + `, vhostHTTPPort) + + localPort := f.AllocPort() + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + [[proxies]] + name = "http2https" + type = "http" + customDomains = ["example.com"] + [proxies.plugin] + type = "http2https" + localAddr = "127.0.0.1:%d" + `, localPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + tlsConfig, err := transport.NewServerTLSConfig("", "", "") + framework.ExpectNoError(err) + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithTLSConfig(tlsConfig), + httpserver.WithResponse([]byte("test")), + ) + f.RunServer("", localServer) + + framework.NewRequestExpect(f). + Port(vhostHTTPPort). + RequestModify(func(r *request.Request) { + r.HTTP().HTTPHost("example.com") + }). + ExpectResp([]byte("test")). + Ensure() + }) + + ginkgo.It("https2http", func() { + generator := &cert.SelfSignedCertGenerator{} + artifacts, err := generator.Generate("example.com") + framework.ExpectNoError(err) + crtPath := f.WriteTempFile("server.crt", string(artifacts.Cert)) + keyPath := f.WriteTempFile("server.key", string(artifacts.Key)) + + serverConf := consts.DefaultServerConfig + vhostHTTPSPort := f.AllocPort() + serverConf += fmt.Sprintf(` + vhostHTTPSPort = %d + `, vhostHTTPSPort) + + localPort := f.AllocPort() + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + [[proxies]] + name = "https2http" + type = "https" + customDomains = ["example.com"] + [proxies.plugin] + type = "https2http" + localAddr = "127.0.0.1:%d" + crtPath = "%s" + keyPath = "%s" + `, localPort, crtPath, keyPath) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithResponse([]byte("test")), + ) + f.RunServer("", localServer) + + framework.NewRequestExpect(f). + Port(vhostHTTPSPort). + RequestModify(func(r *request.Request) { + r.HTTPS().HTTPHost("example.com").TLSConfig(&tls.Config{ + ServerName: "example.com", + InsecureSkipVerify: true, + }) + }). + ExpectResp([]byte("test")). + Ensure() + }) + + ginkgo.It("https2https", func() { + generator := &cert.SelfSignedCertGenerator{} + artifacts, err := generator.Generate("example.com") + framework.ExpectNoError(err) + crtPath := f.WriteTempFile("server.crt", string(artifacts.Cert)) + keyPath := f.WriteTempFile("server.key", string(artifacts.Key)) + + serverConf := consts.DefaultServerConfig + vhostHTTPSPort := f.AllocPort() + serverConf += fmt.Sprintf(` + vhostHTTPSPort = %d + `, vhostHTTPSPort) + + localPort := f.AllocPort() + clientConf := consts.DefaultClientConfig + fmt.Sprintf(` + [[proxies]] + name = "https2https" + type = "https" + customDomains = ["example.com"] + [proxies.plugin] + type = "https2https" + localAddr = "127.0.0.1:%d" + crtPath = "%s" + keyPath = "%s" + `, localPort, crtPath, keyPath) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + tlsConfig, err := transport.NewServerTLSConfig("", "", "") + framework.ExpectNoError(err) + localServer := httpserver.New( + httpserver.WithBindPort(localPort), + httpserver.WithResponse([]byte("test")), + httpserver.WithTLSConfig(tlsConfig), + ) + f.RunServer("", localServer) + + framework.NewRequestExpect(f). + Port(vhostHTTPSPort). + RequestModify(func(r *request.Request) { + r.HTTPS().HTTPHost("example.com").TLSConfig(&tls.Config{ + ServerName: "example.com", + InsecureSkipVerify: true, + }) + }). + ExpectResp([]byte("test")). + Ensure() + }) +}) diff --git a/test/e2e/v1/plugin/server.go b/test/e2e/v1/plugin/server.go new file mode 100644 index 00000000000..66456f57f95 --- /dev/null +++ b/test/e2e/v1/plugin/server.go @@ -0,0 +1,416 @@ +package plugin + +import ( + "fmt" + "time" + + "github.com/onsi/ginkgo/v2" + + plugin "github.com/fatedier/frp/pkg/plugin/server" + "github.com/fatedier/frp/pkg/transport" + "github.com/fatedier/frp/test/e2e/framework" + "github.com/fatedier/frp/test/e2e/framework/consts" + pluginpkg "github.com/fatedier/frp/test/e2e/pkg/plugin" +) + +var _ = ginkgo.Describe("[Feature: Server-Plugins]", func() { + f := framework.NewDefaultFramework() + + ginkgo.Describe("Login", func() { + newFunc := func() *plugin.Request { + var r plugin.Request + r.Content = &plugin.LoginContent{} + return &r + } + + ginkgo.It("Auth for custom meta token", func() { + localPort := f.AllocPort() + + clientAddressGot := false + handler := func(req *plugin.Request) *plugin.Response { + var ret plugin.Response + content := req.Content.(*plugin.LoginContent) + if content.ClientAddress != "" { + clientAddressGot = true + } + if content.Metas["token"] == "123" { + ret.Unchange = true + } else { + ret.Reject = true + ret.RejectReason = "invalid token" + } + return &ret + } + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) + + f.RunServer("", pluginServer) + + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + [[httpPlugins]] + name = "user-manager" + addr = "127.0.0.1:%d" + path = "/handler" + ops = ["Login"] + `, localPort) + clientConf := consts.DefaultClientConfig + + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + metadatas.token = "123" + + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + `, framework.TCPEchoServerPort, remotePort) + + remotePort2 := f.AllocPort() + invalidTokenClientConf := consts.DefaultClientConfig + fmt.Sprintf(` + [[proxies]] + name = "tcp2" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + `, framework.TCPEchoServerPort, remotePort2) + + f.RunProcesses([]string{serverConf}, []string{clientConf, invalidTokenClientConf}) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + framework.NewRequestExpect(f).Port(remotePort2).ExpectError(true).Ensure() + + framework.ExpectTrue(clientAddressGot) + }) + }) + + ginkgo.Describe("NewProxy", func() { + newFunc := func() *plugin.Request { + var r plugin.Request + r.Content = &plugin.NewProxyContent{} + return &r + } + + ginkgo.It("Validate Info", func() { + localPort := f.AllocPort() + handler := func(req *plugin.Request) *plugin.Response { + var ret plugin.Response + content := req.Content.(*plugin.NewProxyContent) + if content.ProxyName == "tcp" { + ret.Unchange = true + } else { + ret.Reject = true + } + return &ret + } + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) + + f.RunServer("", pluginServer) + + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + [[httpPlugins]] + name = "test" + addr = "127.0.0.1:%d" + path = "/handler" + ops = ["NewProxy"] + `, localPort) + clientConf := consts.DefaultClientConfig + + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + `, framework.TCPEchoServerPort, remotePort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + }) + + ginkgo.It("Mofify RemotePort", func() { + localPort := f.AllocPort() + remotePort := f.AllocPort() + handler := func(req *plugin.Request) *plugin.Response { + var ret plugin.Response + content := req.Content.(*plugin.NewProxyContent) + content.RemotePort = remotePort + ret.Content = content + return &ret + } + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) + + f.RunServer("", pluginServer) + + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + [[httpPlugins]] + name = "test" + addr = "127.0.0.1:%d" + path = "/handler" + ops = ["NewProxy"] + `, localPort) + clientConf := consts.DefaultClientConfig + + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = 0 + `, framework.TCPEchoServerPort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + }) + }) + + ginkgo.Describe("CloseProxy", func() { + newFunc := func() *plugin.Request { + var r plugin.Request + r.Content = &plugin.CloseProxyContent{} + return &r + } + + ginkgo.It("Validate Info", func() { + localPort := f.AllocPort() + var recordProxyName string + handler := func(req *plugin.Request) *plugin.Response { + var ret plugin.Response + content := req.Content.(*plugin.CloseProxyContent) + recordProxyName = content.ProxyName + return &ret + } + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) + + f.RunServer("", pluginServer) + + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + [[httpPlugins]] + name = "test" + addr = "127.0.0.1:%d" + path = "/handler" + ops = ["CloseProxy"] + `, localPort) + clientConf := consts.DefaultClientConfig + + remotePort := f.AllocPort() + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + `, framework.TCPEchoServerPort, remotePort) + + _, clients := f.RunProcesses([]string{serverConf}, []string{clientConf}) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + + for _, c := range clients { + _ = c.Stop() + } + + time.Sleep(1 * time.Second) + + framework.ExpectEqual(recordProxyName, "tcp") + }) + }) + + ginkgo.Describe("Ping", func() { + newFunc := func() *plugin.Request { + var r plugin.Request + r.Content = &plugin.PingContent{} + return &r + } + + ginkgo.It("Validate Info", func() { + localPort := f.AllocPort() + + var record string + handler := func(req *plugin.Request) *plugin.Response { + var ret plugin.Response + content := req.Content.(*plugin.PingContent) + record = content.Ping.PrivilegeKey + ret.Unchange = true + return &ret + } + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) + + f.RunServer("", pluginServer) + + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + [[httpPlugins]] + name = "test" + addr = "127.0.0.1:%d" + path = "/handler" + ops = ["Ping"] + `, localPort) + + remotePort := f.AllocPort() + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + transport.heartbeatInterval = 1 + auth.additionalScopes = ["HeartBeats"] + + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + `, framework.TCPEchoServerPort, remotePort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + + time.Sleep(3 * time.Second) + framework.ExpectNotEqual("", record) + }) + }) + + ginkgo.Describe("NewWorkConn", func() { + newFunc := func() *plugin.Request { + var r plugin.Request + r.Content = &plugin.NewWorkConnContent{} + return &r + } + + ginkgo.It("Validate Info", func() { + localPort := f.AllocPort() + + var record string + handler := func(req *plugin.Request) *plugin.Response { + var ret plugin.Response + content := req.Content.(*plugin.NewWorkConnContent) + record = content.NewWorkConn.RunID + ret.Unchange = true + return &ret + } + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) + + f.RunServer("", pluginServer) + + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + [[httpPlugins]] + name = "test" + addr = "127.0.0.1:%d" + path = "/handler" + ops = ["NewWorkConn"] + `, localPort) + + remotePort := f.AllocPort() + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + `, framework.TCPEchoServerPort, remotePort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + + framework.ExpectNotEqual("", record) + }) + }) + + ginkgo.Describe("NewUserConn", func() { + newFunc := func() *plugin.Request { + var r plugin.Request + r.Content = &plugin.NewUserConnContent{} + return &r + } + ginkgo.It("Validate Info", func() { + localPort := f.AllocPort() + + var record string + handler := func(req *plugin.Request) *plugin.Response { + var ret plugin.Response + content := req.Content.(*plugin.NewUserConnContent) + record = content.RemoteAddr + ret.Unchange = true + return &ret + } + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, nil) + + f.RunServer("", pluginServer) + + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + [[httpPlugins]] + name = "test" + addr = "127.0.0.1:%d" + path = "/handler" + ops = ["NewUserConn"] + `, localPort) + + remotePort := f.AllocPort() + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + `, framework.TCPEchoServerPort, remotePort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + + framework.ExpectNotEqual("", record) + }) + }) + + ginkgo.Describe("HTTPS Protocol", func() { + newFunc := func() *plugin.Request { + var r plugin.Request + r.Content = &plugin.NewUserConnContent{} + return &r + } + ginkgo.It("Validate Login Info, disable tls verify", func() { + localPort := f.AllocPort() + + var record string + handler := func(req *plugin.Request) *plugin.Response { + var ret plugin.Response + content := req.Content.(*plugin.NewUserConnContent) + record = content.RemoteAddr + ret.Unchange = true + return &ret + } + tlsConfig, err := transport.NewServerTLSConfig("", "", "") + framework.ExpectNoError(err) + pluginServer := pluginpkg.NewHTTPPluginServer(localPort, newFunc, handler, tlsConfig) + + f.RunServer("", pluginServer) + + serverConf := consts.DefaultServerConfig + fmt.Sprintf(` + [[httpPlugins]] + name = "test" + addr = "https://127.0.0.1:%d" + path = "/handler" + ops = ["NewUserConn"] + `, localPort) + + remotePort := f.AllocPort() + clientConf := consts.DefaultClientConfig + clientConf += fmt.Sprintf(` + [[proxies]] + name = "tcp" + type = "tcp" + localPort = {{ .%s }} + remotePort = %d + `, framework.TCPEchoServerPort, remotePort) + + f.RunProcesses([]string{serverConf}, []string{clientConf}) + + framework.NewRequestExpect(f).Port(remotePort).Ensure() + + framework.ExpectNotEqual("", record) + }) + }) +}) diff --git a/web/frps/auto-imports.d.ts b/web/frps/auto-imports.d.ts index 08908edd6a4..afc379b3e12 100644 --- a/web/frps/auto-imports.d.ts +++ b/web/frps/auto-imports.d.ts @@ -1,5 +1,3 @@ // Generated by 'unplugin-auto-import' export {} -declare global { - -} +declare global {}