You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
star/net/natt.go

766 lines
20 KiB
Go

6 months ago
package net
import (
"b612.me/apps/b612/netforward"
"b612.me/starlog"
3 months ago
"b612.me/starmap"
6 months ago
"context"
"encoding/binary"
"encoding/json"
"fmt"
"github.com/huin/goupnp/dcps/internetgateway2"
"math/rand"
"net"
"net/http"
"strconv"
"strings"
"sync"
"time"
)
type NatThroughs struct {
Lists []*NatThrough
WebPort int
AutoUPnP bool
KeepAlivePeriod int
KeepAliveIdel int
KeepAliveCount int
STUN string
Remote string
HealthCheckInterval int
3 months ago
Type string
6 months ago
}
func (n *NatThroughs) Close() {
for _, v := range n.Lists {
v.Close()
}
}
func (n *NatThroughs) Parse(reqs []string) error {
if n.KeepAlivePeriod == 0 {
n.KeepAlivePeriod = 10
}
if n.KeepAliveIdel == 0 {
n.KeepAliveIdel = 30
}
if n.KeepAliveCount == 0 {
n.KeepAliveCount = 5
}
if n.STUN == "" {
n.STUN = "turn.b612.me:3478"
}
3 months ago
if n.Type == "" {
n.Type = "tcp"
}
6 months ago
for _, v := range reqs {
var req = NatThrough{
Forward: netforward.NetForward{
LocalAddr: "0.0.0.0",
6 months ago
DialTimeout: time.Second * 5,
3 months ago
UDPTimeout: time.Second * 30,
6 months ago
KeepAlivePeriod: n.KeepAlivePeriod,
KeepAliveIdel: n.KeepAliveIdel,
KeepAliveCount: n.KeepAliveCount,
UsingKeepAlive: true,
EnableTCP: true,
3 months ago
EnableUDP: true,
6 months ago
},
3 months ago
Type: n.Type,
6 months ago
STUN: n.STUN,
Remote: n.Remote,
KeepAlivePeriod: n.KeepAlivePeriod,
KeepAliveIdel: n.KeepAliveIdel,
KeepAliveCount: n.KeepAliveCount,
AutoUPnP: n.AutoUPnP,
HealthCheckInterval: n.HealthCheckInterval,
}
strs := strings.Split(v, ",")
switch len(strs) {
case 1:
req.Forward.RemoteURI = strs[0]
case 2:
ipport := strings.Split(strs[0], ":")
if len(ipport) == 1 {
port, err := strconv.Atoi(ipport[0])
if err != nil {
return err
}
req.Forward.LocalPort = port
} else {
req.Forward.LocalAddr = ipport[0]
port, err := strconv.Atoi(ipport[1])
if err != nil {
return err
}
req.Forward.LocalPort = port
}
req.Forward.RemoteURI = strs[1]
case 3:
ipport := strings.Split(strs[1], ":")
if len(ipport) == 1 {
port, err := strconv.Atoi(ipport[0])
if err != nil {
return err
}
req.Forward.LocalPort = port
} else {
req.Forward.LocalAddr = ipport[0]
port, err := strconv.Atoi(ipport[1])
if err != nil {
return err
}
req.Forward.LocalPort = port
}
req.Forward.RemoteURI = strs[2]
req.Name = strs[0]
case 4:
ipport := strings.Split(strs[2], ":")
if len(ipport) == 1 {
port, err := strconv.Atoi(ipport[0])
if err != nil {
return err
}
req.Forward.LocalPort = port
} else {
req.Forward.LocalAddr = ipport[0]
port, err := strconv.Atoi(ipport[1])
if err != nil {
return err
}
req.Forward.LocalPort = port
}
req.Type = strings.ToLower(strs[0])
req.Forward.RemoteURI = strs[3]
req.Name = strs[1]
}
n.Lists = append(n.Lists, &req)
}
return nil
}
func (n *NatThroughs) Run() error {
go n.WebService()
wg := sync.WaitGroup{}
for _, v := range n.Lists {
wg.Add(1)
go func(v *NatThrough) {
defer wg.Done()
if err := v.Run(); err != nil {
3 months ago
starlog.Errorf("Failed to run natThrough: %v\n", err)
6 months ago
}
v.HealthCheck()
}(v)
}
wg.Wait()
return nil
}
type nattinfo struct {
Id int `json:"id"`
Name string `json:"name"`
Ext string `json:"ext"`
Local string `json:"local"`
Forward string `json:"forward"`
}
func (n *NatThroughs) WebService() error {
if n.WebPort == 0 {
return nil
}
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", n.WebPort))
if err != nil {
return err
}
starlog.Infof("Web service listen on %d\n", n.WebPort)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
var str string
for k, v := range n.Lists {
str += fmt.Sprintf("id:%d name:%s : %s <----> %s <-----> %s\n", k, v.Name, v.ExtUrl, v.localipport, v.Forward.RemoteURI)
}
w.Write([]byte(str))
})
http.HandleFunc("/json", func(w http.ResponseWriter, r *http.Request) {
var res []nattinfo
for k, v := range n.Lists {
res = append(res, nattinfo{
Id: k,
Name: v.Name,
Ext: v.ExtUrl,
Local: v.localipport,
Forward: v.Forward.RemoteURI,
})
}
w.Header().Set("Content-Type", "application/json")
data, _ := json.Marshal(res)
w.Write(data)
return
})
http.HandleFunc("/jump", func(w http.ResponseWriter, r *http.Request) {
types := "https://"
name := r.URL.Query().Get("name")
if name == "" {
w.Write([]byte("id is empty"))
return
}
if r.URL.Query().Get("type") == "http" {
types = "http://"
}
for _, v := range n.Lists {
if v.Name == name {
http.Redirect(w, r, types+v.ExtUrl, http.StatusFound)
return
}
}
})
return http.Serve(listener, nil)
}
// NatThrough 类似于natter.py 是一个用于Full Cone NAT直接穿透的工具
type NatThrough struct {
Name string
OriginLocalPort int
Forward netforward.NetForward
Type string
STUN string
Remote string
KeepAlivePeriod int
KeepAliveIdel int
KeepAliveCount int
AutoUPnP bool
isOk bool
ExtUrl string
localipport string
keepaliveConn net.Conn
HealthCheckInterval int
stopFn context.CancelFunc
stopCtx context.Context
}
func (n *NatThrough) Close() {
n.stopFn()
n.Forward.Close()
}
func (c *NatThrough) Run() error {
c.isOk = false
c.stopCtx, c.stopFn = context.WithCancel(context.Background())
c.OriginLocalPort = c.Forward.LocalPort
if c.Forward.LocalPort == 0 {
listener, err := net.Listen(c.Type, c.Forward.LocalAddr+":0")
if err != nil {
return fmt.Errorf("Failed to listen on %s: %v", c.Forward.LocalAddr, err)
}
if c.Type == "tcp" {
c.Forward.LocalPort = listener.Addr().(*net.TCPAddr).Port
} else {
c.Forward.LocalPort = listener.Addr().(*net.UDPAddr).Port
}
listener.Close()
}
if c.Type == "tcp" {
c.Forward.EnableTCP = true
c.Forward.EnableUDP = false
} else {
c.Forward.EnableTCP = false
c.Forward.EnableUDP = true
}
3 months ago
starlog.Infof("NatThrough Type: %s\n", c.Type)
6 months ago
starlog.Infof("Local Port: %d\n", c.Forward.LocalPort)
starlog.Infof("Keepalive To: %s\n", c.Remote)
starlog.Infof("Forward To: %s\n", c.Forward.RemoteURI)
innerIp, extIp, err := c.GetIPPortFromSTUN(c.Type, c.Forward.LocalAddr, c.Forward.LocalPort, c.STUN)
if err != nil {
return fmt.Errorf("Failed to get external IP and port: %v", err)
}
starlog.Infof("Internal Addr: %s \n", innerIp.String())
starlog.Infof("External Addr: %s \n", extIp.String())
getIP := func(ip net.Addr) string {
switch ip.(type) {
case *net.TCPAddr:
return ip.(*net.TCPAddr).IP.String()
case *net.UDPAddr:
return ip.(*net.UDPAddr).IP.String()
default:
return ""
}
}
getPort := func(ip net.Addr) int {
switch ip.(type) {
case *net.TCPAddr:
return ip.(*net.TCPAddr).Port
case *net.UDPAddr:
return ip.(*net.UDPAddr).Port
default:
return 0
}
}
go func() {
if err := c.KeepAlive(c.Forward.LocalAddr, c.Forward.LocalPort); err != nil {
3 months ago
starlog.Errorf("Failed to run keepalive: %v\n", err)
6 months ago
c.Forward.Close()
c.stopFn()
}
}()
innerIp, extIp, err = c.GetIPPortFromSTUN(c.Type, c.Forward.LocalAddr, c.Forward.LocalPort, c.STUN)
if err != nil {
return fmt.Errorf("Failed to get external IP and port: %v", err)
}
starlog.Infof("Retest Internal Addr: %s \n", innerIp.String())
starlog.Infof("Retest External Addr: %s \n", extIp.String())
if c.AutoUPnP {
go c.HandleUPnP(getIP(innerIp), uint16(getPort(extIp)))
}
err = c.Forward.Run()
if err != nil {
return fmt.Errorf("Failed to run forward: %v", err)
}
c.isOk = true
c.localipport = fmt.Sprintf("%s:%d", getIP(innerIp), getPort(innerIp))
c.ExtUrl = fmt.Sprintf("%s:%d", getIP(extIp), getPort(extIp))
return nil
}
func (c *NatThrough) HealthCheck() {
3 months ago
getIP := func(ip net.Addr) string {
switch ip.(type) {
case *net.TCPAddr:
return ip.(*net.TCPAddr).IP.String()
case *net.UDPAddr:
return ip.(*net.UDPAddr).IP.String()
default:
return ""
}
}
getPort := func(ip net.Addr) int {
switch ip.(type) {
case *net.TCPAddr:
return ip.(*net.TCPAddr).Port
case *net.UDPAddr:
return ip.(*net.UDPAddr).Port
default:
return 0
}
}
6 months ago
count := 0
if c.HealthCheckInterval == 0 {
c.HealthCheckInterval = 30
}
for {
select {
case <-c.stopCtx.Done():
return
case <-time.After(time.Second * time.Duration(c.HealthCheckInterval)):
}
if c.Type == "udp" {
3 months ago
_, extIp, err := c.UdpKeppaliveSTUN(c.Forward.UdpListener(), c.STUN)
if err != nil {
count++
starlog.Errorf("Health Check Error: %v\n", err)
continue
}
extUrl := fmt.Sprintf("%s:%d", getIP(extIp), getPort(extIp))
if c.ExtUrl != extUrl {
count++
} else {
count = 0
}
starlog.Noticef("Health Check:Origin %s,Current %s\n", c.ExtUrl, extUrl)
6 months ago
} else {
3 months ago
conn, err := net.DialTimeout("tcp", c.ExtUrl, time.Second*2)
if err != nil {
starlog.Warningf("Health Check Fail: %v\n", err)
count++
} else {
count = 0
starlog.Infof("Health Check Ok\n")
conn.(*net.TCPConn).SetLinger(0)
conn.Close()
}
6 months ago
}
if count >= 3 {
count = 0
starlog.Errorf("Failed to connect to remote, close connection retrying\n")
c.stopFn()
c.keepaliveConn.Close()
c.Forward.Close()
forward := netforward.NetForward{
LocalAddr: c.Forward.LocalAddr,
LocalPort: c.OriginLocalPort,
RemoteURI: c.Forward.RemoteURI,
KeepAlivePeriod: c.KeepAlivePeriod,
KeepAliveIdel: c.KeepAliveIdel,
KeepAliveCount: c.KeepAliveCount,
UsingKeepAlive: true,
}
time.Sleep(time.Second * 22)
c.Forward = forward
c.Run()
}
}
}
func (c *NatThrough) KeepAlive(localAddr string, localPort int) error {
for {
select {
case <-c.stopCtx.Done():
return nil
default:
}
if c.Type == "tcp" {
dialer := net.Dialer{
Control: netforward.ControlSetReUseAddr,
LocalAddr: &net.TCPAddr{IP: net.ParseIP(localAddr), Port: localPort},
}
conn, err := dialer.Dial("tcp", c.Remote)
if err != nil {
starlog.Errorf("Failed to dial remote: %v\n", err)
time.Sleep(time.Second * 5)
continue
}
c.keepaliveConn = conn
conn.(*net.TCPConn).SetLinger(0)
netforward.SetTcpInfo(conn.(*net.TCPConn), true, c.KeepAliveIdel, c.KeepAlivePeriod, c.KeepAliveCount, 0)
starlog.Infof("Keepalive local:%s remote: %s\n", conn.LocalAddr().String(), conn.RemoteAddr().String())
go func() {
for {
str := fmt.Sprintf("HEAD /keep-alive HTTP/1.1\r\n"+
"Host: %s\r\n"+
"User-Agent: curl/8.0.0 (B612)\r\n"+
"Accept: */*\r\n"+
"Connection: keep-alive\r\n\r\n", strings.Split(c.Remote, ":")[0])
//fmt.Println(str)
if _, err = conn.Write([]byte(str)); err != nil {
fmt.Println(err)
}
time.Sleep(time.Second * 20)
}
}()
for {
_, err := conn.Read(make([]byte, 4096))
if err != nil {
starlog.Warningf("Failed to keepalive remote: %v\n", err)
conn.Close()
break
}
}
} else if c.Type == "udp" {
rmtUdpAddr, err := net.ResolveUDPAddr("udp", c.Remote)
if err != nil {
return err
}
3 months ago
if c.Forward.UdpListener() == nil {
6 months ago
time.Sleep(time.Second * 5)
continue
}
3 months ago
c.keepaliveConn = c.Forward.UdpListener()
6 months ago
for {
3 months ago
_, err = c.Forward.UdpListener().WriteTo([]byte("b612 udp nat through"), rmtUdpAddr)
6 months ago
if err != nil {
3 months ago
c.keepaliveConn.Close()
6 months ago
starlog.Warningf("Failed to keepalive remote: %v\n", err)
3 months ago
time.Sleep(time.Second * 30)
6 months ago
break
}
3 months ago
starlog.Infof("UDP Keepalive Ok! %v\n", rmtUdpAddr.String())
6 months ago
time.Sleep(time.Second * 30)
}
}
}
}
func (c *NatThrough) HandleUPnP(localaddr string, extPort uint16) {
for {
select {
case <-c.stopCtx.Done():
return
default:
}
client, err := c.FoundUsableUPnP()
if err != nil {
starlog.Errorf("Failed to find UPnP device: %v\n", err)
time.Sleep(time.Second * 20)
continue
}
starlog.Infof("Found UPnP device!\n")
_, _, _, _, _, err = client.GetSpecificPortMappingEntry("", uint16(c.Forward.LocalPort), "TCP")
if err == nil {
starlog.Infof("Port mapping Ok\n")
time.Sleep(time.Second * 20)
continue
}
err = client.AddPortMapping("", uint16(c.Forward.LocalPort), strings.ToUpper(c.Type), uint16(c.Forward.LocalPort), localaddr, true, "B612 TCP Nat PassThrough", 75)
if err != nil {
starlog.Errorf("Failed to add port mapping: %v\n", err)
time.Sleep(time.Second * 20)
continue
}
starlog.Infof("Port mapping added:externalPort %d,localAddr %s,localPort %d\n", extPort, localaddr, c.Forward.LocalPort)
time.Sleep(time.Second * 20)
}
}
func (c *NatThrough) GetIPPortFromSTUN(netType string, localip string, localPort int, stunServer string) (net.Addr, net.Addr, error) {
// 替换为你的 TURN 服务器地址
stunAddr, err := net.ResolveUDPAddr("udp", stunServer)
if err != nil {
return nil, nil, fmt.Errorf("failed to resolve STUN server address: %v", err)
}
var conn net.Conn
if netType == "tcp" {
dialer := net.Dialer{
Control: netforward.ControlSetReUseAddr,
LocalAddr: &net.TCPAddr{IP: net.ParseIP(localip), Port: localPort},
}
conn, err = dialer.Dial("tcp", stunAddr.String())
if err != nil {
return nil, nil, err
}
conn.(*net.TCPConn).SetLinger(0)
}
if netType == "udp" {
3 months ago
6 months ago
conn, err = net.DialUDP(netType, &net.UDPAddr{IP: net.ParseIP(localip), Port: localPort}, stunAddr)
if err != nil {
return nil, nil, fmt.Errorf("failed to connect to STUN server: %v", err)
}
}
defer conn.Close()
innerAddr := conn.LocalAddr()
// Create STUN request
transactionID := make([]byte, 12)
rand.Read(transactionID)
stunRequest := make([]byte, 20)
binary.BigEndian.PutUint16(stunRequest[0:], 0x0001) // Message Type: Binding Request
binary.BigEndian.PutUint16(stunRequest[2:], 0x0000) // Message Length
copy(stunRequest[4:], []byte{0x21, 0x12, 0xa4, 0x42}) // Magic Cookie
copy(stunRequest[8:], transactionID) // Transaction ID
_, err = conn.Write(stunRequest)
if err != nil {
return nil, nil, fmt.Errorf("failed to send STUN request: %v", err)
}
buf := make([]byte, 1500)
conn.SetReadDeadline(time.Now().Add(3 * time.Second))
n, err := conn.Read(buf)
if err != nil {
return nil, nil, fmt.Errorf("failed to receive STUN response: %v", err)
}
// Parse STUN response
if n < 20 {
return nil, nil, fmt.Errorf("invalid STUN response")
}
payload := buf[20:n]
var ip uint32
var port uint16
for len(payload) > 0 {
attrType := binary.BigEndian.Uint16(payload[0:])
attrLen := binary.BigEndian.Uint16(payload[2:])
if len(payload) < int(4+attrLen) {
return nil, nil, fmt.Errorf("invalid STUN attribute length")
}
if attrType == 0x0001 || attrType == 0x0020 {
port = binary.BigEndian.Uint16(payload[6:])
ip = binary.BigEndian.Uint32(payload[8:])
if attrType == 0x0020 {
port ^= 0x2112
ip ^= 0x2112a442
}
break
}
payload = payload[4+attrLen:]
}
if ip == 0 || port == 0 {
return nil, nil, fmt.Errorf("invalid STUN response")
}
outerAddr := &net.UDPAddr{
IP: net.IPv4(byte(ip>>24), byte(ip>>16), byte(ip>>8), byte(ip)),
Port: int(port),
}
return innerAddr, outerAddr, nil
}
3 months ago
func (c *NatThrough) UdpKeppaliveSTUN(conn *net.UDPConn, stunServer string) (net.Addr, net.Addr, error) {
// 替换为你的 TURN 服务器地址
var target *starmap.StarStack
{
tmpConn, err := net.Dial("udp", stunServer)
if err != nil {
return nil, nil, fmt.Errorf("failed to connect to STUN server: %v", err)
}
if c.Forward.UdpHooks == nil {
c.Forward.UdpHooks = make(map[string]*starmap.StarStack)
}
if c.Forward.UdpHooks[tmpConn.RemoteAddr().String()] == nil {
c.Forward.UdpHooks[tmpConn.RemoteAddr().String()] = starmap.NewStarStack(16)
}
target = c.Forward.UdpHooks[tmpConn.RemoteAddr().String()]
tmpConn.Close()
}
stunAddr, err := net.ResolveUDPAddr("udp", stunServer)
if err != nil {
return nil, nil, fmt.Errorf("failed to resolve STUN server address: %v", err)
}
innerAddr := conn.LocalAddr()
// Create STUN request
transactionID := make([]byte, 12)
rand.Read(transactionID)
stunRequest := make([]byte, 20)
binary.BigEndian.PutUint16(stunRequest[0:], 0x0001) // Message Type: Binding Request
binary.BigEndian.PutUint16(stunRequest[2:], 0x0000) // Message Length
copy(stunRequest[4:], []byte{0x21, 0x12, 0xa4, 0x42}) // Magic Cookie
copy(stunRequest[8:], transactionID) // Transaction ID
_, err = conn.WriteToUDP(stunRequest, stunAddr)
if err != nil {
return nil, nil, fmt.Errorf("failed to send STUN request: %v", err)
}
time.Sleep(time.Millisecond * 2500)
tmp, err := target.Pop()
if err != nil {
return nil, nil, fmt.Errorf("failed to receive STUN response: %v", err)
}
buf := tmp.([]byte)
n := len(buf)
// Parse STUN response
if n < 20 {
return nil, nil, fmt.Errorf("invalid STUN response")
}
payload := buf[20:n]
var ip uint32
var port uint16
for len(payload) > 0 {
attrType := binary.BigEndian.Uint16(payload[0:])
attrLen := binary.BigEndian.Uint16(payload[2:])
if len(payload) < int(4+attrLen) {
return nil, nil, fmt.Errorf("invalid STUN attribute length")
}
if attrType == 0x0001 || attrType == 0x0020 {
port = binary.BigEndian.Uint16(payload[6:])
ip = binary.BigEndian.Uint32(payload[8:])
if attrType == 0x0020 {
port ^= 0x2112
ip ^= 0x2112a442
}
break
}
payload = payload[4+attrLen:]
}
if ip == 0 || port == 0 {
return nil, nil, fmt.Errorf("invalid STUN response")
}
outerAddr := &net.UDPAddr{
IP: net.IPv4(byte(ip>>24), byte(ip>>16), byte(ip>>8), byte(ip)),
Port: int(port),
}
return innerAddr, outerAddr, nil
}
6 months ago
func (c *NatThrough) GetMyOutIP() string {
tmp, err := net.Dial("udp", "8.8.8.8:53")
if err != nil {
return ""
}
return tmp.LocalAddr().(*net.UDPAddr).IP.String()
}
func (c *NatThrough) FoundUsableUPnP() (RouterClient, error) {
wg := sync.WaitGroup{}
found := false
result := make(chan RouterClient, 3)
defer close(result)
wg.Add(3)
go func() {
defer wg.Done()
clients, errors, err := internetgateway2.NewWANIPConnection2Clients()
if err != nil {
return
}
if len(errors) > 0 {
return
}
if len(clients) == 0 {
return
}
starlog.Infof("Found WANIPConnection2 clients:%s\n", clients[0].Location.String())
found = true
result <- clients[0]
}()
go func() {
defer wg.Done()
clients, errors, err := internetgateway2.NewWANIPConnection1Clients()
if err != nil {
return
}
if len(errors) > 0 {
return
}
if len(clients) == 0 {
return
}
starlog.Infof("Found WANIPConnection1 clients:%s\n", clients[0].Location.String())
found = true
result <- clients[0]
}()
go func() {
defer wg.Done()
clients, errors, err := internetgateway2.NewWANPPPConnection1Clients()
if err != nil {
return
}
if len(errors) > 0 {
return
}
if len(clients) == 0 {
return
}
starlog.Infof("Found WANPPPConnection1 clients:%s\n", clients[0].Location.String())
found = true
result <- clients[0]
}()
wg.Wait()
if found {
return <-result, nil
}
return nil, fmt.Errorf("no UPnP devices discovered")
}
type RouterClient interface {
AddPortMapping(
NewRemoteHost string,
NewExternalPort uint16,
NewProtocol string,
NewInternalPort uint16,
NewInternalClient string,
NewEnabled bool,
NewPortMappingDescription string,
NewLeaseDuration uint32,
) (err error)
GetExternalIPAddress() (
NewExternalIPAddress string,
err error,
)
DeletePortMapping(NewRemoteHost string, NewExternalPort uint16, NewProtocol string) (err error)
GetSpecificPortMappingEntry(NewRemoteHost string, NewExternalPort uint16, NewProtocol string) (NewInternalPort uint16, NewInternalClient string, NewEnabled bool, NewPortMappingDescription string, NewLeaseDuration uint32, err error)
}