Bladeren bron

Support udp associate command

Hua-Au-Yeung 3 jaren geleden
bovenliggende
commit
0e122981cb
5 gewijzigde bestanden met toevoegingen van 129 en 6 verwijderingen
  1. 1 1
      README.md
  2. 27 0
      src/lib/command.ts
  3. 7 4
      src/lib/server.ts
  4. 93 0
      src/lib/udpassociate.ts
  5. 1 1
      src/server_test.ts

+ 1 - 1
README.md

@@ -11,9 +11,9 @@ $ npm build
 - [x] Support IPv6
 - [x] Support Connect command
 - [x] Support DNS query cache
+- [x] Support Udp Associate
 - [ ] Support Username/Password authentication
 - [ ] Support Bind command
-- [ ] Support Udp Associate
 - [ ] Socks5 client
 
 

+ 27 - 0
src/lib/command.ts

@@ -85,7 +85,34 @@ export class Command {
     }
 }
 
+export class UdpAssociateCommand {
+    public static parseCommand(chunk: Buffer): [boolean, AddressType, string, number, Buffer] {
+        // TODO check if data is valid
+        const rsv: number = chunk.readUInt16BE(0);
+        const frag: number = chunk.readUint8(2);
+        const addressType: AddressType = chunk.readUint8(3) as AddressType;
+        const [host, port] = parseAddress(chunk);
+        const dataLength = chunk.length - 6 -
+            (addressType == AddressType.IPv4 ? 4 : (AddressType.IPv6 ? 16 : host.length));
+        const dataStartIndex: number = addressType == AddressType.IPv4 ? 10 : (AddressType.IPv6 ? 22 : (6 + host.length));
 
+        let data = Buffer.alloc(dataLength);
+        chunk.copy(data, 0, dataStartIndex);
+
+        return [true, addressType, host, port, data];
+    }
+
+    public static generateCommandReplyHeader(addressType: AddressType, address: string, port: number): Buffer {
+        const replyBufferLength = 6
+            + (addressType == AddressType.IPv4 ? 4 : (AddressType.IPv6 ? 16 : address.length));
+        let reply = Buffer.alloc(replyBufferLength);
+        reply.writeUInt16BE(0, 0);
+        reply.writeUint8(0, 2);
+        reply.writeUint8(addressType, 3);
+        const nextOffset: number = host2Buffer(reply, address);
+        reply.writeUInt16BE(port, nextOffset);
+
+        return reply;
     }
 }
 

+ 7 - 4
src/lib/server.ts

@@ -3,13 +3,13 @@ import {inspect} from "util";
 import {DnsCache} from "./dnscache.js";
 import {Command, CommandReply} from './command.js'
 import {AddressType, AuthMethodType, ClientSocketState, CommandReplyType, CommandType} from "./constants.js";
+import {UdpAssociate} from './udpassociate.js';
 
