Skip to content

Commit

Permalink
✨ feat: Add bind
Browse files Browse the repository at this point in the history
  • Loading branch information
MoIzadloo committed Apr 4, 2023
2 parents 9d1257a + 9d226fc commit 4e7a101
Show file tree
Hide file tree
Showing 18 changed files with 695 additions and 84 deletions.
94 changes: 94 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,45 @@ server.useReq('associate', (info, socket) => {
server.listen(port, host)
```

### Bind (TCP Relay)

When the bind command is sent to a SOCKS proxy server v4|v5, the proxy server starts to listen on a new TCP port
and sends the relay information back to the client. When another remote client connects to the proxy server on this port
the SOCKS proxy sends a notification that an incoming connection has been accepted to the initial client and
a full duplex stream is now established to the initial client and the client that connected to that special port.
according to the SOCKS RFC, only one inbound connection is allowed per bind request.
you can easily change my implementation of the bind command with your implementation or even deactivate it with the help of the useReq hook
and inform the client with a command not supported reply.

```typescript
import { createServer, Reply } from 'tsocks'

const host = '127.0.0.1'
const port = 1080

const server = createServer({
socks4: true,
socks5: true,
})

server.useReq('bind', (info, socket) => {
const host = info.address.host
const port = info.address.port // Port number
const type = info.address.type // ipv4 | ipv6 | domain
const version = info.version // SOCKS version
// You can implement the rest how ever you want or reject the request
// With the proper reply code as blow
// Remember the response should be decided by the version
if (version === 5) {
const reply = new Reply(version, 0x07, info.address)
} else {
const reply = new Reply(version, 0x5b, info.address)
}
})

server.listen(port, host)
```

### SOCKS Adaptor

You may want to use the SOCKS protocol to handle incoming network traffic.
Expand Down Expand Up @@ -433,6 +472,61 @@ try {
}
```

### Bind (TCP Relay)

When the bind command is sent to a SOCKS proxy server v4|v5, the proxy server starts to listen on a new TCP port
and sends the relay information back to the client. When another remote client connects to the proxy server on this port
the SOCKS proxy sends a notification that an incoming connection has been accepted to the initial client and
a full duplex stream is now established to the initial client and the client that connected to that special port.
according to the SOCKS RFC, only one inbound connection is allowed per bind request.

```typescript
import { createServer, connect, Reply } from 'tsocks'
import * as net from 'net'

const host = '127.0.0.1'
const port = 1080
// v5 or v4
const version = 5

// Create simple socks server
const server = createServer()
server.listen(port, host)

try {
// Send a bind request
const info = await connect(port, host, version).bind(0, '0.0.0.0')
const remote = net.connect(info.address.port, info.address.host)
const states = ['information', 'relay']
let state = 0
console.log(info.address)
// The relays Address and port
// host: "143.123.35.425",
// port: 3342,
// type: "ipv4",
info.socket.on('data', (data) => {
switch (states[state]) {
case states[1]:
console.log(data.toString())
remote.destroy()
info.socket.destroy()
break
default:
const reply = Reply.from(data)
console.log(reply.addr)
// The remote address of the client that connected to the SOCKS proxy
//host: "122.153.15.225",
//port: 4562,
//type: "ipv4",
remote.write(Buffer.from('Hello'))
state++
}
})
} catch (err) {
console.log(err)
}
```

## References

- [RFC - SOCKS Protocol Version 5](https://www.rfc-editor.org/rfc/rfc1928)
Expand Down
8 changes: 4 additions & 4 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
## Todo

- [ ] Implementation of server (Connect | Bind | Associate)
- [ ] Implementation of client (Connect | Bind | Associate)
- [ ] Complete documentation
- [ ] Complete test coverage

### In Progress

- [ ] Implementation of server Bind
- [ ] Implementation of client Bind
- [ ] Complete documentation
- [ ] Complete test coverage

### Done ✓

- [x] Implementation of server Connect
- [x] Implementation of client Connect
- [x] Implementation of server Associate
- [x] Implementation of client Associate
- [x] Implementation of server Bind
- [x] Implementation of client Bind
1 change: 1 addition & 0 deletions config/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ module.exports = {
os: false,
dgram: false,
buffer: false,
util: false,
},
extensions: ['.ts', '.js', '.tsx', '.jsx'],
},
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@
},
"dependencies": {
"@types/ip": "^1.1.0",
"ip": "^1.1.8"
"@types/tcp-port-used": "^1.0.1",
"ip": "^1.1.8",
"tcp-port-used": "^1.0.2"
},
"devDependencies": {
"@commitlint/cli": "^17.4.2",
Expand Down
66 changes: 64 additions & 2 deletions src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import Event from '../helper/event'
import Connection, { EventTypes } from '../helper/connection'
import * as handlers from './handlers/index'
import { Handlers } from '../helper/handlers'
import { HandlerResolve } from '../helper/handler'
import {
handler as reqHandler,
Handler,
HandlerResolve,
} from '../helper/handler'
import Authenticator from './auth/authenticator'
import Address from '../helper/address'
import { AuthMethod } from '../helper/authMethod'
Expand Down Expand Up @@ -58,6 +62,16 @@ export class Client {
this.event = new Event<EventTypes>()
}

/**
* The connector method establishes a TCP connection to the proxy server
* @param port - Proxy server port
* @param host - Proxy server host
* @param cmd - Request command
* @param resolve - This function fires after the server response
* @param reject - This function fires after the server response if any error occurs
* @param version - SOCKS version
* @param userId - UserId for identification in SOCKS4
*/
private connector(
port: number,
host: string,
Expand Down Expand Up @@ -92,6 +106,43 @@ export class Client {
return connection
}

/**
* Sends a bind request
* @param port - Target port
* @param host - Target host address
* @param version - Server protocol version
* @returns void
*/
bind(port: number, host: string, version?: 4 | 5, userId?: string) {
return new Promise<HandlerResolve>((resolve, reject) => {
const connection = this.connector(
port,
host,
COMMANDS.bind,
resolve,
reject,
version,
userId
)
if (connection?.request?.ver === 5) {
const authenticator = new Authenticator(connection)
authenticator.authenticate()
} else if (connection?.request?.ver === 4) {
if (connection?.request?.addr?.type === 'domain') {
reject('The Address type is not supported')
}
connection.handlers.req.bind(connection)
}
})
}

/**
* Sends an associate request
* @param port - Target port
* @param host - Target host address
* @param version - Server protocol version
* @returns void
*/
associate(port: number, host: string, version?: 4 | 5) {
return new Promise<HandlerResolve>((resolve, reject) => {
if (version === 5 || (!version && this.version === 5)) {
Expand All @@ -112,7 +163,7 @@ export class Client {
}

/**
* Connect to the target host trough SOCKS server
* Sends a connect request
* @param port - Target port
* @param host - Target host address
* @param version - Server protocol version
Expand Down Expand Up @@ -151,6 +202,17 @@ export class Client {
this.handlers.auth.push(handler)
return this
}

/**
* Get the handler function, and update this.handlers.req
* @param cmd - Specify handler type (connect | associate | bind)
* @param handler - Emitted when new request appears
* @returns Server
*/
public useReq(cmd: keyof Handlers['req'], handler: Handler): Client {
this.handlers.req[cmd] = reqHandler(handler)
return this
}
}

/**
Expand Down
24 changes: 3 additions & 21 deletions src/client/handlers/associate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { handler } from '../../helper/handler'
import { COMMANDS, SOCKS4REPLY, SOCKS5REPLY } from '../../helper/constants'
import { COMMANDS } from '../../helper/constants'
import Request from '../../helper/request'
import Reply from '../../helper/reply'

Expand All @@ -12,26 +12,8 @@ export const associate = handler((info, socket, event, resolve, reject) => {
socket.write(request.toBuffer())
socket.on('data', (data) => {
const reply = Reply.from(data)
if (reject) {
if (
reply.rep !== SOCKS5REPLY.succeeded.code &&
reply.rep !== SOCKS4REPLY.granted.code
) {
let msg = ''
msg += Object.values(reply.ver === 5 ? SOCKS5REPLY : SOCKS4REPLY).find(
(rep) => {
return rep.code === reply.rep
}
)?.msg
reject(msg)
}
}
if (resolve) {
resolve({
address: reply.addr,
socket: socket,
rsv: reply.rsv,
})
if (resolve && reject) {
reply.promiseHandler(socket, resolve, reject)
}
socket.removeAllListeners('data')
})
Expand Down
24 changes: 3 additions & 21 deletions src/client/handlers/bind.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { handler } from '../../helper/handler'
import Request from '../../helper/request'
import { COMMANDS, SOCKS4REPLY, SOCKS5REPLY } from '../../helper/constants'
import { COMMANDS } from '../../helper/constants'
import Reply from '../../helper/reply'

/**
Expand All @@ -18,26 +18,8 @@ export const bind = handler((info, socket, event, resolve, reject) => {
socket.write(request.toBuffer())
socket.on('data', (data) => {
const reply = Reply.from(data)
if (reject) {
if (
reply.rep !== SOCKS5REPLY.succeeded.code &&
reply.rep !== SOCKS4REPLY.granted.code
) {
let msg = ''
msg += Object.values(reply.ver === 5 ? SOCKS5REPLY : SOCKS4REPLY).find(
(rep) => {
return rep.code === reply.rep
}
)?.msg
reject(msg)
}
}
if (resolve) {
resolve({
address: reply.addr,
socket: socket,
rsv: reply.rsv,
})
if (resolve && reject) {
reply.promiseHandler(socket, resolve, reject)
}
socket.removeAllListeners('data')
})
Expand Down
24 changes: 3 additions & 21 deletions src/client/handlers/connect.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { handler } from '../../helper/handler'
import { COMMANDS, SOCKS4REPLY, SOCKS5REPLY } from '../../helper/constants'
import { COMMANDS } from '../../helper/constants'
import Request from '../../helper/request'
import Reply from '../../helper/reply'

Expand All @@ -18,26 +18,8 @@ export const connect = handler((info, socket, event, resolve, reject) => {
socket.write(request.toBuffer())
socket.on('data', (data) => {
const reply = Reply.from(data)
if (reject) {
if (
reply.rep !== SOCKS5REPLY.succeeded.code &&
reply.rep !== SOCKS4REPLY.granted.code
) {
let msg = ''
msg += Object.values(reply.ver === 5 ? SOCKS5REPLY : SOCKS4REPLY).find(
(rep) => {
return rep.code === reply.rep
}
)?.msg
reject(msg)
}
}
if (resolve) {
resolve({
address: reply.addr,
socket: socket,
rsv: reply.rsv,
})
if (resolve && reject) {
reply.promiseHandler(socket, resolve, reject)
}
socket.removeAllListeners('data')
})
Expand Down
8 changes: 8 additions & 0 deletions src/helper/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ class Connection {
socks4: true,
}

/**
* Clients Request
*/
request?: Request

constructor(
Expand Down Expand Up @@ -111,6 +114,11 @@ class Connection {
this.socket.write(writable.toBuffer())
}

/**
* Read n bytes of data without the modification of the readable object data
* @param bytes - Number of bytes to be read from data
* @returns Buffer
*/
public cat(bytes?: number): Buffer {
return this.readable.cat(bytes)
}
Expand Down
1 change: 1 addition & 0 deletions src/helper/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface HandlerResolve {
socket: net.Socket
address: Address
rsv?: number
args?: any
}

export type Handler = (
Expand Down

0 comments on commit 4e7a101

Please sign in to comment.