forked from Mxmilu666/frp
feat(dns): add support for DNS-over-HTTPS and update version
This commit is contained in:
@@ -163,7 +163,7 @@ func RegisterClientCommonConfigFlags(cmd *cobra.Command, c *v1.ClientCommonConfi
|
||||
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")
|
||||
cmd.PersistentFlags().StringVarP(&c.DNSServer, "dns_server", "", "", "specify dns server instead of using system default one, support DoH url like https://1.1.1.1/dns-query")
|
||||
c.Transport.TLS.Enable = cmd.PersistentFlags().BoolP("tls_enable", "", true, "enable frpc tls")
|
||||
}
|
||||
cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user")
|
||||
|
||||
@@ -49,7 +49,8 @@ type ClientCommonConfig struct {
|
||||
// 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.
|
||||
// is "", the default DNS will be used. A DNS-over-HTTPS endpoint is also
|
||||
// supported, e.g. "https://1.1.1.1/dns-query".
|
||||
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
|
||||
|
||||
@@ -17,9 +17,15 @@ package net
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func SetDefaultDNSAddress(dnsAddress string) {
|
||||
// DNS-over-HTTPS endpoint, e.g. https://1.1.1.1/dns-query
|
||||
if strings.HasPrefix(dnsAddress, "https://") {
|
||||
SetDefaultDNSOverHTTPS(dnsAddress)
|
||||
return
|
||||
}
|
||||
if _, _, err := net.SplitHostPort(dnsAddress); err != nil {
|
||||
dnsAddress = net.JoinHostPort(dnsAddress, "53")
|
||||
}
|
||||
|
||||
181
pkg/util/net/doh.go
Normal file
181
pkg/util/net/doh.go
Normal file
@@ -0,0 +1,181 @@
|
||||
// Copyright 2026 The Lolia Team
|
||||
//
|
||||
// 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 net
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
dohMimeType = "application/dns-message"
|
||||
dohMaxResponseSize = 65535
|
||||
dohRequestTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// SetDefaultDNSOverHTTPS replaces net.DefaultResolver with one that sends
|
||||
// DNS queries to the given DNS-over-HTTPS (RFC 8484) endpoint,
|
||||
// e.g. "https://1.1.1.1/dns-query" or "https://dns.google/dns-query".
|
||||
func SetDefaultDNSOverHTTPS(dohURL string) {
|
||||
client := &http.Client{
|
||||
Timeout: dohRequestTimeout,
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: dohRequestTimeout,
|
||||
// Use a fresh resolver to look up the DoH server hostname itself,
|
||||
// avoiding infinite recursion through net.DefaultResolver.
|
||||
Resolver: &net.Resolver{},
|
||||
}).DialContext,
|
||||
MaxIdleConns: 10,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ForceAttemptHTTP2: true,
|
||||
},
|
||||
}
|
||||
net.DefaultResolver = &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, _ string) (net.Conn, error) {
|
||||
return &dohConn{
|
||||
ctx: ctx,
|
||||
client: client,
|
||||
url: dohURL,
|
||||
network: network,
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// dohConn adapts the DNS wire-format messages exchanged by net.Resolver
|
||||
// into DNS-over-HTTPS requests. Since dohConn does not implement
|
||||
// net.PacketConn, the resolver always uses stream (TCP-style) framing with a
|
||||
// 2-byte big-endian length prefix, regardless of the dialed network. The
|
||||
// resolver writes a query and then reads the response from the same
|
||||
// connection; the HTTP round trip happens synchronously inside Write.
|
||||
type dohConn struct {
|
||||
ctx context.Context
|
||||
client *http.Client
|
||||
url string
|
||||
network string
|
||||
|
||||
mu sync.Mutex
|
||||
deadline time.Time
|
||||
reqBuf bytes.Buffer // unprocessed request bytes
|
||||
respBuf bytes.Buffer // response stream with 2-byte length prefixes
|
||||
}
|
||||
|
||||
func (c *dohConn) Write(b []byte) (int, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.reqBuf.Write(b)
|
||||
for {
|
||||
data := c.reqBuf.Bytes()
|
||||
if len(data) < 2 {
|
||||
break
|
||||
}
|
||||
msgLen := int(binary.BigEndian.Uint16(data))
|
||||
if len(data) < 2+msgLen {
|
||||
break
|
||||
}
|
||||
msg := make([]byte, msgLen)
|
||||
copy(msg, data[2:2+msgLen])
|
||||
c.reqBuf.Next(2 + msgLen)
|
||||
|
||||
resp, err := c.roundTrip(msg)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var lenBuf [2]byte
|
||||
binary.BigEndian.PutUint16(lenBuf[:], uint16(len(resp)))
|
||||
c.respBuf.Write(lenBuf[:])
|
||||
c.respBuf.Write(resp)
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (c *dohConn) Read(b []byte) (int, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.respBuf.Len() == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
return c.respBuf.Read(b)
|
||||
}
|
||||
|
||||
func (c *dohConn) roundTrip(query []byte) ([]byte, error) {
|
||||
ctx := c.ctx
|
||||
if !c.deadline.IsZero() {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithDeadline(ctx, c.deadline)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.url, bytes.NewReader(query))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", dohMimeType)
|
||||
req.Header.Set("Accept", dohMimeType)
|
||||
|
||||
resp, err := c.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("doh server %s returned status %d", c.url, resp.StatusCode)
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, dohMaxResponseSize+1))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(body) > dohMaxResponseSize {
|
||||
return nil, fmt.Errorf("doh response from %s exceeds %d bytes", c.url, dohMaxResponseSize)
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func (c *dohConn) Close() error { return nil }
|
||||
|
||||
func (c *dohConn) LocalAddr() net.Addr { return &dohAddr{network: c.network, addr: "doh-client"} }
|
||||
func (c *dohConn) RemoteAddr() net.Addr { return &dohAddr{network: c.network, addr: c.url} }
|
||||
|
||||
func (c *dohConn) SetDeadline(t time.Time) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.deadline = t
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *dohConn) SetReadDeadline(t time.Time) error { return c.SetDeadline(t) }
|
||||
func (c *dohConn) SetWriteDeadline(t time.Time) error { return c.SetDeadline(t) }
|
||||
|
||||
type dohAddr struct {
|
||||
network string
|
||||
addr string
|
||||
}
|
||||
|
||||
func (a *dohAddr) Network() string { return a.network }
|
||||
func (a *dohAddr) String() string { return a.addr }
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
package version
|
||||
|
||||
var version = "LoliaFRP-CLI 0.67.4"
|
||||
var version = "LoliaFRP-CLI 0.67.5"
|
||||
|
||||
func Full() string {
|
||||
return version
|
||||
|
||||
Reference in New Issue
Block a user