sctp-socket 0.1.4 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.gitignore +1 -0
- data/CHANGES.md +21 -0
- data/README.md +75 -0
- data/Rakefile +3 -1
- data/examples/client_for_server_test.rb +34 -0
- data/examples/sctp_server_example.rb +69 -0
- data/examples/server_using_sctp_server.rb +65 -0
- data/ext/sctp/socket.c +604 -115
- data/lib/sctp/server.rb +207 -0
- data/sctp-socket.gemspec +2 -2
- data.tar.gz.sig +0 -0
- metadata +6 -7
- metadata.gz.sig +0 -0
- data/.github/FUNDING.yml +0 -4
- data/.github/workflows/ruby.yml +0 -59
- data/ext/sctp/socket/client.c +0 -7
- data/ext/sctp/socket/server.c +0 -7
- data/ext/sctp/socket.h +0 -18
data/lib/sctp/server.rb
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'sctp/socket'
|
3
|
+
|
4
|
+
module SCTP
|
5
|
+
# SCTP::Server provides a TCPServer-like interface for SCTP sockets.
|
6
|
+
#
|
7
|
+
# Unlike TCP which uses accept() to create new sockets for each connection,
|
8
|
+
# SCTP can operate in one-to-many mode where a single socket handles
|
9
|
+
# multiple associations. This server class provides both modes:
|
10
|
+
#
|
11
|
+
# 1. One-to-many mode (default): All connections share the server socket
|
12
|
+
# 2. One-to-one mode: Uses peeloff() to create individual sockets per association
|
13
|
+
#
|
14
|
+
# Example usage:
|
15
|
+
#
|
16
|
+
# # Basic one-to-many server
|
17
|
+
# server = SCTP::Server.new(['127.0.0.1'], 9999)
|
18
|
+
# loop do
|
19
|
+
# data, info = server.recvmsg
|
20
|
+
# puts "Received: #{data} from association #{info.association_id}"
|
21
|
+
# server.sendmsg("Echo: #{data}", association_id: info.association_id)
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# # One-to-one server with peeloff
|
25
|
+
# server = SCTP::Server.new(['127.0.0.1'], 9999, one_to_one: true)
|
26
|
+
# loop do
|
27
|
+
# client = server.accept # Returns a new SCTP::Socket for the association
|
28
|
+
# Thread.new(client) do |c|
|
29
|
+
# data, info = c.recvmsg
|
30
|
+
# c.sendmsg("Echo: #{data}")
|
31
|
+
# c.close
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
class Server
|
36
|
+
attr_reader :socket, :addresses, :port, :one_to_one
|
37
|
+
|
38
|
+
# Create a new SCTP server.
|
39
|
+
#
|
40
|
+
# @param addresses [Array<String>, String, nil] IP addresses to bind to.
|
41
|
+
# If nil, binds to all available addresses.
|
42
|
+
# @param port [Integer] Port number to bind to
|
43
|
+
# @param one_to_one [Boolean] If true, uses one-to-one mode with peeloff.
|
44
|
+
# If false (default), uses one-to-many mode.
|
45
|
+
# @param backlog [Integer] Listen backlog (default: 128)
|
46
|
+
# @param socket_options [Hash] Additional socket options
|
47
|
+
def initialize(addresses = nil, port = 0, one_to_one: false, backlog: 128, **socket_options)
|
48
|
+
@addresses = Array(addresses) if addresses
|
49
|
+
@port = port
|
50
|
+
@one_to_one = one_to_one
|
51
|
+
@backlog = backlog
|
52
|
+
@socket_options = socket_options
|
53
|
+
@pending_associations = {}
|
54
|
+
|
55
|
+
# Create the main server socket
|
56
|
+
if one_to_one
|
57
|
+
@socket = SCTP::Socket.new(2, 1) # AF_INET, SOCK_STREAM
|
58
|
+
else
|
59
|
+
@socket = SCTP::Socket.new(2, 5) # AF_INET, SOCK_SEQPACKET
|
60
|
+
end
|
61
|
+
|
62
|
+
setup_socket
|
63
|
+
bind_and_listen
|
64
|
+
end
|
65
|
+
|
66
|
+
# Accept a new association (one-to-one mode only).
|
67
|
+
#
|
68
|
+
# In one-to-many mode, this method is not used. Instead, use recvmsg
|
69
|
+
# to receive data from any association.
|
70
|
+
#
|
71
|
+
# @return [SCTP::Socket] A new socket for the accepted association
|
72
|
+
# @raise [RuntimeError] If not in one-to-one mode
|
73
|
+
def accept
|
74
|
+
raise "accept() only available in one-to-one mode" unless @one_to_one
|
75
|
+
|
76
|
+
# Wait for a message to establish an association
|
77
|
+
data, info = @socket.recvmsg
|
78
|
+
association_id = info.association_id
|
79
|
+
|
80
|
+
# Check if we already have a peeled-off socket for this association
|
81
|
+
if @pending_associations[association_id]
|
82
|
+
client_socket = @pending_associations.delete(association_id)
|
83
|
+
else
|
84
|
+
# Peeloff a new socket for this association
|
85
|
+
client_socket = @socket.peeloff(association_id)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Store the initial message in the client socket for retrieval
|
89
|
+
client_socket.instance_variable_set(:@initial_message, [data, info])
|
90
|
+
|
91
|
+
# Add a method to retrieve the initial message
|
92
|
+
def client_socket.initial_message
|
93
|
+
@initial_message
|
94
|
+
end
|
95
|
+
|
96
|
+
client_socket
|
97
|
+
end
|
98
|
+
|
99
|
+
# Receive a message from any association (one-to-many mode).
|
100
|
+
#
|
101
|
+
# @param flags [Integer] Receive flags (default: 0)
|
102
|
+
# @return [Array<String, Struct>] Message data and info struct
|
103
|
+
def recvmsg(flags = 0)
|
104
|
+
@socket.recvmsg(flags)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Send a message to a specific association (one-to-many mode).
|
108
|
+
#
|
109
|
+
# @param data [String] Data to send
|
110
|
+
# @param options [Hash] Send options including :association_id
|
111
|
+
# @return [Integer] Number of bytes sent
|
112
|
+
def sendmsg(data, **options)
|
113
|
+
@socket.sendmsg(options.merge(message: data))
|
114
|
+
end
|
115
|
+
|
116
|
+
# Get local addresses bound to this server.
|
117
|
+
#
|
118
|
+
# @return [Array<String>] Local addresses
|
119
|
+
def addr
|
120
|
+
@socket.getlocalnames
|
121
|
+
end
|
122
|
+
|
123
|
+
# Get the local port number.
|
124
|
+
#
|
125
|
+
# @return [Integer] Port number
|
126
|
+
def local_port
|
127
|
+
@socket.port
|
128
|
+
end
|
129
|
+
|
130
|
+
# Check if the server is closed.
|
131
|
+
#
|
132
|
+
# @return [Boolean] true if closed, false otherwise
|
133
|
+
def closed?
|
134
|
+
@socket.closed?
|
135
|
+
end
|
136
|
+
|
137
|
+
# Close the server socket.
|
138
|
+
#
|
139
|
+
# @param options [Hash] Close options (e.g., linger: seconds)
|
140
|
+
def close(**options)
|
141
|
+
@socket.close(options) unless closed?
|
142
|
+
end
|
143
|
+
|
144
|
+
# Get server socket information.
|
145
|
+
#
|
146
|
+
# @return [String] String representation of the server
|
147
|
+
def to_s
|
148
|
+
if closed?
|
149
|
+
"#<SCTP::Server:closed>"
|
150
|
+
else
|
151
|
+
mode = @one_to_one ? "one-to-one" : "one-to-many"
|
152
|
+
"#<SCTP::Server:#{addr.join(',')}:#{local_port} (#{mode})>"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
alias_method :inspect, :to_s
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
def setup_socket
|
161
|
+
# Apply any socket options provided
|
162
|
+
@socket_options.each do |option, value|
|
163
|
+
case option
|
164
|
+
when :reuse_addr
|
165
|
+
# SCTP typically doesn't need SO_REUSEADDR like TCP
|
166
|
+
# but we can support it for compatibility
|
167
|
+
when :autoclose
|
168
|
+
@socket.autoclose = value
|
169
|
+
when :nodelay
|
170
|
+
@socket.nodelay = value
|
171
|
+
when :init_msg
|
172
|
+
@socket.set_initmsg(value)
|
173
|
+
when :subscriptions
|
174
|
+
@socket.subscribe(value)
|
175
|
+
else
|
176
|
+
# For any other options, try to call them as methods
|
177
|
+
if @socket.respond_to?("#{option}=")
|
178
|
+
@socket.send("#{option}=", value)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Set up default subscriptions for server operation
|
184
|
+
@socket.subscribe(
|
185
|
+
data_io: true,
|
186
|
+
association: true,
|
187
|
+
address: true,
|
188
|
+
send_failure: true,
|
189
|
+
shutdown: true
|
190
|
+
)
|
191
|
+
end
|
192
|
+
|
193
|
+
def bind_and_listen
|
194
|
+
if @addresses && !@addresses.empty?
|
195
|
+
@socket.bindx(port: @port, addresses: @addresses, reuse_addr: true)
|
196
|
+
else
|
197
|
+
# Bind to all available addresses
|
198
|
+
@socket.bindx(port: @port, reuse_addr: true)
|
199
|
+
end
|
200
|
+
|
201
|
+
@socket.listen(@backlog)
|
202
|
+
|
203
|
+
# Update port if it was auto-assigned (port 0)
|
204
|
+
@port = @socket.port if @port == 0
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
data/sctp-socket.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |spec|
|
2
2
|
spec.name = 'sctp-socket'
|
3
|
-
spec.version = '0.1
|
3
|
+
spec.version = '0.2.1'
|
4
4
|
spec.author = 'Daniel Berger'
|
5
5
|
spec.email = 'djberg96@gmail.com'
|
6
6
|
spec.summary = 'Ruby bindings for SCTP sockets'
|
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.cert_chain = ['certs/djberg96_pub.pem']
|
10
10
|
|
11
11
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
12
|
-
f.match(%r{^(test|spec|features)/})
|
12
|
+
f.match(%r{^(test|spec|features|.github)/})
|
13
13
|
end
|
14
14
|
|
15
15
|
spec.extensions = ['ext/sctp/extconf.rb']
|
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sctp-socket
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Berger
|
@@ -35,7 +35,7 @@ cert_chain:
|
|
35
35
|
ORVCZpRuCPpmC8qmqxUnARDArzucjaclkxjLWvCVHeFa9UP7K3Nl9oTjJNv+7/jM
|
36
36
|
WZs4eecIcUc4tKdHxcAJ0MO/Dkqq7hGaiHpwKY76wQ1+8xAh
|
37
37
|
-----END CERTIFICATE-----
|
38
|
-
date: 2025-
|
38
|
+
date: 2025-08-17 00:00:00.000000000 Z
|
39
39
|
dependencies:
|
40
40
|
- !ruby/object:Gem::Dependency
|
41
41
|
name: bundler
|
@@ -103,8 +103,6 @@ extensions:
|
|
103
103
|
- ext/sctp/extconf.rb
|
104
104
|
extra_rdoc_files: []
|
105
105
|
files:
|
106
|
-
- ".github/FUNDING.yml"
|
107
|
-
- ".github/workflows/ruby.yml"
|
108
106
|
- ".gitignore"
|
109
107
|
- CHANGES.md
|
110
108
|
- Gemfile
|
@@ -114,12 +112,13 @@ files:
|
|
114
112
|
- Rakefile
|
115
113
|
- certs/djberg96_pub.pem
|
116
114
|
- examples/client_example.rb
|
115
|
+
- examples/client_for_server_test.rb
|
116
|
+
- examples/sctp_server_example.rb
|
117
117
|
- examples/server_example.rb
|
118
|
+
- examples/server_using_sctp_server.rb
|
118
119
|
- ext/sctp/extconf.rb
|
119
120
|
- ext/sctp/socket.c
|
120
|
-
-
|
121
|
-
- ext/sctp/socket/client.c
|
122
|
-
- ext/sctp/socket/server.c
|
121
|
+
- lib/sctp/server.rb
|
123
122
|
- sctp-socket.gemspec
|
124
123
|
homepage: https://github.com/djberg96/sctp-socket
|
125
124
|
licenses:
|
metadata.gz.sig
CHANGED
Binary file
|
data/.github/FUNDING.yml
DELETED
data/.github/workflows/ruby.yml
DELETED
@@ -1,59 +0,0 @@
|
|
1
|
-
name: Ruby
|
2
|
-
|
3
|
-
on:
|
4
|
-
push:
|
5
|
-
branches: [ main ]
|
6
|
-
paths-ignore:
|
7
|
-
- '**/*.md'
|
8
|
-
pull_request:
|
9
|
-
branches: [ main ]
|
10
|
-
paths-ignore:
|
11
|
-
- '**/*.md'
|
12
|
-
workflow_dispatch:
|
13
|
-
|
14
|
-
jobs:
|
15
|
-
test:
|
16
|
-
runs-on: ubuntu-latest
|
17
|
-
strategy:
|
18
|
-
matrix:
|
19
|
-
ruby-version: ['3.2', '3.3']
|
20
|
-
steps:
|
21
|
-
- uses: actions/checkout@v4
|
22
|
-
- name: Install SCTP headers
|
23
|
-
run: |
|
24
|
-
sudo apt-get install libsctp-dev lksctp-tools
|
25
|
-
sudo ip link add dummy1 type dummy
|
26
|
-
sudo ip link add dummy2 type dummy
|
27
|
-
sudo ip addr add 1.1.1.1/24 dev dummy1
|
28
|
-
sudo ip addr add 1.1.1.2/24 dev dummy2
|
29
|
-
sudo ip link set dummy1 up
|
30
|
-
sudo ip link set dummy2 up
|
31
|
-
- name: Setup Ruby
|
32
|
-
uses: ruby/setup-ruby@v1
|
33
|
-
with:
|
34
|
-
ruby-version: ${{ matrix.ruby-version }}
|
35
|
-
bundler-cache: true
|
36
|
-
- name: Run Specs
|
37
|
-
run: bundle exec rake
|
38
|
-
freebsd:
|
39
|
-
runs-on: ubuntu-latest
|
40
|
-
steps:
|
41
|
-
- uses: actions/checkout@v4
|
42
|
-
- name: Test in FreeBSD
|
43
|
-
id: test
|
44
|
-
uses: vmactions/freebsd-vm@v1
|
45
|
-
with:
|
46
|
-
usesh: true
|
47
|
-
prepare: |
|
48
|
-
pkg install -y llvm ruby devel/ruby-gems sctplib git-tiny
|
49
|
-
|
50
|
-
run: |
|
51
|
-
git config --global --add safe.directory /home/runner/work/sctp-socket/sctp-socket
|
52
|
-
kldload sctp
|
53
|
-
ifconfig lo1 create
|
54
|
-
ifconfig lo1 1.1.1.1/24 up
|
55
|
-
ifconfig lo2 create
|
56
|
-
ifconfig lo2 1.1.1.2/24 up
|
57
|
-
gem install bundler --no-document
|
58
|
-
bundle install --quiet
|
59
|
-
bundle exec rake
|
data/ext/sctp/socket/client.c
DELETED
data/ext/sctp/socket/server.c
DELETED
data/ext/sctp/socket.h
DELETED
@@ -1,18 +0,0 @@
|
|
1
|
-
#ifndef SCTP_SOCKET_INCLUDED
|
2
|
-
#define SCTP_SOCKET_INCLUDED
|
3
|
-
|
4
|
-
#include <ruby.h>
|
5
|
-
#include <netinet/sctp.h>
|
6
|
-
|
7
|
-
VALUE rb_hash_aref2(VALUE, const char*);
|
8
|
-
|
9
|
-
void Init_socket();
|
10
|
-
void Init_server();
|
11
|
-
void Init_client();
|
12
|
-
|
13
|
-
extern VALUE mSCTP;
|
14
|
-
extern VALUE cSocket;
|
15
|
-
extern VALUE cClient;
|
16
|
-
extern VALUE cServer;
|
17
|
-
|
18
|
-
#endif
|