sshTunnelGateway refactor (#3784)

This commit is contained in:
fatedier
2023-11-21 11:19:35 +08:00
parent 8b432e179d
commit d5b41f1e14
34 changed files with 1036 additions and 1255 deletions

212
pkg/config/flags.go Normal file
View File

@@ -0,0 +1,212 @@
// 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 (
"fmt"
"strconv"
"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")
}
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")
}

View File

@@ -16,21 +16,11 @@ package v1
import (
"github.com/samber/lo"
"golang.org/x/crypto/ssh"
"github.com/fatedier/frp/pkg/config/types"
"github.com/fatedier/frp/pkg/util/util"
)
type SSHTunnelGateway struct {
BindPort int `json:"bindPort,omitempty" validate:"gte=0,lte=65535"`
PrivateKeyFilePath string `json:"privateKeyFilePath,omitempty"`
PublicKeyFilesPath string `json:"publicKeyFilesPath,omitempty"`
// store all public key file. load all when init
PublicKeyFilesMap map[string]ssh.PublicKey
}
type ServerConfig struct {
APIMetadata
@@ -41,9 +31,6 @@ type ServerConfig struct {
// BindPort specifies the port that the server listens on. By default, this
// value is 7000.
BindPort int `json:"bindPort,omitempty"`
SSHTunnelGateway SSHTunnelGateway `json:"sshGatewayConfig,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"`
@@ -80,6 +67,8 @@ type ServerConfig struct {
// value is "", a default page will be displayed.
Custom404Page string `json:"custom404Page,omitempty"`
SSHTunnelGateway SSHTunnelGateway `json:"sshTunnelGateway,omitempty"`
WebServer WebServerConfig `json:"webServer,omitempty"`
// EnablePrometheus will export prometheus metrics on webserver address
// in /metrics api.
@@ -114,6 +103,7 @@ func (c *ServerConfig) Complete() {
c.Log.Complete()
c.Transport.Complete()
c.WebServer.Complete()
c.SSHTunnelGateway.Complete()
c.BindAddr = util.EmptyOr(c.BindAddr, "0.0.0.0")
c.BindPort = util.EmptyOr(c.BindPort, 7000)
@@ -202,3 +192,14 @@ type TLSServerConfig struct {
TLSConfig
}
type SSHTunnelGateway struct {
BindPort int `json:"bindPort,omitempty"`
PrivateKeyFile string `json:"privateKeyFile,omitempty"`
AutoGenPrivateKeyPath string `json:"autoGenPrivateKeyPath,omitempty"`
AuthorizedKeysFile string `json:"authorizedKeysFile,omitempty"`
}
func (c *SSHTunnelGateway) Complete() {
c.AutoGenPrivateKeyPath = util.EmptyOr(c.AutoGenPrivateKeyPath, "./.autogen_ssh_key")
}

View File

@@ -1,72 +0,0 @@
package v1
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"os"
"path/filepath"
"golang.org/x/crypto/ssh"
)
const (
// custom define
SSHClientLoginUserPrefix = "_frpc_ssh_client_"
)
// encodePrivateKeyToPEM encodes Private Key from RSA to PEM format
func GeneratePrivateKey() ([]byte, error) {
privateKey, err := generatePrivateKey()
if err != nil {
return nil, errors.New("gen private key error")
}
privBlock := pem.Block{
Type: "RSA PRIVATE KEY",
Headers: nil,
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
}
return pem.EncodeToMemory(&privBlock), nil
}
// generatePrivateKey creates a RSA Private Key of specified byte size
func generatePrivateKey() (*rsa.PrivateKey, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return nil, err
}
err = privateKey.Validate()
if err != nil {
return nil, err
}
return privateKey, nil
}
func LoadSSHPublicKeyFilesInDir(dirPath string) (map[string]ssh.PublicKey, error) {
fileMap := make(map[string]ssh.PublicKey)
files, err := os.ReadDir(dirPath)
if err != nil {
return nil, err
}
for _, file := range files {
filePath := filepath.Join(dirPath, file.Name())
content, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}
parsedAuthorizedKey, _, _, _, err := ssh.ParseAuthorizedKey(content)
if err != nil {
continue
}
fileMap[ssh.FingerprintSHA256(parsedAuthorizedKey)] = parsedAuthorizedKey
}
return fileMap, nil
}

View File

@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !frps
package plugin
import (

View File

@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !frps
package plugin
import (

View File

@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !frps
package plugin
import (

View File

@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !frps
package plugin
import (

View File

@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !frps
package plugin
import (

View File

@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !frps
package plugin
import (

View File

@@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !frps
package plugin
import (

149
pkg/ssh/gateway.go Normal file
View File

@@ -0,0 +1,149 @@
// 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 ssh
import (
"fmt"
"net"
"os"
"strconv"
"strings"
"golang.org/x/crypto/ssh"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/transport"
"github.com/fatedier/frp/pkg/util/log"
utilnet "github.com/fatedier/frp/pkg/util/net"
)
type Gateway struct {
bindPort int
ln net.Listener
serverPeerListener *utilnet.InternalListener
sshConfig *ssh.ServerConfig
}
func NewGateway(
cfg v1.SSHTunnelGateway, bindAddr string,
serverPeerListener *utilnet.InternalListener,
) (*Gateway, error) {
sshConfig := &ssh.ServerConfig{}
// privateKey
var (
privateKeyBytes []byte
err error
)
if cfg.PrivateKeyFile != "" {
privateKeyBytes, err = os.ReadFile(cfg.PrivateKeyFile)
} else {
if cfg.AutoGenPrivateKeyPath != "" {
privateKeyBytes, _ = os.ReadFile(cfg.AutoGenPrivateKeyPath)
}
if len(privateKeyBytes) == 0 {
privateKeyBytes, err = transport.NewRandomPrivateKey()
if err == nil && cfg.AutoGenPrivateKeyPath != "" {
err = os.WriteFile(cfg.AutoGenPrivateKeyPath, privateKeyBytes, 0o600)
}
}
}
if err != nil {
return nil, err
}
privateKey, err := ssh.ParsePrivateKey(privateKeyBytes)
if err != nil {
return nil, err
}
sshConfig.AddHostKey(privateKey)
sshConfig.PublicKeyCallback = func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
if cfg.AuthorizedKeysFile == "" {
return &ssh.Permissions{
Extensions: map[string]string{
"user": "",
},
}, nil
}
authorizedKeysMap, err := loadAuthorizedKeysFromFile(cfg.AuthorizedKeysFile)
if err != nil {
return nil, fmt.Errorf("internal error")
}
user, ok := authorizedKeysMap[string(key.Marshal())]
if !ok {
return nil, fmt.Errorf("unknown public key for remoteAddr %q", conn.RemoteAddr())
}
return &ssh.Permissions{
Extensions: map[string]string{
"user": user,
},
}, nil
}
ln, err := net.Listen("tcp", net.JoinHostPort(bindAddr, strconv.Itoa(cfg.BindPort)))
if err != nil {
return nil, err
}
return &Gateway{
bindPort: cfg.BindPort,
ln: ln,
serverPeerListener: serverPeerListener,
sshConfig: sshConfig,
}, nil
}
func (g *Gateway) Run() {
for {
conn, err := g.ln.Accept()
if err != nil {
return
}
go g.handleConn(conn)
}
}
func (g *Gateway) handleConn(conn net.Conn) {
defer conn.Close()
ts, err := NewTunnelServer(conn, g.sshConfig, g.serverPeerListener)
if err != nil {
return
}
if err := ts.Run(); err != nil {
log.Error("ssh tunnel server run error: %v", err)
}
}
func loadAuthorizedKeysFromFile(path string) (map[string]string, error) {
authorizedKeysMap := make(map[string]string) // value is username
authorizedKeysBytes, err := os.ReadFile(path)
if err != nil {
return nil, err
}
for len(authorizedKeysBytes) > 0 {
pubKey, comment, _, rest, err := ssh.ParseAuthorizedKey(authorizedKeysBytes)
if err != nil {
return nil, err
}
authorizedKeysMap[string(pubKey.Marshal())] = strings.TrimSpace(comment)
authorizedKeysBytes = rest
}
return authorizedKeysMap, nil
}

279
pkg/ssh/server.go Normal file
View File

@@ -0,0 +1,279 @@
// 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 ssh
import (
"context"
"encoding/binary"
"fmt"
"net"
"strings"
"time"
libio "github.com/fatedier/golib/io"
"github.com/samber/lo"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh"
"github.com/fatedier/frp/pkg/config"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
utilnet "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/pkg/virtual"
)
const (
// https://datatracker.ietf.org/doc/html/rfc4254#page-16
ChannelTypeServerOpenChannel = "forwarded-tcpip"
RequestTypeForward = "tcpip-forward"
)
type tcpipForward struct {
Host string
Port uint32
}
// https://datatracker.ietf.org/doc/html/rfc4254#page-16
type forwardedTCPPayload struct {
Addr string
Port uint32
// can be default empty value but do not delete it
// because ssh protocol shoule be reserved
OriginAddr string
OriginPort uint32
}
type TunnelServer struct {
underlyingConn net.Conn
sshConn *ssh.ServerConn
sc *ssh.ServerConfig
vc *virtual.Client
serverPeerListener *utilnet.InternalListener
doneCh chan struct{}
}
func NewTunnelServer(conn net.Conn, sc *ssh.ServerConfig, serverPeerListener *utilnet.InternalListener) (*TunnelServer, error) {
s := &TunnelServer{
underlyingConn: conn,
sc: sc,
serverPeerListener: serverPeerListener,
doneCh: make(chan struct{}),
}
return s, nil
}
func (s *TunnelServer) Run() error {
sshConn, channels, requests, err := ssh.NewServerConn(s.underlyingConn, s.sc)
if err != nil {
return err
}
s.sshConn = sshConn
addr, extraPayload, err := s.waitForwardAddrAndExtraPayload(channels, requests, 3*time.Second)
if err != nil {
return err
}
clientCfg, pc, err := s.parseClientAndProxyConfigurer(addr, extraPayload)
if err != nil {
return err
}
clientCfg.User = util.EmptyOr(sshConn.Permissions.Extensions["user"], clientCfg.User)
pc.Complete(clientCfg.User)
s.vc = virtual.NewClient(clientCfg)
// join workConn and ssh channel
s.vc.SetInWorkConnCallback(func(base *v1.ProxyBaseConfig, workConn net.Conn, m *msg.StartWorkConn) bool {
c, err := s.openConn(addr)
if err != nil {
return false
}
libio.Join(c, workConn)
return false
})
// transfer connection from virtual client to server peer listener
go func() {
l := s.vc.PeerListener()
for {
conn, err := l.Accept()
if err != nil {
return
}
_ = s.serverPeerListener.PutConn(conn)
}
}()
xl := xlog.New().AddPrefix(xlog.LogPrefix{Name: "sshVirtualClient", Value: "sshVirtualClient", Priority: 100})
ctx := xlog.NewContext(context.Background(), xl)
go func() {
_ = s.vc.Run(ctx)
}()
s.vc.UpdateProxyConfigurer([]v1.ProxyConfigurer{pc})
_ = sshConn.Wait()
_ = sshConn.Close()
s.vc.Close()
close(s.doneCh)
return nil
}
func (s *TunnelServer) waitForwardAddrAndExtraPayload(
channels <-chan ssh.NewChannel,
requests <-chan *ssh.Request,
timeout time.Duration,
) (*tcpipForward, string, error) {
addrCh := make(chan *tcpipForward, 1)
extraPayloadCh := make(chan string, 1)
// get forward address
go func() {
addrGot := false
for req := range requests {
switch req.Type {
case RequestTypeForward:
if !addrGot {
payload := tcpipForward{}
if err := ssh.Unmarshal(req.Payload, &payload); err != nil {
return
}
addrGot = true
addrCh <- &payload
}
default:
if req.WantReply {
_ = req.Reply(true, nil)
}
}
}
}()
// get extra payload
go func() {
for newChannel := range channels {
// extraPayload will send to extraPayloadCh
go s.handleNewChannel(newChannel, extraPayloadCh)
}
}()
var (
addr *tcpipForward
extraPayload string
)
timer := time.NewTimer(timeout)
defer timer.Stop()
for {
select {
case v := <-addrCh:
addr = v
case extra := <-extraPayloadCh:
extraPayload = extra
case <-timer.C:
return nil, "", fmt.Errorf("get addr and extra payload timeout")
}
if addr != nil && extraPayload != "" {
break
}
}
return addr, extraPayload, nil
}
func (s *TunnelServer) parseClientAndProxyConfigurer(_ *tcpipForward, extraPayload string) (*v1.ClientCommonConfig, v1.ProxyConfigurer, error) {
cmd := &cobra.Command{}
args := strings.Split(extraPayload, " ")
if len(args) < 1 {
return nil, nil, fmt.Errorf("invalid extra payload")
}
proxyType := strings.TrimSpace(args[0])
supportTypes := []string{"tcp", "http", "https", "tcpmux", "stcp"}
if !lo.Contains(supportTypes, proxyType) {
return nil, nil, fmt.Errorf("invalid proxy type: %s, support types: %v", proxyType, supportTypes)
}
pc := v1.NewProxyConfigurerByType(v1.ProxyType(proxyType))
if pc == nil {
return nil, nil, fmt.Errorf("new proxy configurer error")
}
config.RegisterProxyFlags(cmd, pc)
clientCfg := v1.ClientCommonConfig{}
config.RegisterClientCommonConfigFlags(cmd, &clientCfg)
if err := cmd.ParseFlags(args); err != nil {
return nil, nil, fmt.Errorf("parse flags from ssh client error: %v", err)
}
return &clientCfg, pc, nil
}
func (s *TunnelServer) handleNewChannel(channel ssh.NewChannel, extraPayloadCh chan string) {
ch, reqs, err := channel.Accept()
if err != nil {
return
}
go s.keepAlive(ch)
for req := range reqs {
if req.Type != "exec" {
continue
}
if len(req.Payload) <= 4 {
continue
}
end := 4 + binary.BigEndian.Uint32(req.Payload[:4])
if len(req.Payload) < int(end) {
continue
}
extraPayload := string(req.Payload[4:end])
select {
case extraPayloadCh <- extraPayload:
default:
}
}
}
func (s *TunnelServer) keepAlive(ch ssh.Channel) {
tk := time.NewTicker(time.Second * 30)
defer tk.Stop()
for {
select {
case <-tk.C:
_, err := ch.SendRequest("heartbeat", false, nil)
if err != nil {
return
}
case <-s.doneCh:
return
}
}
}
func (s *TunnelServer) openConn(addr *tcpipForward) (net.Conn, error) {
payload := forwardedTCPPayload{
Addr: addr.Host,
Port: addr.Port,
}
channel, reqs, err := s.sshConn.OpenChannel(ChannelTypeServerOpenChannel, ssh.Marshal(&payload))
if err != nil {
return nil, fmt.Errorf("open ssh channel error: %v", err)
}
go ssh.DiscardRequests(reqs)
conn := utilnet.WrapReadWriteCloserToConn(channel, s.underlyingConn)
return conn, nil
}

View File

@@ -1,497 +0,0 @@
package ssh
import (
"encoding/binary"
"errors"
"flag"
"fmt"
"io"
"net"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
gerror "github.com/fatedier/golib/errors"
"golang.org/x/crypto/ssh"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/util/log"
)
const (
// ssh protocol define
// https://datatracker.ietf.org/doc/html/rfc4254#page-16
ChannelTypeServerOpenChannel = "forwarded-tcpip"
RequestTypeForward = "tcpip-forward"
// golang ssh package define.
// https://pkg.go.dev/golang.org/x/crypto/ssh
RequestTypeHeartbeat = "keepalive@openssh.com"
)
// 当 proxy 失败会返回该错误
type VProxyError struct{}
// ssh protocol define
// https://datatracker.ietf.org/doc/html/rfc4254#page-16
// parse ssh client cmds input
type forwardedTCPPayload struct {
Addr string
Port uint32
// can be default empty value but do not delete it
// because ssh protocol shoule be reserved
OriginAddr string
OriginPort uint32
}
// custom define
// parse ssh client cmds input
type CmdPayload struct {
Address string
Port uint32
}
// custom define
// with frp control cmds
type ExtraPayload struct {
Type string
// TODO port can be set by extra message and priority to ssh raw cmd
Address string
Port uint32
}
type Service struct {
tcpConn net.Conn
cfg *ssh.ServerConfig
sshConn *ssh.ServerConn
gChannel <-chan ssh.NewChannel
gReq <-chan *ssh.Request
addrPayloadCh chan CmdPayload
extraPayloadCh chan ExtraPayload
proxyPayloadCh chan v1.ProxyConfigurer
replyCh chan interface{}
closeCh chan struct{}
exit int32
}
func NewSSHService(
tcpConn net.Conn,
cfg *ssh.ServerConfig,
proxyPayloadCh chan v1.ProxyConfigurer,
replyCh chan interface{},
) (ss *Service, err error) {
ss = &Service{
tcpConn: tcpConn,
cfg: cfg,
addrPayloadCh: make(chan CmdPayload),
extraPayloadCh: make(chan ExtraPayload),
proxyPayloadCh: proxyPayloadCh,
replyCh: replyCh,
closeCh: make(chan struct{}),
exit: 0,
}
ss.sshConn, ss.gChannel, ss.gReq, err = ssh.NewServerConn(tcpConn, cfg)
if err != nil {
log.Error("ssh handshake error: %v", err)
return nil, err
}
log.Info("ssh connection success")
return ss, nil
}
func (ss *Service) Run() {
go ss.loopGenerateProxy()
go ss.loopParseCmdPayload()
go ss.loopParseExtraPayload()
go ss.loopReply()
}
func (ss *Service) Exit() <-chan struct{} {
return ss.closeCh
}
func (ss *Service) Close() {
if atomic.LoadInt32(&ss.exit) == 1 {
return
}
select {
case <-ss.closeCh:
return
default:
}
close(ss.closeCh)
close(ss.addrPayloadCh)
close(ss.extraPayloadCh)
_ = ss.sshConn.Wait()
ss.sshConn.Close()
ss.tcpConn.Close()
atomic.StoreInt32(&ss.exit, 1)
log.Info("ssh service close")
}
func (ss *Service) loopParseCmdPayload() {
for {
select {
case req, ok := <-ss.gReq:
if !ok {
log.Info("global request is close")
ss.Close()
return
}
switch req.Type {
case RequestTypeForward:
var addrPayload CmdPayload
if err := ssh.Unmarshal(req.Payload, &addrPayload); err != nil {
log.Error("ssh unmarshal error: %v", err)
return
}
_ = gerror.PanicToError(func() {
ss.addrPayloadCh <- addrPayload
})
default:
if req.Type == RequestTypeHeartbeat {
log.Debug("ssh heartbeat data")
} else {
log.Info("default req, data: %v", req)
}
}
if req.WantReply {
err := req.Reply(true, nil)
if err != nil {
log.Error("reply to ssh client error: %v", err)
}
}
case <-ss.closeCh:
log.Info("loop parse cmd payload close")
return
}
}
}
func (ss *Service) loopSendHeartbeat(ch ssh.Channel) {
tk := time.NewTicker(time.Second * 60)
defer tk.Stop()
for {
select {
case <-tk.C:
ok, err := ch.SendRequest("heartbeat", false, nil)
if err != nil {
log.Error("channel send req error: %v", err)
if err == io.EOF {
ss.Close()
return
}
continue
}
log.Debug("heartbeat send success, ok: %v", ok)
case <-ss.closeCh:
return
}
}
}
func (ss *Service) loopParseExtraPayload() {
log.Info("loop parse extra payload start")
for newChannel := range ss.gChannel {
ch, req, err := newChannel.Accept()
if err != nil {
log.Error("channel accept error: %v", err)
return
}
go ss.loopSendHeartbeat(ch)
go func(req <-chan *ssh.Request) {
for r := range req {
if len(r.Payload) <= 4 {
log.Info("r.payload is less than 4")
continue
}
if !strings.Contains(string(r.Payload), "tcp") && !strings.Contains(string(r.Payload), "http") {
log.Info("ssh protocol exchange data")
continue
}
// [4byte data_len|data]
end := 4 + binary.BigEndian.Uint32(r.Payload[:4])
if end > uint32(len(r.Payload)) {
end = uint32(len(r.Payload))
}
p := string(r.Payload[4:end])
msg, err := parseSSHExtraMessage(p)
if err != nil {
log.Error("parse ssh extra message error: %v, payload: %v", err, r.Payload)
continue
}
_ = gerror.PanicToError(func() {
ss.extraPayloadCh <- msg
})
return
}
}(req)
}
}
func (ss *Service) SSHConn() *ssh.ServerConn {
return ss.sshConn
}
func (ss *Service) TCPConn() net.Conn {
return ss.tcpConn
}
func (ss *Service) loopReply() {
for {
select {
case <-ss.closeCh:
log.Info("loop reply close")
return
case req := <-ss.replyCh:
switch req.(type) {
case *VProxyError:
log.Error("run frp proxy error, close ssh service")
ss.Close()
default:
// TODO
}
}
}
}
func (ss *Service) loopGenerateProxy() {
log.Info("loop generate proxy start")
for {
if atomic.LoadInt32(&ss.exit) == 1 {
return
}
wg := new(sync.WaitGroup)
wg.Add(2)
var p1 CmdPayload
var p2 ExtraPayload
go func() {
defer wg.Done()
for {
select {
case <-ss.closeCh:
return
case p1 = <-ss.addrPayloadCh:
return
}
}
}()
go func() {
defer wg.Done()
for {
select {
case <-ss.closeCh:
return
case p2 = <-ss.extraPayloadCh:
return
}
}
}()
wg.Wait()
if atomic.LoadInt32(&ss.exit) == 1 {
return
}
switch p2.Type {
case "http":
case "tcp":
ss.proxyPayloadCh <- &v1.TCPProxyConfig{
ProxyBaseConfig: v1.ProxyBaseConfig{
Name: fmt.Sprintf("ssh-proxy-%v-%v", ss.tcpConn.RemoteAddr().String(), time.Now().UnixNano()),
Type: p2.Type,
ProxyBackend: v1.ProxyBackend{
LocalIP: p1.Address,
},
},
RemotePort: int(p1.Port),
}
default:
log.Warn("invalid frp proxy type: %v", p2.Type)
}
}
}
func parseSSHExtraMessage(s string) (p ExtraPayload, err error) {
sn := len(s)
log.Info("parse ssh extra message: %v", s)
ss := strings.Fields(s)
if len(ss) == 0 {
if sn != 0 {
ss = append(ss, s)
} else {
return p, fmt.Errorf("invalid ssh input, args: %v", ss)
}
}
for i, v := range ss {
ss[i] = strings.TrimSpace(v)
}
if ss[0] != "tcp" && ss[0] != "http" {
return p, fmt.Errorf("only support tcp/http now")
}
switch ss[0] {
case "tcp":
tcpCmd, err := ParseTCPCommand(ss)
if err != nil {
return ExtraPayload{}, fmt.Errorf("invalid ssh input: %v", err)
}
port, _ := strconv.Atoi(tcpCmd.Port)
p = ExtraPayload{
Type: "tcp",
Address: tcpCmd.Address,
Port: uint32(port),
}
case "http":
httpCmd, err := ParseHTTPCommand(ss)
if err != nil {
return ExtraPayload{}, fmt.Errorf("invalid ssh input: %v", err)
}
_ = httpCmd
p = ExtraPayload{
Type: "http",
}
}
return p, nil
}
type HTTPCommand struct {
Domain string
BasicAuthUser string
BasicAuthPass string
}
func ParseHTTPCommand(params []string) (*HTTPCommand, error) {
if len(params) < 2 {
return nil, errors.New("invalid HTTP command")
}
var (
basicAuth string
domainURL string
basicAuthUser string
basicAuthPass string
)
fs := flag.NewFlagSet("http", flag.ContinueOnError)
fs.StringVar(&basicAuth, "basic-auth", "", "")
fs.StringVar(&domainURL, "domain", "", "")
fs.SetOutput(&nullWriter{}) // Disables usage output
err := fs.Parse(params[2:])
if err != nil {
if !errors.Is(err, flag.ErrHelp) {
return nil, err
}
}
if basicAuth != "" {
authParts := strings.SplitN(basicAuth, ":", 2)
basicAuthUser = authParts[0]
if len(authParts) > 1 {
basicAuthPass = authParts[1]
}
}
httpCmd := &HTTPCommand{
Domain: domainURL,
BasicAuthUser: basicAuthUser,
BasicAuthPass: basicAuthPass,
}
return httpCmd, nil
}
type TCPCommand struct {
Address string
Port string
}
func ParseTCPCommand(params []string) (*TCPCommand, error) {
if len(params) == 0 || params[0] != "tcp" {
return nil, errors.New("invalid TCP command")
}
if len(params) == 1 {
return &TCPCommand{}, nil
}
var (
address string
port string
)
fs := flag.NewFlagSet("tcp", flag.ContinueOnError)
fs.StringVar(&address, "address", "", "The IP address to listen on")
fs.StringVar(&port, "port", "", "The port to listen on")
fs.SetOutput(&nullWriter{}) // Disables usage output
args := params[1:]
err := fs.Parse(args)
if err != nil {
if !errors.Is(err, flag.ErrHelp) {
return nil, err
}
}
parsedAddr, err := net.ResolveIPAddr("ip", address)
if err != nil {
return nil, err
}
if _, err := net.LookupPort("tcp", port); err != nil {
return nil, err
}
tcpCmd := &TCPCommand{
Address: parsedAddr.String(),
Port: port,
}
return tcpCmd, nil
}
type nullWriter struct{}
func (w *nullWriter) Write(p []byte) (n int, err error) { return len(p), nil }

View File

@@ -1,185 +0,0 @@
package ssh
import (
"context"
"fmt"
"net"
"sync/atomic"
"time"
"golang.org/x/crypto/ssh"
"github.com/fatedier/frp/pkg/config"
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/log"
frp_net "github.com/fatedier/frp/pkg/util/net"
"github.com/fatedier/frp/pkg/util/util"
"github.com/fatedier/frp/pkg/util/xlog"
"github.com/fatedier/frp/server/controller"
"github.com/fatedier/frp/server/proxy"
)
// VirtualService is a client VirtualService run in frps
type VirtualService struct {
clientCfg v1.ClientCommonConfig
pxyCfg v1.ProxyConfigurer
serverCfg v1.ServerConfig
sshSvc *Service
// uniq id got from frps, attach it in loginMsg
runID string
loginMsg *msg.Login
// All resource managers and controllers
rc *controller.ResourceController
exit uint32 // 0 means not exit
// SSHService context
ctx context.Context
// call cancel to stop SSHService
cancel context.CancelFunc
replyCh chan interface{}
pxy proxy.Proxy
}
func NewVirtualService(
ctx context.Context,
clientCfg v1.ClientCommonConfig,
serverCfg v1.ServerConfig,
logMsg msg.Login,
rc *controller.ResourceController,
pxyCfg v1.ProxyConfigurer,
sshSvc *Service,
replyCh chan interface{},
) (svr *VirtualService, err error) {
svr = &VirtualService{
clientCfg: clientCfg,
serverCfg: serverCfg,
rc: rc,
loginMsg: &logMsg,
sshSvc: sshSvc,
pxyCfg: pxyCfg,
ctx: ctx,
exit: 0,
replyCh: replyCh,
}
svr.runID, err = util.RandID()
if err != nil {
return nil, err
}
go svr.loopCheck()
return
}
func (svr *VirtualService) Run(ctx context.Context) (err error) {
ctx, cancel := context.WithCancel(ctx)
svr.ctx = xlog.NewContext(ctx, xlog.New())
svr.cancel = cancel
remoteAddr, err := svr.RegisterProxy(&msg.NewProxy{
ProxyName: svr.pxyCfg.(*v1.TCPProxyConfig).Name,
ProxyType: svr.pxyCfg.(*v1.TCPProxyConfig).Type,
RemotePort: svr.pxyCfg.(*v1.TCPProxyConfig).RemotePort,
})
if err != nil {
return err
}
log.Info("run a reverse proxy on port: %v", remoteAddr)
return nil
}
func (svr *VirtualService) Close() {
svr.GracefulClose(time.Duration(0))
}
func (svr *VirtualService) GracefulClose(d time.Duration) {
atomic.StoreUint32(&svr.exit, 1)
svr.pxy.Close()
if svr.cancel != nil {
svr.cancel()
}
svr.replyCh <- &VProxyError{}
}
func (svr *VirtualService) loopCheck() {
<-svr.sshSvc.Exit()
svr.pxy.Close()
log.Info("virtual client service close")
}
func (svr *VirtualService) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err error) {
var pxyConf v1.ProxyConfigurer
pxyConf, err = config.NewProxyConfigurerFromMsg(pxyMsg, &svr.serverCfg)
if err != nil {
return
}
// User info
userInfo := plugin.UserInfo{
User: svr.loginMsg.User,
Metas: svr.loginMsg.Metas,
RunID: svr.runID,
}
svr.pxy, err = proxy.NewProxy(svr.ctx, &proxy.Options{
LoginMsg: svr.loginMsg,
UserInfo: userInfo,
Configurer: pxyConf,
ResourceController: svr.rc,
GetWorkConnFn: svr.GetWorkConn,
PoolCount: 10,
ServerCfg: &svr.serverCfg,
})
if err != nil {
return remoteAddr, err
}
remoteAddr, err = svr.pxy.Run()
if err != nil {
log.Warn("proxy run error: %v", err)
return
}
defer func() {
if err != nil {
log.Warn("proxy close")
svr.pxy.Close()
}
}()
return
}
func (svr *VirtualService) GetWorkConn() (workConn net.Conn, err error) {
// tell ssh client open a new stream for work
payload := forwardedTCPPayload{
Addr: svr.serverCfg.BindAddr, // TODO refine
Port: uint32(svr.pxyCfg.(*v1.TCPProxyConfig).RemotePort),
}
channel, reqs, err := svr.sshSvc.SSHConn().OpenChannel(ChannelTypeServerOpenChannel, ssh.Marshal(payload))
if err != nil {
return nil, fmt.Errorf("open ssh channel error: %v", err)
}
go ssh.DiscardRequests(reqs)
workConn = frp_net.WrapReadWriteCloserToConn(channel, svr.sshSvc.tcpConn)
return workConn, nil
}

View File

@@ -128,3 +128,15 @@ func NewClientTLSConfig(certPath, keyPath, caPath, serverName string) (*tls.Conf
return base, nil
}
func NewRandomPrivateKey() ([]byte, error) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
keyPEM := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(key),
})
return keyPEM, nil
}

View File

@@ -15,40 +15,81 @@
package xlog
import (
"sort"
"github.com/fatedier/frp/pkg/util/log"
)
type LogPrefix struct {
// Name is the name of the prefix, it won't be displayed in log but used to identify the prefix.
Name string
// Value is the value of the prefix, it will be displayed in log.
Value string
// The prefix with higher priority will be displayed first, default is 10.
Priority int
}
// Logger is not thread safety for operations on prefix
type Logger struct {
prefixes []string
prefixes []LogPrefix
prefixString string
}
func New() *Logger {
return &Logger{
prefixes: make([]string, 0),
prefixes: make([]LogPrefix, 0),
}
}
func (l *Logger) ResetPrefixes() (old []string) {
func (l *Logger) ResetPrefixes() (old []LogPrefix) {
old = l.prefixes
l.prefixes = make([]string, 0)
l.prefixes = make([]LogPrefix, 0)
l.prefixString = ""
return
}
func (l *Logger) AppendPrefix(prefix string) *Logger {
l.prefixes = append(l.prefixes, prefix)
l.prefixString += "[" + prefix + "] "
return l.AddPrefix(LogPrefix{
Name: prefix,
Value: prefix,
Priority: 10,
})
}
func (l *Logger) AddPrefix(prefix LogPrefix) *Logger {
found := false
if prefix.Priority <= 0 {
prefix.Priority = 10
}
for _, p := range l.prefixes {
if p.Name == prefix.Name {
found = true
p.Value = prefix.Value
p.Priority = prefix.Priority
}
}
if !found {
l.prefixes = append(l.prefixes, prefix)
}
l.renderPrefixString()
return l
}
func (l *Logger) renderPrefixString() {
sort.SliceStable(l.prefixes, func(i, j int) bool {
return l.prefixes[i].Priority < l.prefixes[j].Priority
})
l.prefixString = ""
for _, v := range l.prefixes {
l.prefixString += "[" + v.Value + "] "
}
}
func (l *Logger) Spawn() *Logger {
nl := New()
for _, v := range l.prefixes {
nl.AppendPrefix(v)
}
nl.prefixes = append(nl.prefixes, l.prefixes...)
nl.renderPrefixString()
return nl
}

92
pkg/virtual/client.go Normal file
View File

@@ -0,0 +1,92 @@
// 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 virtual
import (
"context"
"net"
"github.com/fatedier/frp/client"
v1 "github.com/fatedier/frp/pkg/config/v1"
"github.com/fatedier/frp/pkg/msg"
utilnet "github.com/fatedier/frp/pkg/util/net"
)
type Client struct {
l *utilnet.InternalListener
svr *client.Service
}
func NewClient(cfg *v1.ClientCommonConfig) *Client {
cfg.Complete()
ln := utilnet.NewInternalListener()
svr := client.NewService(cfg, nil, nil, "")
svr.SetConnectorCreator(func(context.Context, *v1.ClientCommonConfig) client.Connector {
return &pipeConnector{
peerListener: ln,
}
})
return &Client{
l: ln,
svr: svr,
}
}
func (c *Client) PeerListener() net.Listener {
return c.l
}
func (c *Client) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
c.svr.SetInWorkConnCallback(cb)
}
func (c *Client) UpdateProxyConfigurer(proxyCfgs []v1.ProxyConfigurer) {
_ = c.svr.ReloadConf(proxyCfgs, nil)
}
func (c *Client) Run(ctx context.Context) error {
return c.svr.Run(ctx)
}
func (c *Client) Close() {
c.l.Close()
c.svr.Close()
}
type pipeConnector struct {
peerListener *utilnet.InternalListener
}
func (pc *pipeConnector) Open() error {
return nil
}
func (pc *pipeConnector) Connect() (net.Conn, error) {
c1, c2 := net.Pipe()
if err := pc.peerListener.PutConn(c1); err != nil {
c1.Close()
c2.Close()
return nil, err
}
return c2, nil
}
func (pc *pipeConnector) Close() error {
pc.peerListener.Close()
return nil
}