package net import ( "b612.me/apps/b612/netforward" "b612.me/starlog" "b612.me/starmap" "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 Type string } 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" } if n.Type == "" { n.Type = "tcp" } for _, v := range reqs { var req = NatThrough{ Forward: netforward.NetForward{ LocalAddr: "0.0.0.0", DialTimeout: time.Second * 5, UDPTimeout: time.Second * 30, KeepAlivePeriod: n.KeepAlivePeriod, KeepAliveIdel: n.KeepAliveIdel, KeepAliveCount: n.KeepAliveCount, UsingKeepAlive: true, EnableTCP: true, EnableUDP: true, }, Type: n.Type, 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 { starlog.Errorf("Failed to run natThrough: %v\n", err) } 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 } starlog.Infof("NatThrough Type: %s\n", c.Type) 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 { starlog.Errorf("Failed to run keepalive: %v\n", err) 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() { 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 } } 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" { _, 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) } else { 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() } } 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 } if c.Forward.UdpListener() == nil { time.Sleep(time.Second * 5) continue } c.keepaliveConn = c.Forward.UdpListener() for { _, err = c.Forward.UdpListener().WriteTo([]byte("b612 udp nat through"), rmtUdpAddr) if err != nil { c.keepaliveConn.Close() starlog.Warningf("Failed to keepalive remote: %v\n", err) time.Sleep(time.Second * 30) break } starlog.Infof("UDP Keepalive Ok! %v\n", rmtUdpAddr.String()) 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" { 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 } 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 } 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) }