-export class Socks5Server/* extends EventEmitter*/{
+export class Socks5Server {
     private tcpServer: net.Server;
     private acceptAuthMethod: AuthMethodType;
 
     constructor(port: number, hostname: string) {
-        /*super();*/
         this.acceptAuthMethod = AuthMethodType.NoAuth; // only NoAuth
 
         this.tcpServer = net.createServer((clientSocket:net.Socket) => {
@@ -156,9 +156,12 @@ export class Socks5Server/* extends EventEmitter*/{
                     });
 
                     break;
-                case CommandType.Bind:
-                // TODO
                 case CommandType.UdpAssociate:
+                    console.log(`Udp Associate command received`);
+                    const udpAssociate = new UdpAssociate(socket);
+                    udpAssociate.generateLocalListener();
+                    break;
+                case CommandType.Bind:
                 // TODO
                 default:
                     commandReply = new CommandReply(

+ 93 - 0
src/lib/udpassociate.ts

@@ -0,0 +1,93 @@
+import dgram from 'dgram';
+import * as net from 'net';
+import {inspect} from "util";
+import {AddressType, CommandReplyType} from "./constants.js";
+import {CommandReply, UdpAssociateCommand} from './command.js'
+import {DnsCache} from "./dnscache.js";
+
+// TODO local udp socks5 timeout
+
+export class UdpAssociate {
+    // @ts-ignore
+    private localListener: dgram.Socket;
+    private mainSocketAddressInfo: {} | net.AddressInfo;
+    // @ts-ignore
+    private localListenerAddressType: AddressType;
+    public localListenerAddress: string = '';
+    public localListenerPort: number = 0;
+    constructor(
+        private mainSocket: net.Socket
+    ) {
+        this.mainSocketAddressInfo = this.mainSocket.address();
+    }
+
+    public generateLocalListener():void {
+        // @ts-ignore
+        this.localListener = this.mainSocketAddressInfo.family == 'IPv4' ?
+            dgram.createSocket("udp4") : dgram.createSocket("udp6");
+        // @ts-ignore
+        this.localListenerAddressType = this.mainSocketAddressInfo.family == 'IPv4' ?
+            AddressType.IPv4 : AddressType.IPv6;
+
+        this.localListener.on('listening', () => {
+            this.localListenerAddress = this.localListener.address().address;
+            this.localListenerPort = this.localListener.address().port;
+
+            const commandReply: CommandReply = new CommandReply(
+                CommandReplyType.Succeeded,
+                this.localListenerAddressType, this.localListenerAddress, this.localListenerPort
+            );
+            const replyBuffer: Buffer = commandReply.generateBuffer();
+            this.mainSocket.write(replyBuffer);
+        });
+        // TODO Check if client is valid
+        this.localListener.on('message', (msg, clientAddressInfo) => {
+            console.log(`UdpAssociate received: ${inspect(msg)} `);
+            // console.log(inspect(UdpAssociateCommand.parseCommand(msg)));
+            const [status, addressType, host, port, data] = UdpAssociateCommand.parseCommand(msg);
+            // TODO status == false
+            const remoteHost = addressType == AddressType.Domain ? DnsCache.dnsQuery(host) : host;
+            const remotePort = port;
+
+            const udpType: string = addressType == AddressType.IPv4 ? 'udp4' : 'udp6';
+            // @ts-ignore
+            const remoteSocket = dgram.createSocket(udpType);
+            remoteSocket.on('message', (msg, RemoteAddressInfo) => {
+                const udpAssociateReplyHeader = UdpAssociateCommand.generateCommandReplyHeader(addressType, host, port);
+                console.log(`Udp Associate Reply header: ${inspect(udpAssociateReplyHeader)}`);
+                // console.log(`Received Remote: ${inspect(msg)}`);
+                this.localListener.send(
+                    Buffer.concat([udpAssociateReplyHeader, msg]),
+                    clientAddressInfo.port, clientAddressInfo.address,
+                    (error) => {
+                        // TODO
+                    });
+            });
+
+            remoteSocket.on('close', () => {
+                this.mainSocket.end();
+                this.localListener.close();
+            });
+
+            remoteSocket.on('error', () => {
+               this.mainSocket.end();
+               this.localListener.close();
+            });
+
+            remoteSocket.send(data, remotePort, remoteHost, (error) => {
+                if (error) remoteSocket.close();
+            });
+        });
+
+        this.localListener.on('close', () => {
+            this.mainSocket.end();
+        })
+
+        this.localListener.on('error', () => {
+           this.mainSocket.end();
+        });
+
+        // @ts-ignore
+        this.localListener.bind(0, this.mainSocketAddressInfo.address);
+    }
+}

+ 1 - 1
src/server_test.ts

@@ -4,4 +4,4 @@ process.on('uncaughtException', function (err) {
     console.error(err);
 });
 
-const server = new Socks5Server(2211, 'localhost');
+const server = new Socks5Server(2211, '127.0.0.1');