mirror of https://github.com/fatedier/frp
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
440 lines
13 KiB
440 lines
13 KiB
// 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 nathole |
|
|
|
import ( |
|
"context" |
|
"fmt" |
|
"math/rand" |
|
"net" |
|
"strconv" |
|
"strings" |
|
"time" |
|
|
|
"github.com/fatedier/golib/pool" |
|
"github.com/samber/lo" |
|
"golang.org/x/net/ipv4" |
|
"k8s.io/apimachinery/pkg/util/sets" |
|
|
|
"github.com/fatedier/frp/pkg/msg" |
|
"github.com/fatedier/frp/pkg/transport" |
|
"github.com/fatedier/frp/pkg/util/xlog" |
|
) |
|
|
|
var ( |
|
// mode 0: simple detect mode, usually for both EasyNAT or HardNAT & EasyNAT(Public Network) |
|
// a. receiver sends detect message with low TTL |
|
// b. sender sends normal detect message to receiver |
|
// c. receiver receives detect message and sends back a message to sender |
|
// |
|
// mode 1: For HardNAT & EasyNAT, send detect messages to multiple guessed ports. |
|
// Usually applicable to scenarios where port changes are regular. |
|
// Most of the steps are the same as mode 0, but EasyNAT is fixed as the receiver and will send detect messages |
|
// with low TTL to multiple guessed ports of the sender. |
|
// |
|
// mode 2: For HardNAT & EasyNAT, ports changes are not regular. |
|
// a. HardNAT machine will listen on multiple ports and send detect messages with low TTL to EasyNAT machine |
|
// b. EasyNAT machine will send detect messages to random ports of HardNAT machine. |
|
// |
|
// mode 3: For HardNAT & HardNAT, both changes in the ports are regular. |
|
// Most of the steps are the same as mode 1, but the sender also needs to send detect messages to multiple guessed |
|
// ports of the receiver. |
|
// |
|
// mode 4: For HardNAT & HardNAT, one of the changes in the ports is regular. |
|
// Regular port changes are usually on the sender side. |
|
// a. Receiver listens on multiple ports and sends detect messages with low TTL to the sender's guessed range ports. |
|
// b. Sender sends detect messages to random ports of the receiver. |
|
SupportedModes = []int{DetectMode0, DetectMode1, DetectMode2, DetectMode3, DetectMode4} |
|
SupportedRoles = []string{DetectRoleSender, DetectRoleReceiver} |
|
|
|
DetectMode0 = 0 |
|
DetectMode1 = 1 |
|
DetectMode2 = 2 |
|
DetectMode3 = 3 |
|
DetectMode4 = 4 |
|
DetectRoleSender = "sender" |
|
DetectRoleReceiver = "receiver" |
|
) |
|
|
|
type PrepareResult struct { |
|
Addrs []string |
|
AssistedAddrs []string |
|
ListenConn *net.UDPConn |
|
NatType string |
|
Behavior string |
|
} |
|
|
|
// PreCheck is used to check if the proxy is ready for penetration. |
|
// Call this function before calling Prepare to avoid unnecessary preparation work. |
|
func PreCheck( |
|
ctx context.Context, transporter transport.MessageTransporter, |
|
proxyName string, timeout time.Duration, |
|
) error { |
|
timeoutCtx, cancel := context.WithTimeout(ctx, timeout) |
|
defer cancel() |
|
|
|
var natHoleRespMsg *msg.NatHoleResp |
|
transactionID := NewTransactionID() |
|
m, err := transporter.Do(timeoutCtx, &msg.NatHoleVisitor{ |
|
TransactionID: transactionID, |
|
ProxyName: proxyName, |
|
PreCheck: true, |
|
}, transactionID, msg.TypeNameNatHoleResp) |
|
if err != nil { |
|
return fmt.Errorf("get natHoleRespMsg error: %v", err) |
|
} |
|
mm, ok := m.(*msg.NatHoleResp) |
|
if !ok { |
|
return fmt.Errorf("get natHoleRespMsg error: invalid message type") |
|
} |
|
natHoleRespMsg = mm |
|
|
|
if natHoleRespMsg.Error != "" { |
|
return fmt.Errorf("%s", natHoleRespMsg.Error) |
|
} |
|
return nil |
|
} |
|
|
|
// Prepare is used to do some preparation work before penetration. |
|
func Prepare(stunServers []string) (*PrepareResult, error) { |
|
// discover for Nat type |
|
addrs, localAddr, err := Discover(stunServers, "") |
|
if err != nil { |
|
return nil, fmt.Errorf("discover error: %v", err) |
|
} |
|
if len(addrs) < 2 { |
|
return nil, fmt.Errorf("discover error: not enough addresses") |
|
} |
|
|
|
localIPs, _ := ListLocalIPsForNatHole(10) |
|
natFeature, err := ClassifyNATFeature(addrs, localIPs) |
|
if err != nil { |
|
return nil, fmt.Errorf("classify nat feature error: %v", err) |
|
} |
|
|
|
laddr, err := net.ResolveUDPAddr("udp4", localAddr.String()) |
|
if err != nil { |
|
return nil, fmt.Errorf("resolve local udp addr error: %v", err) |
|
} |
|
listenConn, err := net.ListenUDP("udp4", laddr) |
|
if err != nil { |
|
return nil, fmt.Errorf("listen local udp addr error: %v", err) |
|
} |
|
|
|
assistedAddrs := make([]string, 0, len(localIPs)) |
|
for _, ip := range localIPs { |
|
assistedAddrs = append(assistedAddrs, net.JoinHostPort(ip, strconv.Itoa(laddr.Port))) |
|
} |
|
return &PrepareResult{ |
|
Addrs: addrs, |
|
AssistedAddrs: assistedAddrs, |
|
ListenConn: listenConn, |
|
NatType: natFeature.NatType, |
|
Behavior: natFeature.Behavior, |
|
}, nil |
|
} |
|
|
|
// ExchangeInfo is used to exchange information between client and visitor. |
|
// 1. Send input message to server by msgTransporter. |
|
// 2. Server will gather information from client and visitor and analyze it. Then send back a NatHoleResp message to them to tell them how to do next. |
|
// 3. Receive NatHoleResp message from server. |
|
func ExchangeInfo( |
|
ctx context.Context, transporter transport.MessageTransporter, |
|
laneKey string, m msg.Message, timeout time.Duration, |
|
) (*msg.NatHoleResp, error) { |
|
timeoutCtx, cancel := context.WithTimeout(ctx, timeout) |
|
defer cancel() |
|
|
|
var natHoleRespMsg *msg.NatHoleResp |
|
m, err := transporter.Do(timeoutCtx, m, laneKey, msg.TypeNameNatHoleResp) |
|
if err != nil { |
|
return nil, fmt.Errorf("get natHoleRespMsg error: %v", err) |
|
} |
|
mm, ok := m.(*msg.NatHoleResp) |
|
if !ok { |
|
return nil, fmt.Errorf("get natHoleRespMsg error: invalid message type") |
|
} |
|
natHoleRespMsg = mm |
|
|
|
if natHoleRespMsg.Error != "" { |
|
return nil, fmt.Errorf("natHoleRespMsg get error info: %s", natHoleRespMsg.Error) |
|
} |
|
if len(natHoleRespMsg.CandidateAddrs) == 0 { |
|
return nil, fmt.Errorf("natHoleRespMsg get empty candidate addresses") |
|
} |
|
return natHoleRespMsg, nil |
|
} |
|
|
|
// MakeHole is used to make a NAT hole between client and visitor. |
|
func MakeHole(ctx context.Context, listenConn *net.UDPConn, m *msg.NatHoleResp, key []byte) (*net.UDPConn, *net.UDPAddr, error) { |
|
xl := xlog.FromContextSafe(ctx) |
|
transactionID := NewTransactionID() |
|
sendToRangePortsFunc := func(conn *net.UDPConn, addr string) error { |
|
return sendSidMessage(ctx, conn, m.Sid, transactionID, addr, key, m.DetectBehavior.TTL) |
|
} |
|
|
|
listenConns := []*net.UDPConn{listenConn} |
|
var detectAddrs []string |
|
if m.DetectBehavior.Role == DetectRoleSender { |
|
// sender |
|
if m.DetectBehavior.SendDelayMs > 0 { |
|
time.Sleep(time.Duration(m.DetectBehavior.SendDelayMs) * time.Millisecond) |
|
} |
|
detectAddrs = m.AssistedAddrs |
|
detectAddrs = append(detectAddrs, m.CandidateAddrs...) |
|
} else { |
|
// receiver |
|
if len(m.DetectBehavior.CandidatePorts) == 0 { |
|
detectAddrs = m.CandidateAddrs |
|
} |
|
|
|
if m.DetectBehavior.ListenRandomPorts > 0 { |
|
for i := 0; i < m.DetectBehavior.ListenRandomPorts; i++ { |
|
tmpConn, err := net.ListenUDP("udp4", nil) |
|
if err != nil { |
|
xl.Warn("listen random udp addr error: %v", err) |
|
continue |
|
} |
|
listenConns = append(listenConns, tmpConn) |
|
} |
|
} |
|
} |
|
|
|
detectAddrs = lo.Uniq(detectAddrs) |
|
for _, detectAddr := range detectAddrs { |
|
for _, conn := range listenConns { |
|
if err := sendSidMessage(ctx, conn, m.Sid, transactionID, detectAddr, key, m.DetectBehavior.TTL); err != nil { |
|
xl.Trace("send sid message from %s to %s error: %v", conn.LocalAddr(), detectAddr, err) |
|
} |
|
} |
|
} |
|
if len(m.DetectBehavior.CandidatePorts) > 0 { |
|
for _, conn := range listenConns { |
|
sendSidMessageToRangePorts(ctx, conn, m.CandidateAddrs, m.DetectBehavior.CandidatePorts, sendToRangePortsFunc) |
|
} |
|
} |
|
if m.DetectBehavior.SendRandomPorts > 0 { |
|
ctx, cancel := context.WithCancel(ctx) |
|
defer cancel() |
|
for i := range listenConns { |
|
go sendSidMessageToRandomPorts(ctx, listenConns[i], m.CandidateAddrs, m.DetectBehavior.SendRandomPorts, sendToRangePortsFunc) |
|
} |
|
} |
|
|
|
timeout := 5 * time.Second |
|
if m.DetectBehavior.ReadTimeoutMs > 0 { |
|
timeout = time.Duration(m.DetectBehavior.ReadTimeoutMs) * time.Millisecond |
|
} |
|
|
|
if len(listenConns) == 1 { |
|
raddr, err := waitDetectMessage(ctx, listenConns[0], m.Sid, key, timeout, m.DetectBehavior.Role) |
|
if err != nil { |
|
return nil, nil, fmt.Errorf("wait detect message error: %v", err) |
|
} |
|
return listenConns[0], raddr, nil |
|
} |
|
|
|
type result struct { |
|
lConn *net.UDPConn |
|
raddr *net.UDPAddr |
|
} |
|
resultCh := make(chan result) |
|
for _, conn := range listenConns { |
|
go func(lConn *net.UDPConn) { |
|
addr, err := waitDetectMessage(ctx, lConn, m.Sid, key, timeout, m.DetectBehavior.Role) |
|
if err != nil { |
|
lConn.Close() |
|
return |
|
} |
|
select { |
|
case resultCh <- result{lConn: lConn, raddr: addr}: |
|
default: |
|
lConn.Close() |
|
} |
|
}(conn) |
|
} |
|
|
|
select { |
|
case result := <-resultCh: |
|
return result.lConn, result.raddr, nil |
|
case <-time.After(timeout): |
|
return nil, nil, fmt.Errorf("wait detect message timeout") |
|
case <-ctx.Done(): |
|
return nil, nil, fmt.Errorf("wait detect message canceled") |
|
} |
|
} |
|
|
|
func waitDetectMessage( |
|
ctx context.Context, conn *net.UDPConn, sid string, key []byte, |
|
timeout time.Duration, role string, |
|
) (*net.UDPAddr, error) { |
|
xl := xlog.FromContextSafe(ctx) |
|
for { |
|
buf := pool.GetBuf(1024) |
|
_ = conn.SetReadDeadline(time.Now().Add(timeout)) |
|
n, raddr, err := conn.ReadFromUDP(buf) |
|
_ = conn.SetReadDeadline(time.Time{}) |
|
if err != nil { |
|
return nil, err |
|
} |
|
xl.Debug("get udp message local %s, from %s", conn.LocalAddr(), raddr) |
|
var m msg.NatHoleSid |
|
if err := DecodeMessageInto(buf[:n], key, &m); err != nil { |
|
xl.Warn("decode sid message error: %v", err) |
|
continue |
|
} |
|
pool.PutBuf(buf) |
|
|
|
if m.Sid != sid { |
|
xl.Warn("get sid message with wrong sid: %s, expect: %s", m.Sid, sid) |
|
continue |
|
} |
|
|
|
if !m.Response { |
|
// only wait for response messages if we are a sender |
|
if role == DetectRoleSender { |
|
continue |
|
} |
|
|
|
m.Response = true |
|
buf2, err := EncodeMessage(&m, key) |
|
if err != nil { |
|
xl.Warn("encode sid message error: %v", err) |
|
continue |
|
} |
|
_, _ = conn.WriteToUDP(buf2, raddr) |
|
} |
|
return raddr, nil |
|
} |
|
} |
|
|
|
func sendSidMessage( |
|
ctx context.Context, conn *net.UDPConn, |
|
sid string, transactionID string, addr string, key []byte, ttl int, |
|
) error { |
|
xl := xlog.FromContextSafe(ctx) |
|
ttlStr := "" |
|
if ttl > 0 { |
|
ttlStr = fmt.Sprintf(" with ttl %d", ttl) |
|
} |
|
xl.Trace("send sid message from %s to %s%s", conn.LocalAddr(), addr, ttlStr) |
|
raddr, err := net.ResolveUDPAddr("udp4", addr) |
|
if err != nil { |
|
return err |
|
} |
|
if transactionID == "" { |
|
transactionID = NewTransactionID() |
|
} |
|
m := &msg.NatHoleSid{ |
|
TransactionID: transactionID, |
|
Sid: sid, |
|
Response: false, |
|
Nonce: strings.Repeat("0", rand.Intn(20)), |
|
} |
|
buf, err := EncodeMessage(m, key) |
|
if err != nil { |
|
return err |
|
} |
|
if ttl > 0 { |
|
uConn := ipv4.NewConn(conn) |
|
original, err := uConn.TTL() |
|
if err != nil { |
|
xl.Trace("get ttl error %v", err) |
|
return err |
|
} |
|
xl.Trace("original ttl %d", original) |
|
|
|
err = uConn.SetTTL(ttl) |
|
if err != nil { |
|
xl.Trace("set ttl error %v", err) |
|
} else { |
|
defer func() { |
|
_ = uConn.SetTTL(original) |
|
}() |
|
} |
|
} |
|
|
|
if _, err := conn.WriteToUDP(buf, raddr); err != nil { |
|
return err |
|
} |
|
return nil |
|
} |
|
|
|
func sendSidMessageToRangePorts( |
|
ctx context.Context, conn *net.UDPConn, addrs []string, ports []msg.PortsRange, |
|
sendFunc func(*net.UDPConn, string) error, |
|
) { |
|
xl := xlog.FromContextSafe(ctx) |
|
for _, ip := range lo.Uniq(parseIPs(addrs)) { |
|
for _, portsRange := range ports { |
|
for i := portsRange.From; i <= portsRange.To; i++ { |
|
detectAddr := net.JoinHostPort(ip, strconv.Itoa(i)) |
|
if err := sendFunc(conn, detectAddr); err != nil { |
|
xl.Trace("send sid message from %s to %s error: %v", conn.LocalAddr(), detectAddr, err) |
|
} |
|
time.Sleep(2 * time.Millisecond) |
|
} |
|
} |
|
} |
|
} |
|
|
|
func sendSidMessageToRandomPorts( |
|
ctx context.Context, conn *net.UDPConn, addrs []string, count int, |
|
sendFunc func(*net.UDPConn, string) error, |
|
) { |
|
xl := xlog.FromContextSafe(ctx) |
|
used := sets.New[int]() |
|
getUnusedPort := func() int { |
|
for i := 0; i < 10; i++ { |
|
port := rand.Intn(65535-1024) + 1024 |
|
if !used.Has(port) { |
|
used.Insert(port) |
|
return port |
|
} |
|
} |
|
return 0 |
|
} |
|
|
|
for i := 0; i < count; i++ { |
|
select { |
|
case <-ctx.Done(): |
|
return |
|
default: |
|
} |
|
|
|
port := getUnusedPort() |
|
if port == 0 { |
|
continue |
|
} |
|
|
|
for _, ip := range lo.Uniq(parseIPs(addrs)) { |
|
detectAddr := net.JoinHostPort(ip, strconv.Itoa(port)) |
|
if err := sendFunc(conn, detectAddr); err != nil { |
|
xl.Trace("send sid message from %s to %s error: %v", conn.LocalAddr(), detectAddr, err) |
|
} |
|
time.Sleep(time.Millisecond * 15) |
|
} |
|
} |
|
} |
|
|
|
func parseIPs(addrs []string) []string { |
|
var ips []string |
|
for _, addr := range addrs { |
|
if ip, _, err := net.SplitHostPort(addr); err == nil { |
|
ips = append(ips, ip) |
|
} |
|
} |
|
return ips |
|
}
|
|
|