forked from Mxmilu666/frp
feat(dns): add support for DNS-over-HTTPS and update version
This commit is contained in:
@@ -300,7 +300,13 @@ func runClientWithTokenAndIDs(token string, ids []string, unsafeFeatures *securi
|
|||||||
// Build URL with query parameters
|
// Build URL with query parameters
|
||||||
url := fmt.Sprintf("%s/api/v1/tunnel/frpc/config?token=%s&id=%s", apiServer, token, strings.Join(ids, ","))
|
url := fmt.Sprintf("%s/api/v1/tunnel/frpc/config?token=%s&id=%s", apiServer, token, strings.Join(ids, ","))
|
||||||
// #nosec G107 -- URL is constructed from trusted source (environment variable or hardcoded)
|
// #nosec G107 -- URL is constructed from trusted source (environment variable or hardcoded)
|
||||||
resp, err := http.Get(url)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create API request: %v", err)
|
||||||
|
}
|
||||||
|
// Carry client version in User-Agent so the API knows which frpc version is requesting
|
||||||
|
req.Header.Set("User-Agent", version.Full())
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to fetch config from API: %v", err)
|
return fmt.Errorf("failed to fetch config from API: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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().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().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.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")
|
c.Transport.TLS.Enable = cmd.PersistentFlags().BoolP("tls_enable", "", true, "enable frpc tls")
|
||||||
}
|
}
|
||||||
cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user")
|
cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user")
|
||||||
|
|||||||
@@ -49,7 +49,8 @@ type ClientCommonConfig struct {
|
|||||||
// STUN server to help penetrate NAT hole.
|
// STUN server to help penetrate NAT hole.
|
||||||
NatHoleSTUNServer string `json:"natHoleStunServer,omitempty"`
|
NatHoleSTUNServer string `json:"natHoleStunServer,omitempty"`
|
||||||
// DNSServer specifies a DNS server address for FRPC to use. If this value
|
// 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"`
|
DNSServer string `json:"dnsServer,omitempty"`
|
||||||
// LoginFailExit controls whether or not the client should exit after a
|
// LoginFailExit controls whether or not the client should exit after a
|
||||||
// failed login attempt. If false, the client will retry until a login
|
// failed login attempt. If false, the client will retry until a login
|
||||||
|
|||||||
@@ -17,9 +17,15 @@ package net
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetDefaultDNSAddress(dnsAddress string) {
|
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 {
|
if _, _, err := net.SplitHostPort(dnsAddress); err != nil {
|
||||||
dnsAddress = net.JoinHostPort(dnsAddress, "53")
|
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
|
package version
|
||||||
|
|
||||||
var version = "LoliaFRP-CLI 0.67.4"
|
var version = "LoliaFRP-CLI 0.67.5"
|
||||||
|
|
||||||
func Full() string {
|
func Full() string {
|
||||||
return version
|
return version
|
||||||
|
|||||||
Reference in New Issue
Block a user