Refactor OpenVPN and OpenConnect endpoints

This commit is contained in:
世界
2026-07-18 09:40:17 +08:00
parent 60f3012444
commit a4367df680
25 changed files with 758 additions and 182 deletions
+261
View File
@@ -0,0 +1,261 @@
package main
import (
"context"
"net"
"net/netip"
"sync/atomic"
"testing"
"time"
openconnecttransport "github.com/sagernet/sing-box/transport/openconnect"
openvpntransport "github.com/sagernet/sing-box/transport/openvpn"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/gtcpip/header"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/stretchr/testify/require"
)
type endpointUDPNATDevice struct {
start func() error
writeInboundBuffers func([]*buf.Buffer) error
setPacketWriter func(func([]*buf.Buffer) error)
close func() error
}
type endpointUDPNATPacket struct {
session *endpointUDPNATSession
destination M.Socksaddr
payload []byte
}
type endpointUDPNATSession struct {
id uint64
conn N.PacketConn
closed chan struct{}
}
type endpointUDPNATHandler struct {
nextSessionID atomic.Uint64
packets chan endpointUDPNATPacket
}
func (h *endpointUDPNATHandler) JudgeFlow(uint8, netip.AddrPort, netip.AddrPort, []byte) tun.FlowVerdict {
return tun.FlowVerdict{Action: tun.ActionAccept}
}
func (h *endpointUDPNATHandler) NewConnectionEx(_ context.Context, conn net.Conn, _ M.Socksaddr, _ M.Socksaddr, onClose N.CloseHandlerFunc) {
err := conn.Close()
if onClose != nil {
onClose(err)
}
}
func (h *endpointUDPNATHandler) NewPacketConnectionEx(_ context.Context, conn N.PacketConn, _ M.Socksaddr, _ M.Socksaddr, onClose N.CloseHandlerFunc) {
session := &endpointUDPNATSession{
id: h.nextSessionID.Add(1),
conn: conn,
closed: make(chan struct{}),
}
go func() {
defer close(session.closed)
for {
packetBuffer := buf.NewPacket()
destination, err := conn.ReadPacket(packetBuffer)
if err != nil {
packetBuffer.Release()
if onClose != nil {
onClose(err)
}
return
}
payload := append([]byte(nil), packetBuffer.Bytes()...)
packetBuffer.Release()
h.packets <- endpointUDPNATPacket{
session: session,
destination: destination,
payload: payload,
}
}
}()
}
func TestOpenVPNEndpointUDPNATDataPlane(t *testing.T) {
testEndpointUDPNATDataPlane(t, func(ctx context.Context, handler tun.Handler) (endpointUDPNATDevice, error) {
device, err := openvpntransport.NewDevice(openvpntransport.DeviceOptions{
Context: ctx,
Logger: logger.NOP(),
Handler: handler,
UDPTimeout: time.Minute,
UDPMapping: tun.NATMappingAddressAndPortDependent,
UDPFiltering: tun.NATFilteringAddressAndPortDependent,
UDPNATMax: 1,
MTU: 1500,
Configuration: openvpntransport.Configuration{
MTU: 1500,
Address: []netip.Prefix{netip.MustParsePrefix("10.8.0.1/24")},
},
})
if err != nil {
return endpointUDPNATDevice{}, err
}
return endpointUDPNATDevice{
start: device.Start,
writeInboundBuffers: device.WriteInboundBuffers,
setPacketWriter: func(writer func([]*buf.Buffer) error) {
device.SetPacketWriter(openvpntransport.PacketWriter(writer))
},
close: device.Close,
}, nil
})
}
func TestOpenConnectEndpointUDPNATDataPlane(t *testing.T) {
testEndpointUDPNATDataPlane(t, func(ctx context.Context, handler tun.Handler) (endpointUDPNATDevice, error) {
device, err := openconnecttransport.NewDevice(openconnecttransport.DeviceOptions{
Context: ctx,
Logger: logger.NOP(),
Handler: handler,
UDPTimeout: time.Minute,
UDPMapping: tun.NATMappingAddressAndPortDependent,
UDPFiltering: tun.NATFilteringAddressAndPortDependent,
UDPNATMax: 1,
MTU: 1500,
Configuration: openconnecttransport.Configuration{
MTU: 1500,
Addresses: []netip.Prefix{netip.MustParsePrefix("10.8.0.1/24")},
},
})
if err != nil {
return endpointUDPNATDevice{}, err
}
return endpointUDPNATDevice{
start: device.Start,
writeInboundBuffers: device.WriteInboundBuffers,
setPacketWriter: func(writer func([]*buf.Buffer) error) {
device.SetPacketWriter(openconnecttransport.PacketWriter(writer))
},
close: device.Close,
}, nil
})
}
func testEndpointUDPNATDataPlane(t *testing.T, newDevice func(context.Context, tun.Handler) (endpointUDPNATDevice, error)) {
t.Helper()
if !tun.WithGVisor {
t.Skip("requires gVisor")
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
handler := &endpointUDPNATHandler{packets: make(chan endpointUDPNATPacket, 4)}
device, err := newDevice(ctx, handler)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, device.close())
})
outboundPackets := make(chan []byte, 4)
device.setPacketWriter(func(packetBuffers []*buf.Buffer) error {
for _, packetBuffer := range packetBuffers {
payload, isUDP := endpointUDPPayload(packetBuffer.Bytes())
packetBuffer.Release()
if isUDP {
outboundPackets <- payload
}
}
return nil
})
source := netip.MustParseAddrPort("10.8.0.2:40000")
firstDestination := netip.MustParseAddrPort("192.0.2.1:5001")
secondDestination := netip.MustParseAddrPort("192.0.2.1:5002")
writeEndpointUDPPacket(t, device, source, firstDestination, []byte("before-start"))
require.NoError(t, device.start())
writeEndpointUDPPacket(t, device, source, firstDestination, []byte("request-one"))
firstPacket := waitEndpointUDPNATPacket(t, handler.packets)
require.Equal(t, M.SocksaddrFromNetIP(firstDestination), firstPacket.destination)
require.Equal(t, []byte("request-one"), firstPacket.payload)
require.NoError(t, firstPacket.session.conn.WritePacket(buf.As([]byte("blocked")), M.SocksaddrFromNetIP(secondDestination)))
require.NoError(t, firstPacket.session.conn.WritePacket(buf.As([]byte("allowed-one")), M.SocksaddrFromNetIP(firstDestination)))
require.Equal(t, []byte("allowed-one"), waitEndpointUDPResponse(t, outboundPackets))
writeEndpointUDPPacket(t, device, source, secondDestination, []byte("request-two"))
secondPacket := waitEndpointUDPNATPacket(t, handler.packets)
require.Equal(t, M.SocksaddrFromNetIP(secondDestination), secondPacket.destination)
require.Equal(t, []byte("request-two"), secondPacket.payload)
require.NotEqual(t, firstPacket.session.id, secondPacket.session.id)
select {
case <-firstPacket.session.closed:
case <-time.After(5 * time.Second):
t.Fatal("first UDP NAT session was not evicted at max size")
}
require.NoError(t, secondPacket.session.conn.WritePacket(buf.As([]byte("allowed-two")), M.SocksaddrFromNetIP(secondDestination)))
require.Equal(t, []byte("allowed-two"), waitEndpointUDPResponse(t, outboundPackets))
}
func writeEndpointUDPPacket(t *testing.T, device endpointUDPNATDevice, source netip.AddrPort, destination netip.AddrPort, payload []byte) {
t.Helper()
packet := make([]byte, header.IPv4MinimumSize+header.UDPMinimumSize+len(payload))
ipHeader := header.IPv4(packet)
ipHeader.Encode(&header.IPv4Fields{
TotalLength: uint16(len(packet)),
TTL: 64,
Protocol: uint8(header.UDPProtocolNumber),
SrcAddr: source.Addr(),
DstAddr: destination.Addr(),
})
ipHeader.SetChecksum(^ipHeader.CalculateChecksum())
udpHeader := header.UDP(packet[header.IPv4MinimumSize:])
udpHeader.Encode(&header.UDPFields{
SrcPort: source.Port(),
DstPort: destination.Port(),
Length: uint16(header.UDPMinimumSize + len(payload)),
})
copy(udpHeader.Payload(), payload)
packetBuffer := buf.As(packet)
require.NoError(t, device.writeInboundBuffers([]*buf.Buffer{packetBuffer}))
packetBuffer.Release()
}
func endpointUDPPayload(packet []byte) ([]byte, bool) {
if len(packet) < header.IPv4MinimumSize {
return nil, false
}
ipHeader := header.IPv4(packet)
if !ipHeader.IsValid(len(packet)) || ipHeader.Protocol() != uint8(header.UDPProtocolNumber) {
return nil, false
}
udpPayload := ipHeader.Payload()
if len(udpPayload) < header.UDPMinimumSize {
return nil, false
}
udpHeader := header.UDP(udpPayload)
return append([]byte(nil), udpHeader.Payload()...), true
}
func waitEndpointUDPNATPacket(t *testing.T, packets <-chan endpointUDPNATPacket) endpointUDPNATPacket {
t.Helper()
select {
case packet := <-packets:
return packet
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for UDP NAT packet")
return endpointUDPNATPacket{}
}
}
func waitEndpointUDPResponse(t *testing.T, packets <-chan []byte) []byte {
t.Helper()
select {
case packet := <-packets:
return packet
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for UDP response")
return nil
}
}
+5 -5
View File
@@ -12,10 +12,11 @@ require (
github.com/gofrs/uuid/v5 v5.4.0
github.com/opencontainers/image-spec v1.1.0
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4
github.com/sagernet/sing v0.8.12-0.20260717023913-84ab32b56cb8
github.com/sagernet/sing v0.8.12-0.20260717153536-4f1ed45a99a5
github.com/sagernet/sing-quic v0.6.4-0.20260709034545-e23afe1172dc
github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.1
github.com/sagernet/sing-tun v0.8.12-0.20260717024008-39eed1f6361d
github.com/spyzhov/ajson v0.9.4
github.com/stretchr/testify v1.11.1
go.uber.org/goleak v1.3.0
@@ -152,15 +153,14 @@ require (
github.com/sagernet/nftables v0.3.0-mod.4 // indirect
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 // indirect
github.com/sagernet/sing-mux v0.3.5 // indirect
github.com/sagernet/sing-openconnect v0.0.0-20260717061548-458a8732933e // indirect
github.com/sagernet/sing-openvpn v0.0.0-20260717055507-7e569eca5e4d // indirect
github.com/sagernet/sing-openconnect v0.0.0-20260717081856-cf2c71a71aba // indirect
github.com/sagernet/sing-openvpn v0.0.0-20260718013246-3cd8a7b83247 // indirect
github.com/sagernet/sing-shadowtls v0.2.1 // indirect
github.com/sagernet/sing-snell v0.0.0-20260710094516-a4e97ee24beb // indirect
github.com/sagernet/sing-tun v0.8.12-0.20260717024008-39eed1f6361d // indirect
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb // indirect
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 // indirect
github.com/sagernet/smux v1.5.50-sing-box-mod.1 // indirect
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260717024045-1edfbb9ee544 // indirect
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260717155615-b353b93d194a // indirect
github.com/sagernet/wireguard-go v0.0.5-0.20260717024847-6f5e8b1947ae // indirect
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect
github.com/smallstep/pkcs7 v0.1.1 // indirect
+8 -8
View File
@@ -298,16 +298,16 @@ github.com/sagernet/nftables v0.3.0-mod.4 h1:vnOtcDYeSXv2e5RoRuGH0lrpttQFJ8iC4IC
github.com/sagernet/nftables v0.3.0-mod.4/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o=
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4=
github.com/sagernet/sing v0.8.12-0.20260717023913-84ab32b56cb8 h1:dyRIj+MZ2rc9JVzJoG04jxu+MpvHrLIZLJr0QjNAMGg=
github.com/sagernet/sing v0.8.12-0.20260717023913-84ab32b56cb8/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA=
github.com/sagernet/sing v0.8.12-0.20260717153536-4f1ed45a99a5 h1:WyL7xI7h+mNF6we/arqVNDEB+0iK2SwJRjim8rGydDM=
github.com/sagernet/sing v0.8.12-0.20260717153536-4f1ed45a99a5/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA=
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3 h1:3y6++yIa8XlDhxPkpR4p+7RUHVY2KTP9CPIGnWmOlO8=
github.com/sagernet/sing-cloudflared v0.1.3-0.20260706062323-d9787e794aa3/go.mod h1:XEqEDYRCAYLaoPjZ1ifVWJg5iWAJHL2gOAXe/PM28Cg=
github.com/sagernet/sing-mux v0.3.5 h1:RHnhVEc+SFqkrK4xMygYjDwwLhzp2Bj3lztSukONfhI=
github.com/sagernet/sing-mux v0.3.5/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
github.com/sagernet/sing-openconnect v0.0.0-20260717061548-458a8732933e h1:Kgcf16uKnxBNJMsR8MaWlORtOV2/qi+6yH830+b7Yfc=
github.com/sagernet/sing-openconnect v0.0.0-20260717061548-458a8732933e/go.mod h1:EIzh5HtImfQJxPKXFwS9lyMnmMy4aCQCx7ntQ4u41Gs=
github.com/sagernet/sing-openvpn v0.0.0-20260717055507-7e569eca5e4d h1:KGvybsWqE+Qkd9Ns2AzrrBSyNfbBJ7IZZqgj+oWa6SM=
github.com/sagernet/sing-openvpn v0.0.0-20260717055507-7e569eca5e4d/go.mod h1:CmTGnS5ijVSqFQV1dTq4WvFLUoz7bk9xasBPsX8NcYo=
github.com/sagernet/sing-openconnect v0.0.0-20260717081856-cf2c71a71aba h1:S87Ej/jFssn0qhPF1ExF0YIV0USfGZF2If6kSjGLPt8=
github.com/sagernet/sing-openconnect v0.0.0-20260717081856-cf2c71a71aba/go.mod h1:EIzh5HtImfQJxPKXFwS9lyMnmMy4aCQCx7ntQ4u41Gs=
github.com/sagernet/sing-openvpn v0.0.0-20260718013246-3cd8a7b83247 h1:IfZqHohaWz13eqc6SAUHkmP9xhMpMBDIShl7OtgRd5Y=
github.com/sagernet/sing-openvpn v0.0.0-20260718013246-3cd8a7b83247/go.mod h1:CmTGnS5ijVSqFQV1dTq4WvFLUoz7bk9xasBPsX8NcYo=
github.com/sagernet/sing-quic v0.6.4-0.20260709034545-e23afe1172dc h1:zdc0fj4JdAdgAmQIoh7ZF+B/wPTEF2X75lYDqTmvlaw=
github.com/sagernet/sing-quic v0.6.4-0.20260709034545-e23afe1172dc/go.mod h1:9k+dzGsWMttUGldBzq3dU792YHXzW6NgfbOGltnXq+0=
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
@@ -326,8 +326,8 @@ github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkV
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY=
github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478=
github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8=
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260717024045-1edfbb9ee544 h1:j2tab0dGHutfclhwZxrkSDMXwGXtozIo5BV4DgwS+1Q=
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260717024045-1edfbb9ee544/go.mod h1:p8Ms8FbGlwQJyHb862XmdShTS50fFJ8C71VdO6xvWyk=
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260717155615-b353b93d194a h1:mORXldIuzgU8Bk6n9KpOmLv1rV7Ta8us+lZ9JSv2eBw=
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7.0.20260717155615-b353b93d194a/go.mod h1:p8Ms8FbGlwQJyHb862XmdShTS50fFJ8C71VdO6xvWyk=
github.com/sagernet/wireguard-go v0.0.5-0.20260717024847-6f5e8b1947ae h1:GmxlXWnRmeNfPE1tWXRZIFgKJd5BH5okoDHKZkkI5bw=
github.com/sagernet/wireguard-go v0.0.5-0.20260717024847-6f5e8b1947ae/go.mod h1:hEqi4y5czEg6LYtX2Bpjg+lV0b/J1n+5rA885Z66Mx0=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
+44 -5
View File
@@ -21,6 +21,7 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json/badoption"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
@@ -125,6 +126,8 @@ func TestOpenConnectDockerInterop(t *testing.T) {
require.Nil(subtest, status.AuthForm)
err := exchangeOpenConnectTCPEcho(endpoint, 256*1024, 30*time.Second)
require.NoError(subtest, err)
err = exchangeOpenConnectUDPEcho(endpoint, 1400, 30*time.Second)
require.NoError(subtest, err)
})
t.Run("interactive_password_auth", func(subtest *testing.T) {
@@ -174,11 +177,15 @@ func TestOpenConnectDockerInterop(t *testing.T) {
func openConnectInstanceOptions(server string, certificateAuthorityPath string, username string, password string) option.Options {
endpointOptions := option.OpenConnectEndpointOptions{
Server: server,
Flavor: "anyconnect",
Username: username,
Password: password,
NoUDP: true,
Server: server,
Flavor: "anyconnect",
Username: username,
Password: password,
NoUDP: true,
UDPTimeout: badoption.Duration(time.Minute),
UDPMapping: option.UDPNATBehaviorAddressDependent,
UDPFiltering: option.UDPNATBehaviorAddressAndPortDependent,
UDPNATMax: 128,
TLS: option.OpenConnectTLSOptions{
CertificateAuthorityPath: certificateAuthorityPath,
},
@@ -318,6 +325,38 @@ func exchangeOpenConnectTCPEcho(endpoint adapter.OpenConnectEndpoint, payloadSiz
return nil
}
func exchangeOpenConnectUDPEcho(endpoint adapter.OpenConnectEndpoint, payloadSize int, timeout time.Duration) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
conn, err := endpoint.DialContext(ctx, N.NetworkUDP, M.ParseSocksaddrHostPort(openConnectTunnelAddress, openConnectEchoPort))
if err != nil {
return E.Cause(err, "dial ocserv tunnel UDP echo")
}
defer conn.Close()
err = conn.SetDeadline(time.Now().Add(timeout))
if err != nil {
return E.Cause(err, "set ocserv tunnel UDP echo deadline")
}
payload := make([]byte, payloadSize)
_, err = rand.Read(payload)
if err != nil {
return E.Cause(err, "generate ocserv tunnel UDP echo payload")
}
_, err = conn.Write(payload)
if err != nil {
return E.Cause(err, "write ocserv tunnel UDP echo payload")
}
response := make([]byte, payloadSize+1)
responseLength, err := conn.Read(response)
if err != nil {
return E.Cause(err, "read ocserv tunnel UDP echo payload")
}
if !bytes.Equal(response[:responseLength], payload) {
return E.New("ocserv tunnel UDP echo payload mismatch")
}
return nil
}
func waitForOpenConnectTCPEcho(t *testing.T, endpoint adapter.OpenConnectEndpoint, timeout time.Duration) {
t.Helper()
deadline := time.Now().Add(timeout)
+13 -3
View File
@@ -150,10 +150,16 @@ func runOpenVPNSelfToSelf(t *testing.T, testCase openVPNSelfCase) {
ClientCertificatePath: certificates.caPath,
},
}
serverOptions.UDPMapping = option.UDPNATBehaviorAddressDependent
serverOptions.UDPFiltering = option.UDPNATBehaviorAddressAndPortDependent
serverOptions.UDPNATMax = 128
clientOptions := newOpenVPNTLSClientOptions(testCase.protocol, openVPNPort, certificates.caPath, certificates.clientCertPath, certificates.clientKeyPath)
clientOptions.UDPMapping = option.UDPNATBehaviorAddressDependent
clientOptions.UDPFiltering = option.UDPNATBehaviorAddressAndPortDependent
clientOptions.UDPNATMax = 128
if testCase.tlsCrypt {
tlsCryptKeyPath := writeOpenVPNStaticKeyFile(t, createOpenVPNStaticKey(t))
serverOptions.TLS.ControlWrap = &option.OpenVPNControlWrapOptions{
serverOptions.TLS.ControlWrap = &option.OpenVPNInboundControlWrapOptions{
Type: "tls_crypt",
KeyPath: tlsCryptKeyPath,
}
@@ -538,8 +544,12 @@ func TestOpenVPNClientReconnectSelfToSelf(t *testing.T) {
KeyPath: certificates.serverKeyPath,
ClientCertificatePath: certificates.caPath,
},
KeepaliveInterval: badoption.Duration(time.Second),
KeepaliveTimeout: badoption.Duration(2 * time.Second),
PingInterval: badoption.Duration(time.Second),
PingRestart: badoption.Duration(4 * time.Second),
Push: &option.OpenVPNPushOptions{
PingInterval: badoption.Duration(time.Second),
PingRestart: badoption.Duration(2 * time.Second),
},
Users: []auth.User{
{
Username: openVPNTLSUsername,
+15 -5
View File
@@ -12,11 +12,21 @@ def echo(connection):
connection.sendall(data)
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listener.bind(("0.0.0.0", 18080))
listener.listen()
def echo_udp(connection):
while True:
data, address = connection.recvfrom(65536)
connection.sendto(data, address)
udp_listener = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_listener.bind(("0.0.0.0", 18080))
threading.Thread(target=echo_udp, args=(udp_listener,), daemon=True).start()
tcp_listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tcp_listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
tcp_listener.bind(("0.0.0.0", 18080))
tcp_listener.listen()
print("openconnect echo ready", flush=True)
while True:
accepted, _ = listener.accept()
accepted, _ = tcp_listener.accept()
threading.Thread(target=echo, args=(accepted,), daemon=True).start()