frp改版-域前置

wss实现

github上面有人push了wss实现
https://github.com/fatedier/frp/pull/1919/files
注意:

  • 由于frp不支持wss协议,所以需要cdn配置回源协议为http。
    直接使用wss,frp服务端会报错:invalid protocol version。

具体代码修改

wss实现了就已经实现了配置域前置的第一步。
后面就是回源HOST了。由于默认frp的HOST就是连接的地址,导致无法使用域前置。
先分析WSS的HOST实在哪配置的。
D:\go\src\pkg\mod\golang.org\x\net@v0.0.0-20190724013045-ca1201d0de80\websocket\hybi.go
在hybiClientHandshake函数中,原先的代码是

bw.WriteString("Host: " + removeZone(config.Location.Host) + "\r\n")

下图是我修改后的

func hybiClientHandshake(config *Config, br *bufio.Reader, bw *bufio.Writer) (err error) {
    bw.WriteString("GET " + config.Location.RequestURI() + " HTTP/1.1\r\n")

    // According to RFC 6874, an HTTP client, proxy, or other
    // intermediary must remove any IPv6 zone identifier attached
    // to an outgoing URI.
    host :=config.Location.Host
    if tmpHost :=config.Header.Get("Host");tmpHost !=""{
        host=tmpHost
    }
    bw.WriteString("Host: " + removeZone(host) + "\r\n")



D:\go\src\pkg\mod\golang.org\x\net@v0.0.0-20190724013045-ca1201d0de80\websocket\client.go
NewConfig函数中可向config.Header传递值,需要新加一个参数websocket_domain。

func NewConfig(server, origin string,websocket_domain string) (config *Config, err error) {
    config = new(Config)
    config.Version = ProtocolVersionHybi13
    config.Location, err = url.ParseRequestURI(server)
    if err != nil {
        return
    }
    config.Origin, err = url.ParseRequestURI(origin)
    if err != nil {
        return
    }
    config.Header = http.Header(make(map[string][]string))
    config.Header.Set("Host",websocket_domain)
    return
}

NewConfig在util/net/websocket.go 中的ConnectWebsocketServer被调用。
由于在域前置里,用wss协议的情况下,server_addr用域名会不能正常回源,只能用ip。且会存在证书报错。所以需要做以下修改。

func ConnectWebsocketServer(addr string,websocket_domain string,isSecure bool) (net.Conn, error) {
    if isSecure {
        ho := strings.Split(addr, ":")
        ip, err := net.ResolveIPAddr("ip", ho[0])
        ip_addr := ip.String() + ":" + ho[1]
        if err != nil {
            return nil, err
        }
        addr = "wss://" + ip_addr + FrpWebsocketPath
    } else {
        addr = "ws://" + addr + FrpWebsocketPath
    }
    uri, err := url.Parse(addr)
    if err != nil {
        return nil, err
    }

    var origin string
    if isSecure {
        ho := strings.Split(uri.Host, ":")
        ip, err := net.ResolveIPAddr("ip", ho[0])
        ip_addr := ip.String() + ":" + ho[1]
        if err != nil {
            return nil, err
        }
        origin = "https://" + ip_addr
    } else {
        origin = "http://" + uri.Host
    }
    fmt.Println("addr:"+addr)
    fmt.Println("origin:"+origin)
    cfg, err := websocket.NewConfig(addr, origin, websocket_domain)
    if err != nil {
        return nil, err
    }
    cfg.Dialer = &net.Dialer{
        Timeout: 10 * time.Second,
    }

    conn, err := websocket.DialConfig(cfg)
    if err != nil {
        return nil, err
    }
    return conn, nil


}


ConnectWebsocketServer在util/net/conn.go中ConnectServerByProxy被调用,添加新增参数。

func ConnectServerByProxy(proxyURL string,websocket_domain string, protocol string, addr string) (c net.Conn, err error) {
    switch protocol {
    case "tcp":
        return gnet.DialTcpByProxy(proxyURL, addr)
    case "kcp":
        // http proxy is not supported for kcp
        return ConnectServer(protocol, addr)
    case "websocket":
        return ConnectWebsocketServer(addr, websocket_domain,false)
    case "wss":
        return ConnectWebsocketServer(addr, websocket_domain,true)
    default:
        return nil, fmt.Errorf("unsupport protocol: %s", protocol)
    }
}

ConnectServerByProxyWithTLS调用ConnectWebsocketServer

ConnectServerByProxyWithTLS被两处调用。
我这里是新加了功能,所以写了两种service.go。
分别是client/control.go、client/service.go



后面说一下,如何在frp的配置文件新增参数。
我是直接将配置文件写在变量里面。


调用流程parseClientCommonCfg--->parseClientCommonCfgFromIni---->UnmarshalClientConfFromIni

重点在UnmarshalClientConfFromIni函数中。
可以看到frp解析配置文件,通过conf.Get("common", "server_addr")获取参数,放入cfg.ServerAddr。

conf, err := ini.Load(strings.NewReader(content))
    if err != nil {
        return ClientCommonConf{}, fmt.Errorf("parse ini conf file error: %v", err)
    }

    cfg.AuthClientConfig = auth.UnmarshalAuthClientConfFromIni(conf)

    var (
        tmpStr string
        ok     bool
        v      int64
    )
    if tmpStr, ok = conf.Get("common", "server_addr"); ok {
        cfg.ServerAddr = tmpStr
    }

    if tmpStr, ok = conf.Get("common", "server_port"); ok {
        v, err = strconv.ParseInt(tmpStr, 10, 64)
        if err != nil {
            err = fmt.Errorf("Parse conf error: invalid server_port")
            return
        }
        cfg.ServerPort = int(v)
    }

所以我们只需要models/config/client_common.go 的ClientCommonConf新增结构。

在UnmarshalClientConfFromIni里面,获取protocol分支里面增加获取websocket_domain参数即可

if tmpStr, ok = conf.Get("common", "protocol"); ok {
        // Now it only support tcp and kcp and websocket.
        if tmpStr != "tcp" && tmpStr != "kcp" && tmpStr != "websocket"&& tmpStr != "wss" {
            err = fmt.Errorf("Parse conf error: invalid protocol")
            return
        }
        cfg.Protocol = tmpStr
        if tmpStr, ok = conf.Get("common", "websocket_domain"); ok {
            cfg.Websocket_domain = tmpStr
        }
    }

而client/control.go、client/service.go中都是通过ClientCommonConf获取参数。

最后,修复证书错误问题。
修改websocket/dial.go里的dialWithDialer方法。

func dialWithDialer(dialer *net.Dialer, config *Config) (conn net.Conn, err error) {
    switch config.Location.Scheme {
    case "ws":
        conn, err = dialer.Dial("tcp", parseAuthority(config.Location))

    case "wss":
        config.TlsConfig = &tls.Config{
            InsecureSkipVerify: true,
        }
        conn, err = tls.DialWithDialer(dialer, "tcp", parseAuthority(config.Location), config.TlsConfig)

    default:
        err = ErrBadScheme
    }
    return
}

当使用wss协议的时候,将TlsConfig.InsecureSkipVerify设置为true,即可忽略证书错误了。

frp默认的特征比较明显,修改特征
utils/net/websocket.go
修改常量FrpWebsocketPath。

测试

cdn配置回源http


客户端配置

[common]
  server_addr = xxxxxxxxxxxxx
  server_port = 443
  token=xxx
  websocket_domain=xxxxxxx
  protocol = wss
  tls_enable=true
  [%s]
  type = tcp
  remote_port = %d
  plugin = socks5
    plugin_user = a
    plugin_passwd = %s
    use_encryption = true
  use_compression = true

服务端配置

[common]
#绑定地址
bind_addr = 0.0.0.0
#TCP绑定端口
bind_port = 443
#连接密码
token=xxx

参考

FRP改造计划续-https://uknowsec.cn/posts/notes/FRP%E6%94%B9%E9%80%A0%E8%AE%A1%E5%88%92%E7%BB%AD.html

点击收藏 | 6 关注 | 2
登录 后跟帖