WIP: stun proxy

This commit is contained in:
Simon Ding
2025-05-07 18:16:10 +08:00
parent 5375f66018
commit 9719c6a7c9
10 changed files with 557 additions and 57 deletions

View File

@@ -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
View 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
View 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
View 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
View 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
View 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)
}

View File

@@ -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 {

View File

@@ -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()
}
}
}