dnet-ffi 0.1.3
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.
- data/.document +5 -0
- data/.gitignore +7 -0
- data/LICENSE +20 -0
- data/README.rdoc +37 -0
- data/Rakefile +64 -0
- data/VERSION +1 -0
- data/dnet-ffi.gemspec +99 -0
- data/lib/dnet.rb +35 -0
- data/lib/dnet/addr.rb +204 -0
- data/lib/dnet/arp.rb +168 -0
- data/lib/dnet/blob.rb +246 -0
- data/lib/dnet/bsd.rb +123 -0
- data/lib/dnet/constants.rb +555 -0
- data/lib/dnet/eth.rb +143 -0
- data/lib/dnet/fw.rb +106 -0
- data/lib/dnet/helpers.rb +164 -0
- data/lib/dnet/icmp.rb +304 -0
- data/lib/dnet/intf.rb +194 -0
- data/lib/dnet/ip.rb +315 -0
- data/lib/dnet/ip6.rb +59 -0
- data/lib/dnet/rand.rb +33 -0
- data/lib/dnet/route.rb +103 -0
- data/lib/dnet/tcp.rb +103 -0
- data/lib/dnet/tun.rb +24 -0
- data/lib/dnet/typedefs.rb +12 -0
- data/lib/dnet/udp.rb +31 -0
- data/lib/dnet/util.rb +70 -0
- data/samples/eth_send_raw.rb +29 -0
- data/samples/ifconfig-alike.rb +44 -0
- data/samples/udp_send_raw.rb +74 -0
- data/spec/addr_spec.rb +15 -0
- data/spec/arp_spec.rb +95 -0
- data/spec/blob_spec.rb +15 -0
- data/spec/bsd_spec.rb +60 -0
- data/spec/dnet-ffi_spec.rb +31 -0
- data/spec/eth_spec.rb +47 -0
- data/spec/fw_spec.rb +15 -0
- data/spec/intf_spec.rb +98 -0
- data/spec/ip6_spec.rb +15 -0
- data/spec/ip_spec.rb +15 -0
- data/spec/rand_spec.rb +15 -0
- data/spec/route_spec.rb +94 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/tun_spec.rb +15 -0
- metadata +121 -0
data/lib/dnet/ip6.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
module Dnet
|
2
|
+
# /*
|
3
|
+
# * IPv6 header
|
4
|
+
# */
|
5
|
+
# struct ip6_hdr {
|
6
|
+
# union {
|
7
|
+
# struct ip6_hdr_ctl {
|
8
|
+
# uint32_t ip6_un1_flow; /* 20 bits of flow ID */
|
9
|
+
# uint16_t ip6_un1_plen; /* payload length */
|
10
|
+
# uint8_t ip6_un1_nxt; /* next header */
|
11
|
+
# uint8_t ip6_un1_hlim; /* hop limit */
|
12
|
+
# } ip6_un1;
|
13
|
+
# uint8_t ip6_un2_vfc; /* 4 bits version, top 4 bits class */
|
14
|
+
# } ip6_ctlun;
|
15
|
+
# ip6_addr_t ip6_src;
|
16
|
+
# ip6_addr_t ip6_dst;
|
17
|
+
# } __attribute__((__packed__));
|
18
|
+
|
19
|
+
|
20
|
+
#
|
21
|
+
# Preferred extension header order from RFC 2460, 4.1:
|
22
|
+
#
|
23
|
+
# IP_PROTO_IPV6, IP_PROTO_HOPOPTS, IP_PROTO_DSTOPTS, IP_PROTO_ROUTING,
|
24
|
+
# IP_PROTO_FRAGMENT, IP_PROTO_AH, IP_PROTO_ESP, IP_PROTO_DSTOPTS,
|
25
|
+
# IP_PROTO_*
|
26
|
+
#
|
27
|
+
|
28
|
+
# /*
|
29
|
+
# * Routing header data (IP_PROTO_ROUTING)
|
30
|
+
# */
|
31
|
+
# struct ip6_ext_data_routing {
|
32
|
+
# uint8_t type; /* routing type */
|
33
|
+
# uint8_t segleft; /* segments left */
|
34
|
+
# /* followed by routing type specific data */
|
35
|
+
# } __attribute__((__packed__));
|
36
|
+
|
37
|
+
|
38
|
+
# struct ip6_ext_data_routing0 {
|
39
|
+
# uint8_t type; /* always zero */
|
40
|
+
# uint8_t segleft; /* segments left */
|
41
|
+
# uint8_t reserved; /* reserved field */
|
42
|
+
# uint8_t slmap[3]; /* strict/loose bit map */
|
43
|
+
# ip6_addr_t addr[1]; /* up to 23 addresses */
|
44
|
+
# } __attribute__((__packed__));
|
45
|
+
|
46
|
+
|
47
|
+
# /*
|
48
|
+
# * Fragment header data (IP_PROTO_FRAGMENT)
|
49
|
+
# */
|
50
|
+
# struct ip6_ext_data_fragment {
|
51
|
+
# uint16_t offlg; /* offset, reserved, and flag */
|
52
|
+
# uint32_t ident; /* identification */
|
53
|
+
# } __attribute__((__packed__));
|
54
|
+
#
|
55
|
+
# void ip6_checksum(void *buf, size_t len);
|
56
|
+
|
57
|
+
attach_function :ip6_checksum, [:pointer, :size_t], :void
|
58
|
+
|
59
|
+
end
|
data/lib/dnet/rand.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
|
2
|
+
### libdnet's random number generation
|
3
|
+
|
4
|
+
module Dnet
|
5
|
+
|
6
|
+
# rand_t * rand_open(void);
|
7
|
+
attach_function :rand_open, [], :pointer
|
8
|
+
|
9
|
+
# int rand_get(rand_t *r, void *buf, size_t len);
|
10
|
+
attach_function :rand_get, [:pointer, :string, :size_t], :int
|
11
|
+
|
12
|
+
# int rand_set(rand_t *r, const void *seed, size_t len);
|
13
|
+
attach_function :rand_set, [:pointer, :string, :size_t], :int
|
14
|
+
|
15
|
+
# int rand_add(rand_t *r, const void *buf, size_t len);
|
16
|
+
attach_function :rand_add, [:pointer, :string, :size_t], :int
|
17
|
+
|
18
|
+
# uint8_t rand_uint8(rand_t *r);
|
19
|
+
attach_function :rand_uint8, [:pointer], :uint8
|
20
|
+
|
21
|
+
# uint16 t rand_uint16(rand_t *r);
|
22
|
+
attach_function :rand_uint16, [:pointer], :uint16
|
23
|
+
|
24
|
+
# uint32_t rand_uint32(rand_t *r);
|
25
|
+
attach_function :rand_uint32, [:pointer], :uint32
|
26
|
+
|
27
|
+
# int rand_shuffle(rand_t *r, void *base, size_t nmemb, size_t size);
|
28
|
+
attach_function :rand_shuffle, [:pointer, :string, :size_t, :size_t], :int
|
29
|
+
|
30
|
+
# rand_t * rand_close(rand_t *r);
|
31
|
+
attach_function :rand_close, [:pointer], :pointer
|
32
|
+
|
33
|
+
end
|
data/lib/dnet/route.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
|
2
|
+
### dnet(3) routing interface
|
3
|
+
|
4
|
+
module Dnet
|
5
|
+
|
6
|
+
module Route
|
7
|
+
# Routing table entry
|
8
|
+
#
|
9
|
+
# struct route_entry {
|
10
|
+
# struct addr route_dst; /* destination address */
|
11
|
+
# struct addr route_gw; /* gateway address */
|
12
|
+
# };
|
13
|
+
class Entry < ::FFI::Struct
|
14
|
+
include ::FFI::DRY::StructHelper
|
15
|
+
|
16
|
+
dsl_layout do
|
17
|
+
struct :dst, ::Dnet::Addr, :dest => 'destination gateway'
|
18
|
+
struct :gw, ::Dnet::Addr, :dest => 'gateway address'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Obtains a handle to access the kernel route(4) table.
|
23
|
+
class Handle < LoopableHandle
|
24
|
+
attr_reader :handle
|
25
|
+
|
26
|
+
# Obtains a handle to access the kernel route(4) table. Uses dnet(3)'s
|
27
|
+
# route_open() under the hood.
|
28
|
+
def initialize
|
29
|
+
if (@handle = ::Dnet.route_open()).address == 0
|
30
|
+
raise H_ERR.new("unable to open route handle")
|
31
|
+
end
|
32
|
+
_handle_opened!
|
33
|
+
end
|
34
|
+
|
35
|
+
# Closes the routing handle handle. Uses dnet(3)'s route_close() under the
|
36
|
+
# hood.
|
37
|
+
def close
|
38
|
+
_do_if_open { _handle_closed! ; ::Dnet.route_close(@fw) }
|
39
|
+
end
|
40
|
+
|
41
|
+
# Iterates over the kernel route(4) table, invoking the specified block
|
42
|
+
# with each route cast as a Entry object. Uses dnet(3)'s route_loop()
|
43
|
+
# function under the hood.
|
44
|
+
def loop &block
|
45
|
+
_loop ::Dnet, :route_loop, Entry, &block
|
46
|
+
end
|
47
|
+
|
48
|
+
# Retrieves the routing table entry for the destination 'dst' (supplied as
|
49
|
+
# a String argument and parsed via dnet(3)'s addr_aton() function). Uses
|
50
|
+
# dnet(3)'s route_get() under the hood.
|
51
|
+
def get(dst)
|
52
|
+
_check_open!
|
53
|
+
re = Entry.new
|
54
|
+
if( re.dst.set_string(dst) and ::Dnet.route_get(@handle, re) == 0 )
|
55
|
+
return re
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
# Adds a new routing table entry (supplied as a Entry). Uses dnet(3)'s
|
61
|
+
# route_add() under the hood.
|
62
|
+
def add(entry)
|
63
|
+
_check_open!
|
64
|
+
::Dnet.route_add(@handle, entry)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Delete's the specified route entry for the destination prefix specified
|
68
|
+
# by the destination 'dst' (supplied as a String argument and parsed
|
69
|
+
# by dnet(3)'s addr_aton function).
|
70
|
+
def delete(dst)
|
71
|
+
_check_open!
|
72
|
+
re = Entry.new
|
73
|
+
if( re.dst.set_string(dst) and ::Dnet.route_get(@handle, re) == 0 )
|
74
|
+
return re
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end # Handle
|
78
|
+
|
79
|
+
def self.open(*args)
|
80
|
+
Handle.open(*args){|*y| yield(*y) if block_given? }
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.entries
|
84
|
+
Handle.entries
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.each_entry(*args)
|
88
|
+
Handle.each_entry(*args){|*y| yield(*y) }
|
89
|
+
end
|
90
|
+
end # Route
|
91
|
+
|
92
|
+
# just an alias for Route::Handle
|
93
|
+
RouteHandle = Route::Handle
|
94
|
+
|
95
|
+
callback :route_handler, [:route_t, :string], :int
|
96
|
+
attach_function :route_open, [], :route_t
|
97
|
+
attach_function :route_add, [:route_t, Route::Entry], :int
|
98
|
+
attach_function :route_delete, [:route_t, Route::Entry], :int
|
99
|
+
attach_function :route_get, [:route_t, Route::Entry], :int
|
100
|
+
attach_function :route_loop, [:route_t, :route_handler, :ulong], :int
|
101
|
+
attach_function :route_close, [:route_t], :route_t
|
102
|
+
|
103
|
+
end
|
data/lib/dnet/tcp.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
module Dnet
|
2
|
+
module Tcp
|
3
|
+
|
4
|
+
# TCP header, without options
|
5
|
+
#
|
6
|
+
# field :sport, :uint16, :desc => 'source port'
|
7
|
+
# field :dport, :uint16, :desc => 'destination port'
|
8
|
+
# field :seq, :uint32, :desc => 'sequence number'
|
9
|
+
# field :ack, :uint32, :desc => 'acknowledgment number'
|
10
|
+
# field :off_x2, :uint8, :desc => 'data offset(& 0xf0) unused (& 0x0f)'
|
11
|
+
# field :flags, :uint8, :desc => 'control flags'
|
12
|
+
# field :win, :uint16, :desc => 'window'
|
13
|
+
# field :sum, :uint16, :desc => 'checksum'
|
14
|
+
# field :urgp, :uint16, :desc => 'urgent pointer'
|
15
|
+
#
|
16
|
+
class Hdr < ::FFI::Struct
|
17
|
+
include ::FFI::DRY::StructHelper
|
18
|
+
include ::Dnet::NetEndianHelper
|
19
|
+
|
20
|
+
dsl_layout do
|
21
|
+
field :sport, :uint16, :desc => 'source port'
|
22
|
+
field :dport, :uint16, :desc => 'destination port'
|
23
|
+
field :seq, :uint32, :desc => 'sequence number'
|
24
|
+
field :ack, :uint32, :desc => 'acknowledgment number'
|
25
|
+
field :off_x2, :uint8, :desc => 'data offset(& 0xf0) unused (& 0x0f)'
|
26
|
+
field :flags, :uint8, :desc => 'control flags'
|
27
|
+
field :win, :uint16, :desc => 'window'
|
28
|
+
field :sum, :uint16, :desc => 'checksum'
|
29
|
+
field :urgp, :uint16, :desc => 'urgent pointer'
|
30
|
+
end
|
31
|
+
|
32
|
+
# TCP control flags (flags)
|
33
|
+
module Flags
|
34
|
+
include ::FFI::DRY::ConstFlagsMap
|
35
|
+
slurp_constants(::Dnet, "TH_")
|
36
|
+
def self.list; @@list ||= super() ; end
|
37
|
+
end
|
38
|
+
|
39
|
+
# #define \
|
40
|
+
# tcp_pack_hdr(hdr, sport, dport, seq, ack, flags, win, urp) do { \
|
41
|
+
# struct tcp_hdr *tcp_pack_p = (struct tcp_hdr *)(hdr); \
|
42
|
+
# tcp_pack_p->th_sport = htons(sport); \
|
43
|
+
# tcp_pack_p->th_dport = htons(dport); \
|
44
|
+
# tcp_pack_p->th_seq = htonl(seq); \
|
45
|
+
# tcp_pack_p->th_ack = htonl(ack); \
|
46
|
+
# tcp_pack_p->th_x2 = 0; tcp_pack_p->th_off = 5; \
|
47
|
+
# tcp_pack_p->th_flags = flags; \
|
48
|
+
# tcp_pack_p->th_win = htons(win); \
|
49
|
+
# tcp_pack_p->th_urp = htons(urp); \
|
50
|
+
# } while (0)
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# TCP option (following TCP header)
|
56
|
+
#
|
57
|
+
# struct tcp_opt {
|
58
|
+
# uint8_t opt_type; /* option type */
|
59
|
+
# uint8_t opt_len; /* option length >= TCP_OPT_LEN */
|
60
|
+
# union tcp_opt_data {
|
61
|
+
# uint16_t mss; /* TCP_OPT_MSS */
|
62
|
+
# uint8_t wscale; /* TCP_OPT_WSCALE */
|
63
|
+
# uint16_t sack[19]; /* TCP_OPT_SACK */
|
64
|
+
# uint32_t echo; /* TCP_OPT_ECHO{REPLY} */
|
65
|
+
# uint32_t timestamp[2]; /* TCP_OPT_TIMESTAMP */
|
66
|
+
# uint32_t cc; /* TCP_OPT_CC{NEW,ECHO} */
|
67
|
+
# uint8_t cksum; /* TCP_OPT_ALTSUM */
|
68
|
+
# uint8_t md5[16]; /* TCP_OPT_MD5 */
|
69
|
+
# uint8_t data8[TCP_OPT_LEN_MAX - TCP_OPT_LEN];
|
70
|
+
# } opt_data;
|
71
|
+
# } __attribute__((__packed__));
|
72
|
+
#
|
73
|
+
class Opt < ::FFI::Struct
|
74
|
+
include ::FFI::DRY::StructHelper
|
75
|
+
include ::Dnet::NetEndianHelper
|
76
|
+
|
77
|
+
DATA_LEN = TCP_OPT_LEN_MAX - TCP_OPT_LEN
|
78
|
+
|
79
|
+
dsl_layout do
|
80
|
+
field :otype, :uint8
|
81
|
+
field :len, :uint8
|
82
|
+
array :data8, [:uint8, DATA_LEN]
|
83
|
+
end
|
84
|
+
|
85
|
+
# Options (otype) - http://www.iana.org/assignments/tcp-parameters
|
86
|
+
module Otype
|
87
|
+
include ::FFI::DRY::ConstMap
|
88
|
+
slurp_constants(::Dnet, "TCP_OTYPE_")
|
89
|
+
def self.list; @@list ||= super() ; end
|
90
|
+
end
|
91
|
+
|
92
|
+
end # class Opt
|
93
|
+
|
94
|
+
# TCP FSM states
|
95
|
+
module State
|
96
|
+
include ::FFI::DRY::ConstMap
|
97
|
+
slurp_constants(::Dnet, "TCP_STATE_")
|
98
|
+
def self.list; @@list ||= super() ; end
|
99
|
+
end
|
100
|
+
|
101
|
+
end # module Tcp
|
102
|
+
end # module Dnet
|
103
|
+
|
data/lib/dnet/tun.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
|
2
|
+
### libdnet's tunnel interface
|
3
|
+
|
4
|
+
module Dnet
|
5
|
+
|
6
|
+
# tun_t * tun_open(struct addr *src, struct addr *dst, int mtu);
|
7
|
+
attach_function :tun_open, [:pointer, :pointer, :int], :pointer
|
8
|
+
|
9
|
+
# int tun_fileno(tun_t *t);
|
10
|
+
attach_function :tun_fileno, [:pointer], :int
|
11
|
+
|
12
|
+
# const char * tun_name(tun_t *t);
|
13
|
+
attach_function :tun_name, [:pointer], :string
|
14
|
+
|
15
|
+
# ssize_t tun_send(tun_t *t, const void *buf, size_t size);
|
16
|
+
attach_function :tun_send, [:pointer, :string, :size_t], :ssize_t
|
17
|
+
|
18
|
+
# ssize_t tun_recv(tun_t *t, void *buf, size_t size);
|
19
|
+
attach_function :tun_recv, [:pointer, :string, :size_t], :ssize_t
|
20
|
+
|
21
|
+
# tun_t * tun_close(tun_t *t);
|
22
|
+
attach_function :tun_close, [:pointer], :pointer
|
23
|
+
|
24
|
+
end
|
data/lib/dnet/udp.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
module Dnet
|
2
|
+
|
3
|
+
class Udp
|
4
|
+
class Hdr < ::FFI::Struct
|
5
|
+
include ::FFI::DRY::StructHelper
|
6
|
+
include ::Dnet::NetEndianHelper
|
7
|
+
|
8
|
+
# struct udp_hdr {
|
9
|
+
# uint16_t uh_sport; /* source port */
|
10
|
+
# uint16_t uh_dport; /* destination port */
|
11
|
+
# uint16_t uh_ulen; /* udp length (including header) */
|
12
|
+
# uint16_t uh_sum; /* udp checksum */
|
13
|
+
# };
|
14
|
+
dsl_layout do
|
15
|
+
field :sport, :uint16
|
16
|
+
field :dport, :uint16
|
17
|
+
field :len, :uint16
|
18
|
+
field :sum, :uint16
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
# #define udp_pack_hdr(hdr, sport, dport, ulen) do { \
|
25
|
+
# struct udp_hdr *udp_pack_p = (struct udp_hdr *)(hdr); \
|
26
|
+
# udp_pack_p->uh_sport = htons(sport); \
|
27
|
+
# udp_pack_p->uh_dport = htons(dport); \
|
28
|
+
# udp_pack_p->uh_ulen = htons(ulen); \
|
29
|
+
# } while (0)
|
30
|
+
|
31
|
+
end
|
data/lib/dnet/util.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
|
2
|
+
module Dnet
|
3
|
+
module Util
|
4
|
+
RX_IP4_ADDR = /(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/
|
5
|
+
RX_MAC_ADDR = /(?:(?:[a-f0-9]{1,2}[:-])?{5}[a-f0-9]{1,2})/i
|
6
|
+
|
7
|
+
# A number of helper methods which can be used to extend class, instance,
|
8
|
+
# or module
|
9
|
+
module Helpers
|
10
|
+
|
11
|
+
def unhexify(str, d=/\s*/)
|
12
|
+
str.to_s.strip.gsub(/([A-Fa-f0-9]{1,2})#{d}?/) { $1.hex.chr }
|
13
|
+
end
|
14
|
+
|
15
|
+
# Attempts to derive a memory pointer and length from an "anonymous" object.
|
16
|
+
# Returns an an array object containing [len, pointer]
|
17
|
+
#
|
18
|
+
# buf can be a String or FFI::Pointer. If it is a Pointer, bsz must also
|
19
|
+
# be supplied for the length. If bsz is included with a String, the string
|
20
|
+
# will be truncated if it is longer.
|
21
|
+
#
|
22
|
+
# This is mostly used to support multiple argument types in various
|
23
|
+
# functions.
|
24
|
+
def derive_pointer(buf, bsz=nil)
|
25
|
+
case buf
|
26
|
+
when ::FFI::Pointer
|
27
|
+
raise "no length specified for pointer" if bsz.nil?
|
28
|
+
raise "size must be a number >= 0" unless bsz.is_a? Numeric and bsz >= 0
|
29
|
+
raise "null pointer #{buf.inspect}" if buf.address == 0
|
30
|
+
pbuf = buf
|
31
|
+
when String
|
32
|
+
buf = buf[0,bsz] if bsz
|
33
|
+
pbuf = ::FFI::MemoryPointer.from_string(buf)
|
34
|
+
bsz = buf.size
|
35
|
+
else
|
36
|
+
raise "cannot derive a pointer and size from a #{buf.class}"
|
37
|
+
end
|
38
|
+
return [pbuf, bsz]
|
39
|
+
end
|
40
|
+
|
41
|
+
# takes a IPv4 number and returns it as a 32-bit number
|
42
|
+
def ipv4_atol(str)
|
43
|
+
unless str =~ /^#{::Dnet::Util::RX_IP4_ADDR}$/
|
44
|
+
raise(::ArgumentError, "invalid IP address #{str.inspect}")
|
45
|
+
else
|
46
|
+
u32=0
|
47
|
+
str.split('.',4).each {|o| u32 = ((u32 << 8) | o.to_i) }
|
48
|
+
return u32
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# takes a 32-bit number and returns it as an IPv4 address string.
|
53
|
+
def ipv4_ltoa(int)
|
54
|
+
unless(int.is_a? Numeric and int <= 0xffffffff)
|
55
|
+
raise(::ArgumentError, "not a long integer: #{int.inspect}")
|
56
|
+
end
|
57
|
+
ret = []
|
58
|
+
4.times do
|
59
|
+
ret.unshift(int & 0xff)
|
60
|
+
int >>= 8
|
61
|
+
end
|
62
|
+
ret.join('.')
|
63
|
+
end
|
64
|
+
|
65
|
+
end # module Helpers
|
66
|
+
|
67
|
+
extend(::Dnet::Util::Helpers)
|
68
|
+
end # module Util
|
69
|
+
|
70
|
+
end
|