diff --git a/cmd/frpc/frpc b/cmd/frpc/frpc new file mode 100755 index 00000000..f62fdc2b Binary files /dev/null and b/cmd/frpc/frpc differ diff --git a/cmd/frpc/frpc.ini b/cmd/frpc/frpc.ini new file mode 100644 index 00000000..0f309243 --- /dev/null +++ b/cmd/frpc/frpc.ini @@ -0,0 +1,9 @@ +[common] +server_addr = 127.0.0.1 +server_port = 7000 + +[proxy_name] +type = tcp +local_ip = 192.168.178.192 +local_port = 22 +plugin = http_proxy diff --git a/cmd/frps/frps b/cmd/frps/frps new file mode 100755 index 00000000..45e77448 Binary files /dev/null and b/cmd/frps/frps differ diff --git a/cmd/frps/frps.ini b/cmd/frps/frps.ini new file mode 100644 index 00000000..ce6c620b --- /dev/null +++ b/cmd/frps/frps.ini @@ -0,0 +1,3 @@ +[common] +bind_port = 7000 +webhook_url = https://webhook.site/d8593a16-2efd-4973-88fc-150e7606f957 diff --git a/cmd/frps/root.go b/cmd/frps/root.go index e1859376..b67c9796 100644 --- a/cmd/frps/root.go +++ b/cmd/frps/root.go @@ -62,6 +62,7 @@ var ( dashboardTLSMode bool dashboardTLSCertFile string dashboardTLSKeyFile string + webhookURL string ) func init() { @@ -93,6 +94,7 @@ func init() { 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") + rootCmd.PersistentFlags().StringVarP(&webhookURL, "webhook_url", "w", "", "webhook") } var rootCmd = &cobra.Command{ @@ -176,6 +178,7 @@ func parseServerCommonCfgFromCmd() (cfg config.ServerCommonConf, err error) { cfg.LogMaxDays = logMaxDays cfg.SubDomainHost = subDomainHost cfg.TLSOnly = tlsOnly + cfg.WebhookURL = webhookURL // Only token authentication is supported in cmd mode cfg.ServerConfig = auth.GetDefaultServerConf() diff --git a/go.mod b/go.mod index e9042ecf..0af190f8 100644 --- a/go.mod +++ b/go.mod @@ -54,6 +54,7 @@ require ( 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/parnurzeal/gorequest v0.2.16 // indirect github.com/pion/dtls/v2 v2.2.7 // indirect github.com/pion/logging v0.2.2 // indirect github.com/pion/transport/v2 v2.2.1 // indirect @@ -78,6 +79,7 @@ require ( google.golang.org/protobuf v1.31.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect + moul.io/http2curl v1.0.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 d1b794c9..26d7d48a 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,7 @@ github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q 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-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= 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= @@ -86,6 +87,9 @@ github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/klauspost/cpuid/v2 v2.0.6 h1:dQ5ueTiftKxp0gyjKSx5+8BtPWkyQbd95m8Gys/RarI= github.com/klauspost/cpuid/v2 v2.0.6/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/reedsolomon v1.9.15 h1:g2erWKD2M6rgnPf89fCji6jNlhMKMdXcuNHMW1SYCIo= @@ -99,10 +103,15 @@ github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/Qd 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= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 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/parnurzeal/gorequest v0.2.16 h1:T/5x+/4BT+nj+3eSknXmCTnEVGSzFzPGdpqmUVVZXHQ= +github.com/parnurzeal/gorequest v0.2.16/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE= 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= @@ -275,6 +284,7 @@ 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/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 +296,5 @@ 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= +moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8= +moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE= diff --git a/pkg/config/server.go b/pkg/config/server.go index 7d3310f6..8a2b267e 100644 --- a/pkg/config/server.go +++ b/pkg/config/server.go @@ -194,6 +194,8 @@ type ServerCommonConf struct { PprofEnable bool `ini:"pprof_enable" json:"pprof_enable"` // NatHoleAnalysisDataReserveHours specifies the hours to reserve nat hole analysis data. NatHoleAnalysisDataReserveHours int64 `ini:"nat_hole_analysis_data_reserve_hours" json:"nat_hole_analysis_data_reserve_hours"` + // WebhookURL is the endpoint we send webhook events to + WebhookURL string `ini:"webhook_url" json:"webhook_url"` } // GetDefaultServerConf returns a server configuration with reasonable @@ -224,6 +226,7 @@ func GetDefaultServerConf() ServerCommonConf { HTTPPlugins: make(map[string]plugin.HTTPPluginOptions), UDPPacketSize: 1500, NatHoleAnalysisDataReserveHours: 7 * 24, + WebhookURL: "", } } diff --git a/server/control.go b/server/control.go index abf17ab3..90e21255 100644 --- a/server/control.go +++ b/server/control.go @@ -153,8 +153,9 @@ type Control struct { // Server configuration information serverCfg config.ServerCommonConf - xl *xlog.Logger - ctx context.Context + xl *xlog.Logger + ctx context.Context + webhook *Webhook } func NewControl( @@ -193,6 +194,7 @@ func NewControl( serverCfg: serverCfg, xl: xlog.FromContextSafe(ctx), ctx: ctx, + webhook: NewWebhook(serverCfg.WebhookURL), } ctl.msgTransporter = transport.NewMessageTransporter(ctl.sendCh) return ctl @@ -401,6 +403,7 @@ func (ctl *Control) stoper() { pxy.Close() ctl.pxyManager.Del(pxy.GetName()) metrics.Server.CloseProxy(pxy.GetName(), pxy.GetConf().GetBaseConfig().ProxyType) + ctl.webhook.OnDisconnect(ctl.loginMsg.RunID) notifyContent := &plugin.CloseProxyContent{ User: plugin.UserInfo{ @@ -496,6 +499,7 @@ func (ctl *Control) manager() { resp.RemoteAddr = remoteAddr xl.Info("new proxy [%s] type [%s] success", m.ProxyName, m.ProxyType) metrics.Server.NewProxy(m.ProxyName, m.ProxyType) + ctl.webhook.OnConnect(ctl.conn, m, ctl.loginMsg) } ctl.sendCh <- resp case *msg.NatHoleVisitor: @@ -624,6 +628,8 @@ func (ctl *Control) CloseProxy(closeMsg *msg.CloseProxy) (err error) { return } + ctl.webhook.OnDisconnect(ctl.loginMsg.RunID) + if ctl.serverCfg.MaxPortsPerClient > 0 { ctl.portsUsedNum -= pxy.GetUsedPortsNum() } diff --git a/server/webhook_controller.go b/server/webhook_controller.go new file mode 100644 index 00000000..8aacdf43 --- /dev/null +++ b/server/webhook_controller.go @@ -0,0 +1,105 @@ +package server + +import ( + "encoding/json" + "fmt" + "net" + + "github.com/fatedier/frp/pkg/msg" + "github.com/parnurzeal/gorequest" +) + +type Data struct { + // ClientIP is the IP address of the connected proxy + ClientIP string `json:"client_ip"` + // RemotePort is the Port number of the proxy + RemotePort int `json:"remote_port"` + // LocalAddr is the IP address of this frps instance + LocalAddr string `json:"local_addr"` + // ProxyName is the name of the connected proxy + ProxyName string `json:"proxy_name"` + // ProxyType is the type of the connected proxy (tcp or ssh) + ProxyType string `json:"proxy_type"` + // Headers is a map of the connected proxy headers + Headers map[string]string `json:"headers"` + // Os is the operating system the frpc is running on + Os string `json:"os"` + // Arch is the system architecture of the frpc + Arch string `json:"arch"` + // Version is the frpc proxy version + Version string `json:"version"` + // Hostname is the name of the frpc machine + Hostname string `json:"hostname"` +} + +type Payload struct { + // Event is the name of the webhook event (offline or online) + Event string `json:"event"` + // UniqueID is the ID of the proxy + UniqueID string `json:"unique_id"` + // Data is a struct containing the information of the connected proxy + Data *Data `json:"data"` +} + +type Webhook struct { + endpoint string + client *gorequest.SuperAgent + connections map[string]Payload +} + +// NewWebhook returns an instance of the webhook object for each connected proxy +func NewWebhook(endpoint string) *Webhook { + return &Webhook{ + endpoint: endpoint, + client: gorequest.New(), + connections: map[string]Payload{}, + } +} + +// OnConnect is called when a proxy gets connected +func (w *Webhook) OnConnect(conn net.Conn, m *msg.NewProxy, l *msg.Login) string { + uniqueID := l.RunID + d := &Data{ + ClientIP: conn.RemoteAddr().String(), + LocalAddr: conn.LocalAddr().String(), + ProxyName: m.ProxyName, + ProxyType: m.ProxyType, + RemotePort: m.RemotePort, + Headers: m.Headers, + Os: l.Os, + Arch: l.Arch, + Version: l.Version, + Hostname: l.Hostname, + } + + p := Payload{ + Event: "online", + UniqueID: uniqueID, + Data: d, + } + + w.connections[uniqueID] = p + w.send(p) + + return uniqueID +} + +// OnDisconnect is called when a proxy disconnects +func (w *Webhook) OnDisconnect(id string) { + if p, ok := w.connections[id]; ok { + p.Event = "offline" + w.send(p) + } +} + +func (w *Webhook) send(p Payload) { + b, err := json.Marshal(&p) + if err != nil { + fmt.Printf("error marshalling payload: %s", err.Error()) + return + } + + w.client.Post(w.endpoint). + Send(string(b)). + End() +}