mirror of
https://github.com/simon-ding/polaris.git
synced 2026-02-06 23:21:00 +08:00
WIP: stun proxy
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
@@ -93,6 +94,28 @@ func (client *Client) get(endpoint string, opts map[string]string) (*http.Respon
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (cleint *Client) postJson(endpoint string, body any) (*http.Response, error) {
|
||||
var buff bytes.Buffer
|
||||
buff.WriteString("json=")
|
||||
d, err := json.Marshal(body)
|
||||
if err!= nil {
|
||||
return nil, err
|
||||
}
|
||||
buff.Write(d)
|
||||
log.Println(buff.String())
|
||||
req, err := http.NewRequest("POST", cleint.URL+endpoint, &buff)
|
||||
if err!= nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("User-Agent", "go-qbittorrent v0.1")
|
||||
resp, err := cleint.http.Do(req)
|
||||
if err!= nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// post will perform a POST request with no content-type specified
|
||||
func (client *Client) post(endpoint string, opts map[string]string) (*http.Response, error) {
|
||||
// add optional parameters that the user wants
|
||||
@@ -315,8 +338,9 @@ func (client *Client) Preferences() (prefs Preferences, err error) {
|
||||
}
|
||||
|
||||
// SetPreferences of the qbittorrent client
|
||||
func (client *Client) SetPreferences() (prefsSet bool, err error) {
|
||||
resp, err := client.post("api/v2/app/setPreferences", nil)
|
||||
func (client *Client) SetPreferences(m map[string]any) (prefsSet bool, err error) {
|
||||
|
||||
resp, err := client.postJson("api/v2/app/setPreferences", m)
|
||||
return (resp.Status == "200 OK"), err
|
||||
}
|
||||
|
||||
|
||||
31
pkg/nat/cmd/main.go
Normal file
31
pkg/nat/cmd/main.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"polaris/log"
|
||||
"polaris/pkg/nat"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// This is a placeholder for the main function.
|
||||
// The actual implementation will depend on the specific requirements of the application.
|
||||
src, err := net.Listen("tcp", ":8080")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for {
|
||||
conn, err := src.Accept()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
log.Infof("new connection: %+v", conn)
|
||||
dest, err := net.Dial("tcp", "10.0.0.8:8080")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go nat.ReverseProxy(conn, dest)
|
||||
}
|
||||
select {}
|
||||
}
|
||||
67
pkg/nat/reverse_proxy.go
Normal file
67
pkg/nat/reverse_proxy.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package nat
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
)
|
||||
|
||||
func ReverseProxy(sourceConn net.Conn, targetConn net.Conn) {
|
||||
serverClosed := make(chan struct{}, 1)
|
||||
clientClosed := make(chan struct{}, 1)
|
||||
|
||||
go broker(sourceConn, targetConn, clientClosed)
|
||||
go broker(targetConn, sourceConn, serverClosed)
|
||||
|
||||
// wait for one half of the proxy to exit, then trigger a shutdown of the
|
||||
// other half by calling CloseRead(). This will break the read loop in the
|
||||
// broker and allow us to fully close the connection cleanly without a
|
||||
// "use of closed network connection" error.
|
||||
var waitFor chan struct{}
|
||||
select {
|
||||
case <-clientClosed:
|
||||
// the client closed first and any more packets from the server aren't
|
||||
// useful, so we can optionally SetLinger(0) here to recycle the port
|
||||
// faster.
|
||||
waitFor = serverClosed
|
||||
case <-serverClosed:
|
||||
waitFor = clientClosed
|
||||
}
|
||||
|
||||
// Wait for the other connection to close.
|
||||
// This "waitFor" pattern isn't required, but gives us a way to track the
|
||||
// connection and ensure all copies terminate correctly; we can trigger
|
||||
// stats on entry and deferred exit of this function.
|
||||
<-waitFor
|
||||
}
|
||||
|
||||
func pipe(src net.Conn, dest net.Conn) {
|
||||
errChan := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := io.Copy(src, dest)
|
||||
errChan <- err
|
||||
}()
|
||||
go func() {
|
||||
_, err := io.Copy(dest, src)
|
||||
errChan <- err
|
||||
}()
|
||||
<-errChan
|
||||
}
|
||||
|
||||
// This does the actual data transfer.
|
||||
// The broker only closes the Read side.
|
||||
func broker(dst, src net.Conn, srcClosed chan struct{}) {
|
||||
// We can handle errors in a finer-grained manner by inlining io.Copy (it's
|
||||
// simple, and we drop the ReaderFrom or WriterTo checks for
|
||||
// net.Conn->net.Conn transfers, which aren't needed). This would also let
|
||||
// us adjust buffersize.
|
||||
_, err := io.Copy(dst, src)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Copy error: %s", err)
|
||||
}
|
||||
if err := src.Close(); err != nil {
|
||||
log.Printf("Close error: %s", err)
|
||||
}
|
||||
srcClosed <- struct{}{}
|
||||
}
|
||||
169
pkg/nat/stun.go
Normal file
169
pkg/nat/stun.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package nat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"polaris/log"
|
||||
|
||||
"github.com/pion/stun/v3"
|
||||
)
|
||||
|
||||
func getNatIpAndPort() (*stun.XORMappedAddress, error) {
|
||||
|
||||
var xorAddr stun.XORMappedAddress
|
||||
|
||||
for _, server := range getStunServers() {
|
||||
log.Infof("try to connect to stun server: %s", server)
|
||||
u, err := stun.ParseURI("stun:" + server)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// Creating a "connection" to STUN server.
|
||||
c, err := stun.DialURI(u, &stun.DialConfig{})
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// Building binding request with random transaction id.
|
||||
message := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
|
||||
// Sending request to STUN server, waiting for response message.
|
||||
var err1 error
|
||||
if err := c.Do(message, func(res stun.Event) {
|
||||
if res.Error != nil {
|
||||
err1 = res.Error
|
||||
return
|
||||
}
|
||||
log.Infof("stun server %s response: %v", server, res.Message.String())
|
||||
// Decoding XOR-MAPPED-ADDRESS attribute from message.
|
||||
|
||||
if err := xorAddr.GetFrom(res.Message); err != nil {
|
||||
err1 = err
|
||||
return
|
||||
}
|
||||
fmt.Println("your IP is", xorAddr.IP)
|
||||
fmt.Println("your port is", xorAddr.Port)
|
||||
}); err != nil {
|
||||
log.Warnf("stun server %s error: %v", server, err)
|
||||
continue
|
||||
}
|
||||
if err1 != nil {
|
||||
log.Warnf("stun server %s error: %v", server, err1)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return &xorAddr, nil
|
||||
}
|
||||
|
||||
func getStunServers() []string {
|
||||
var servers []string
|
||||
for _, line := range strings.Split(strings.TrimSpace(stunServers1), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
servers = append(servers, line)
|
||||
}
|
||||
return servers
|
||||
}
|
||||
|
||||
// https://github.com/heiher/natmap/issues/18
|
||||
const stunServers1 = `
|
||||
stun.miwifi.com:3478
|
||||
stun.chat.bilibili.com:3478
|
||||
stun.cloudflare.com:3478
|
||||
turn.cloudflare.com:3478
|
||||
fwa.lifesizecloud.com:3478
|
||||
`
|
||||
|
||||
// https://github.com/pradt2/always-online-stun
|
||||
const stunServers = `
|
||||
stun.miwifi.com:3478
|
||||
stun.ukh.de:3478
|
||||
stun.kanojo.de:3478
|
||||
stun.m-online.net:3478
|
||||
stun.nextcloud.com:3478
|
||||
stun.voztovoice.org:3478
|
||||
stun.oncloud7.ch:3478
|
||||
stun.antisip.com:3478
|
||||
stun.bitburger.de:3478
|
||||
stun.acronis.com:3478
|
||||
stun.signalwire.com:3478
|
||||
stun.sonetel.net:3478
|
||||
stun.poetamatusel.org:3478
|
||||
stun.avigora.fr:3478
|
||||
stun.diallog.com:3478
|
||||
stun.nanocosmos.de:3478
|
||||
stun.romaaeterna.nl:3478
|
||||
stun.heeds.eu:3478
|
||||
stun.freeswitch.org:3478
|
||||
stun.engineeredarts.co.uk:3478
|
||||
stun.root-1.de:3478
|
||||
stun.healthtap.com:3478
|
||||
stun.allflac.com:3478
|
||||
stun.vavadating.com:3478
|
||||
stun.godatenow.com:3478
|
||||
stun.mixvoip.com:3478
|
||||
stun.sip.us:3478
|
||||
stun.sipthor.net:3478
|
||||
stun.stochastix.de:3478
|
||||
stun.kaseya.com:3478
|
||||
stun.files.fm:3478
|
||||
stun.meetwife.com:3478
|
||||
stun.myspeciality.com:3478
|
||||
stun.3wayint.com:3478
|
||||
stun.voip.blackberry.com:3478
|
||||
stun.axialys.net:3478
|
||||
stun.bridesbay.com:3478
|
||||
stun.threema.ch:3478
|
||||
stun.siptrunk.com:3478
|
||||
stun.ncic.com:3478
|
||||
stun.1cbit.ru:3478
|
||||
stun.ttmath.org:3478
|
||||
stun.yesdates.com:3478
|
||||
stun.sonetel.com:3478
|
||||
stun.peethultra.be:3478
|
||||
stun.pure-ip.com:3478
|
||||
stun.business-isp.nl:3478
|
||||
stun.ringostat.com:3478
|
||||
stun.imp.ch:3478
|
||||
stun.cope.es:3478
|
||||
stun.baltmannsweiler.de:3478
|
||||
stun.lovense.com:3478
|
||||
stun.frozenmountain.com:3478
|
||||
stun.linuxtrent.it:3478
|
||||
stun.thinkrosystem.com:3478
|
||||
stun.3deluxe.de:3478
|
||||
stun.skydrone.aero:3478
|
||||
stun.ru-brides.com:3478
|
||||
stun.streamnow.ch:3478
|
||||
stun.atagverwarming.nl:3478
|
||||
stun.ipfire.org:3478
|
||||
stun.fmo.de:3478
|
||||
stun.moonlight-stream.org:3478
|
||||
stun.f.haeder.net:3478
|
||||
stun.nextcloud.com:443
|
||||
stun.finsterwalder.com:3478
|
||||
stun.voipia.net:3478
|
||||
stun.zepter.ru:3478
|
||||
stun.sipnet.net:3478
|
||||
stun.hot-chilli.net:3478
|
||||
stun.zentauron.de:3478
|
||||
stun.geesthacht.de:3478
|
||||
stun.annatel.net:3478
|
||||
stun.flashdance.cx:3478
|
||||
stun.voipgate.com:3478
|
||||
stun.genymotion.com:3478
|
||||
stun.graftlab.com:3478
|
||||
stun.fitauto.ru:3478
|
||||
stun.telnyx.com:3478
|
||||
stun.verbo.be:3478
|
||||
stun.dcalling.de:3478
|
||||
stun.lleida.net:3478
|
||||
stun.romancecompass.com:3478
|
||||
stun.siplogin.de:3478
|
||||
stun.bethesda.net:3478
|
||||
stun.alpirsbacher.de:3478
|
||||
stun.uabrides.com:3478
|
||||
stun.technosens.fr:3478
|
||||
stun.radiojar.com:3478
|
||||
`
|
||||
14
pkg/nat/stun_test.go
Normal file
14
pkg/nat/stun_test.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package nat
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestStun1(t *testing.T) {
|
||||
// s,err := getNatIpAndPort()
|
||||
// if err != nil {
|
||||
// t.Logf("get nat ip and port error: %v", err)
|
||||
// t.Fail()
|
||||
// }
|
||||
|
||||
//NatTraversal()
|
||||
t.Logf("nat ip: ")
|
||||
}
|
||||
197
pkg/nat/traversal.go
Normal file
197
pkg/nat/traversal.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package nat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/pion/stun/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
udp = "udp4"
|
||||
pingMsg = "ping"
|
||||
pongMsg = "pong"
|
||||
timeoutMillis = 500
|
||||
)
|
||||
|
||||
type natTraversal struct {
|
||||
peerAddr *net.UDPAddr
|
||||
cancel chan struct{}
|
||||
port <-chan int
|
||||
}
|
||||
|
||||
func (s *natTraversal) Port() int {
|
||||
return <-s.port
|
||||
}
|
||||
|
||||
func (s *natTraversal) Cancel() {
|
||||
s.cancel <- struct{}{}
|
||||
}
|
||||
|
||||
func NatTraversal(targetAddr string) (*natTraversal, error) { //nolint:gocognit,cyclop
|
||||
|
||||
srvAddr, err := net.ResolveUDPAddr(udp, getStunServers()[0])
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to resolve server addr: %s", err)
|
||||
}
|
||||
|
||||
conn, err := net.ListenUDP(udp, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listen: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Listening on %s", conn.LocalAddr())
|
||||
|
||||
peerAddr, err := net.ResolveUDPAddr(udp, targetAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolve peeraddr: %w", err)
|
||||
}
|
||||
err = sendBindingRequest(conn, srvAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("send binding request: %w", err)
|
||||
}
|
||||
nt := &natTraversal{
|
||||
peerAddr: peerAddr,
|
||||
cancel: make(chan struct{}),
|
||||
port: make(chan int),
|
||||
}
|
||||
go func() {
|
||||
err := doTraversal(conn, peerAddr, nt.cancel)
|
||||
if err != nil {
|
||||
log.Println("nat traversal error:", err)
|
||||
}
|
||||
}()
|
||||
return nt, nil
|
||||
|
||||
}
|
||||
|
||||
func doTraversal(conn *net.UDPConn, peerAddr *net.UDPAddr, quit <-chan struct{}) error {
|
||||
defer func() {
|
||||
_ = conn.Close()
|
||||
}()
|
||||
|
||||
var publicAddr stun.XORMappedAddress
|
||||
|
||||
messageChan := listen(conn)
|
||||
//var peerAddrChan <-chan string
|
||||
|
||||
keepalive := time.Tick(timeoutMillis * time.Millisecond)
|
||||
keepaliveMsg := pingMsg
|
||||
|
||||
gotPong := false
|
||||
sentPong := false
|
||||
|
||||
for {
|
||||
select {
|
||||
case message, ok := <-messageChan:
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case string(message) == pingMsg:
|
||||
keepaliveMsg = pongMsg
|
||||
|
||||
case string(message) == pongMsg:
|
||||
if !gotPong {
|
||||
log.Println("Received pong message.")
|
||||
}
|
||||
|
||||
// One client may skip sending ping if it receives
|
||||
// a ping message before knowning the peer address.
|
||||
keepaliveMsg = pongMsg
|
||||
|
||||
gotPong = true
|
||||
|
||||
case stun.IsMessage(message):
|
||||
m := new(stun.Message)
|
||||
m.Raw = message
|
||||
decErr := m.Decode()
|
||||
if decErr != nil {
|
||||
log.Println("decode:", decErr)
|
||||
|
||||
break
|
||||
}
|
||||
var xorAddr stun.XORMappedAddress
|
||||
if getErr := xorAddr.GetFrom(m); getErr != nil {
|
||||
log.Println("getFrom:", getErr)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if publicAddr.String() != xorAddr.String() {
|
||||
log.Printf("My public address: %s\n", xorAddr)
|
||||
publicAddr = xorAddr
|
||||
|
||||
//peerAddrChan = getPeerAddr()
|
||||
}
|
||||
|
||||
default:
|
||||
send(message, conn, peerAddr)
|
||||
}
|
||||
|
||||
case <-keepalive:
|
||||
// Keep NAT binding alive using STUN server or the peer once it's known
|
||||
err := sendStr(keepaliveMsg, conn, peerAddr)
|
||||
if keepaliveMsg == pongMsg {
|
||||
sentPong = true
|
||||
}
|
||||
_ = sentPong
|
||||
|
||||
if err != nil {
|
||||
log.Panicln("keepalive:", err)
|
||||
}
|
||||
|
||||
case <-quit:
|
||||
_ = conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func listen(conn *net.UDPConn) <-chan []byte {
|
||||
messages := make(chan []byte)
|
||||
go func() {
|
||||
for {
|
||||
buf := make([]byte, 10240)
|
||||
|
||||
n, _, err := conn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
close(messages)
|
||||
|
||||
return
|
||||
}
|
||||
buf = buf[:n]
|
||||
|
||||
messages <- buf
|
||||
}
|
||||
}()
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
func sendBindingRequest(conn *net.UDPConn, addr *net.UDPAddr) error {
|
||||
m := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
|
||||
|
||||
err := send(m.Raw, conn, addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("binding: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func send(msg []byte, conn *net.UDPConn, addr *net.UDPAddr) error {
|
||||
_, err := conn.WriteToUDP(msg, addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("send: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sendStr(msg string, conn *net.UDPConn, addr *net.UDPAddr) error {
|
||||
return send([]byte(msg), conn, addr)
|
||||
}
|
||||
@@ -61,6 +61,23 @@ func (c *Client) GetAll() ([]pkg.Torrent, error) {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetListenPort() (int, error) {
|
||||
pref, err := c.c.Preferences()
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "get preferences")
|
||||
}
|
||||
|
||||
return pref.ListenPort, nil
|
||||
}
|
||||
|
||||
func (c *Client) SetListenPort(port int) error {
|
||||
ok, err := c.c.SetPreferences(map[string]any{"listen_port": port})
|
||||
if !ok || err != nil {
|
||||
return errors.Wrap(err, "set preferences")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Download(link, hash, dir string) (pkg.Torrent, error) {
|
||||
err := c.c.DownloadLinks([]string{link}, qbt.DownloadOptions{Savepath: &dir, Category: &c.category})
|
||||
if err != nil {
|
||||
|
||||
@@ -11,10 +11,17 @@ func Test1(t *testing.T) {
|
||||
log.Errorf("new client error: %v", err)
|
||||
t.Fail()
|
||||
}
|
||||
all, err := c.GetAll()
|
||||
for _, t := range all {
|
||||
name, _ := t.Name()
|
||||
log.Infof("torrent: %+v", name)
|
||||
log.Infof("new client success: %v", c)
|
||||
port, err := c.GetListenPort()
|
||||
if err != nil {
|
||||
log.Errorf("get listen port error: %v", err)
|
||||
t.Fail()
|
||||
} else {
|
||||
log.Infof("listen port: %d", port)
|
||||
err := c.SetListenPort(port + 1)
|
||||
if err!= nil {
|
||||
log.Errorf("set listen port error: %v", err)
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user