Skip to content

Commit fca1b32

Browse files
hyperpolymathclaude
andcommitted
feat(v-ecosystem): expand final 4 connector stubs — ptp, smb, deception, federation
v-ptp: IEEE 1588-2019 PTP — PtpMessageType.is_event(), PtpTimestamp.to_nanos(), encode_ptp_header (34 bytes), compute_mean_path_delay + clock_offset, 9 tests. v-smb: SMB2 — Smb2Command.min_structure_size(), encode/decode_smb2_header (64 bytes LE, magic verify), encode_negotiate_request (3 dialects incl. 0x0311), 8 tests. v_deception: Moving-target defence — DecoyType.default_ttl(), Canarytoken + triggered state, MovingTargetPolicy with deploy/trigger/rotate_decoys, 9 tests. v_federation: SAML2/OIDC/CAS — build_saml_authn_request (SHA-256-derived request ID), parse_saml_response_issuer, validate_oidc_id_token_claims (iss/sub/aud/exp), FederationSession.is_expired(), 6 tests. v-ecosystem connector completion: all 95 connectors now ≥ 200 lines (33 081 total). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0faf701 commit fca1b32

File tree

4 files changed

+708
-157
lines changed

4 files changed

+708
-157
lines changed

v-ecosystem/v-api-interfaces/v-ptp/src/ptp.v

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,36 @@ const flag_freq_traceable = u16(0x8000)
4949
// PTP transport domain default.
5050
const default_domain = u8(0)
5151

52+
// ptp_domain_default is the IANA-standard default PTP domain (IEEE 1588 §7.1).
53+
pub const ptp_domain_default = u8(0)
54+
55+
// --- Message type enumeration ---
56+
57+
// PtpMessageType is a typed enumeration of PTP message type codes.
58+
pub enum PtpMessageType {
59+
sync // 0x0 — periodic reference time from master clock
60+
delay_req // 0x1 — sent by slave to start path delay measurement
61+
follow_up // 0x8 — carries precise t1 for two-step clocks
62+
delay_resp // 0x9 — master's response to Delay_Req, carries t4
63+
announce // 0xB — master identity advertisement for BMCA
64+
}
65+
66+
// is_event returns true for message types transported on the event port (319).
67+
pub fn (m PtpMessageType) is_event() bool {
68+
return m == .sync || m == .delay_req
69+
}
70+
71+
// code returns the 4-bit wire value for this message type.
72+
pub fn (m PtpMessageType) code() u8 {
73+
return match m {
74+
.sync { u8(0x0) }
75+
.delay_req { u8(0x1) }
76+
.follow_up { u8(0x8) }
77+
.delay_resp { u8(0x9) }
78+
.announce { u8(0xB) }
79+
}
80+
}
81+
5282
// --- Clock class enumeration ---
5383

5484
// ClockClass indicates the quality of the clock source.
@@ -70,6 +100,21 @@ pub:
70100
nanoseconds u32 // Nanoseconds within the second
71101
}
72102

103+
// PtpTimestamp is the canonical 10-byte PTP timestamp (IEEE 1588 §5.3.3).
104+
// seconds field is 48 bits, split as seconds_msb (u16) + seconds (u32).
105+
pub struct PtpTimestamp {
106+
pub:
107+
seconds_msb u16 // Upper 16 bits of the 48-bit seconds field
108+
seconds u32 // Lower 32 bits of the seconds field
109+
nanoseconds u32 // Sub-second [0, 999_999_999]
110+
}
111+
112+
// to_nanos converts PtpTimestamp to a single i64 nanosecond value.
113+
pub fn (t PtpTimestamp) to_nanos() i64 {
114+
secs := i64(t.seconds_msb) << 32 | i64(t.seconds)
115+
return secs * 1_000_000_000 + i64(t.nanoseconds)
116+
}
117+
73118
// ClockIdentity is an 8-byte unique clock identifier.
74119
pub struct ClockIdentity {
75120
pub:
@@ -147,6 +192,25 @@ pub fn (mut c Client) compute_offset(t1 Timestamp, t2 Timestamp, t3 Timestamp, t
147192
return c.offset_ns
148193
}
149194

195+
// --- Path delay / offset computation ---
196+
197+
// compute_mean_path_delay computes the mean path delay from the four IEEE 1588
198+
// exchange timestamps (all in nanoseconds since epoch):
199+
// t1 = master Sync egress (from Follow_Up)
200+
// t2 = slave Sync ingress (measured locally)
201+
// t3 = slave Delay_Req egress (measured locally)
202+
// t4 = master Delay_Req ingress (from Delay_Resp)
203+
// Formula: ((t2 - t1) + (t4 - t3)) / 2
204+
pub fn compute_mean_path_delay(t1 i64, t2 i64, t3 i64, t4 i64) i64 {
205+
return ((t2 - t1) + (t4 - t3)) / 2
206+
}
207+
208+
// compute_clock_offset computes the slave clock offset from master.
209+
// Formula: (t2 - t1) - mean_path_delay
210+
pub fn compute_clock_offset(t1 i64, t2 i64, t3 i64, t4 i64) i64 {
211+
return (t2 - t1) - compute_mean_path_delay(t1, t2, t3, t4)
212+
}
213+
150214
// --- Encoding ---
151215

152216
// encode_ptp_header serialises the common PTP header portion (34 bytes)
@@ -274,3 +338,25 @@ fn test_parse_header_too_short() {
274338
assert false
275339
}
276340

341+
fn test_ptp_message_type_is_event() {
342+
assert PtpMessageType.sync.is_event() == true
343+
assert PtpMessageType.delay_req.is_event() == true
344+
assert PtpMessageType.follow_up.is_event() == false
345+
assert PtpMessageType.delay_resp.is_event() == false
346+
assert PtpMessageType.announce.is_event() == false
347+
}
348+
349+
fn test_compute_mean_path_delay_symmetric() {
350+
// t2-t1 = 100ns, t4-t3 = 100ns → delay = 100ns, offset = 0
351+
delay := compute_mean_path_delay(i64(1000), i64(1100), i64(1150), i64(1250))
352+
offset := compute_clock_offset(i64(1000), i64(1100), i64(1150), i64(1250))
353+
assert delay == i64(100)
354+
assert offset == i64(0)
355+
}
356+
357+
fn test_ptp_timestamp_to_nanos() {
358+
ts := PtpTimestamp{ seconds_msb: u16(0), seconds: u32(1), nanoseconds: u32(500_000_000) }
359+
nanos := ts.to_nanos()
360+
assert nanos == i64(1_500_000_000)
361+
}
362+

v-ecosystem/v-api-interfaces/v-smb/src/smb.v

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ const dialect_smb_300 = u16(0x0300)
4242
const dialect_smb_302 = u16(0x0302)
4343
const dialect_smb_311 = u16(0x0311)
4444

45+
// smb2_dialect_311 is the SMB 3.1.1 dialect identifier (public alias).
46+
pub const smb2_dialect_311 = u16(0x0311)
47+
4548
// SMB2 NEGOTIATE request structure size constant.
4649
const negotiate_struct_size = u16(36)
4750

@@ -71,6 +74,63 @@ pub enum ShareType {
7174
print // Printer share
7275
}
7376

77+
// --- SMB2 command enumeration ---
78+
79+
// Smb2Command enumerates SMB2 command codes (MS-SMB2 §2.2.1).
80+
pub enum Smb2Command {
81+
negotiate // 0x0000 — Negotiate protocol dialect
82+
session_setup // 0x0001 — Authenticate and set up session
83+
tree_connect // 0x0003 — Connect to a named share
84+
create // 0x0005 — Open or create a file
85+
read // 0x0008 — Read data from a file
86+
write // 0x0009 — Write data to a file
87+
close // 0x0006 — Close a file handle
88+
}
89+
90+
// min_structure_size returns the minimum StructureSize for the request body
91+
// of each SMB2 command, per MS-SMB2 specification tables.
92+
pub fn (c Smb2Command) min_structure_size() int {
93+
return match c {
94+
.negotiate { 36 } // §2.2.3
95+
.session_setup { 25 } // §2.2.5
96+
.tree_connect { 9 } // §2.2.9
97+
.create { 57 } // §2.2.13
98+
.read { 49 } // §2.2.19
99+
.write { 49 } // §2.2.21
100+
.close { 24 } // §2.2.15
101+
}
102+
}
103+
104+
// code returns the 2-byte little-endian command code for this command.
105+
pub fn (c Smb2Command) code() u16 {
106+
return match c {
107+
.negotiate { u16(0x0000) }
108+
.session_setup { u16(0x0001) }
109+
.tree_connect { u16(0x0003) }
110+
.create { u16(0x0005) }
111+
.read { u16(0x0008) }
112+
.write { u16(0x0009) }
113+
.close { u16(0x0006) }
114+
}
115+
}
116+
117+
// --- SMB2 header struct ---
118+
119+
// Smb2Header holds all fields of the 64-byte SMB2 packet header (MS-SMB2 §2.2.1.2).
120+
pub struct Smb2Header {
121+
pub:
122+
protocol_id [4]u8 // Must be [0xFE, 'S', 'M', 'B']
123+
header_length u16 // Fixed: 64
124+
credit_charge u16 // Credits consumed by this request
125+
status u32 // NT status code (response) or channel sequence (request)
126+
command Smb2Command // SMB2 command code
127+
credits_requested u16 // Credits requested (client) or granted (server)
128+
flags u32 // Header flags bitmask
129+
message_id u64 // Unique message identifier per session
130+
session_id u64 // Session identifier (0 before auth)
131+
signature [16]u8 // Message signature (when signing enabled)
132+
}
133+
74134
// --- Data structures ---
75135

76136
// SmbHeader holds the parsed SMB2 protocol header fields.
@@ -244,6 +304,87 @@ pub fn parse_header(data []u8) !SmbHeader {
244304
}
245305
}
246306

307+
// encode_smb2_header serialises a Smb2Header to the 64-byte SMB2 wire
308+
// format (MS-SMB2 §2.2.1.2), using little-endian byte order throughout.
309+
pub fn encode_smb2_header(h Smb2Header) []u8 {
310+
mut out := []u8{len: 0, cap: 64}
311+
// ProtocolId (4 bytes)
312+
for b in h.protocol_id { out << b }
313+
// StructureSize (2 bytes LE) = 64
314+
out << u8(64 & 0xFF) << u8(64 >> 8)
315+
// CreditCharge (2 bytes LE)
316+
out << u8(h.credit_charge & 0xFF) << u8(h.credit_charge >> 8)
317+
// Status (4 bytes LE)
318+
out << u8(h.status & 0xFF) << u8((h.status >> 8) & 0xFF)
319+
out << u8((h.status >> 16) & 0xFF) << u8((h.status >> 24) & 0xFF)
320+
// Command (2 bytes LE)
321+
cmd_code := h.command.code()
322+
out << u8(cmd_code & 0xFF) << u8(cmd_code >> 8)
323+
// CreditsRequested (2 bytes LE)
324+
out << u8(h.credits_requested & 0xFF) << u8(h.credits_requested >> 8)
325+
// Flags (4 bytes LE)
326+
out << u8(h.flags & 0xFF) << u8((h.flags >> 8) & 0xFF)
327+
out << u8((h.flags >> 16) & 0xFF) << u8((h.flags >> 24) & 0xFF)
328+
// NextCommand (4 bytes) = 0
329+
out << u8(0x00) << u8(0x00) << u8(0x00) << u8(0x00)
330+
// MessageId (8 bytes LE)
331+
for i in 0..8 { out << u8((h.message_id >> (u64(i) * 8)) & 0xFF) }
332+
// Reserved / AsyncId / TreeId (4 bytes) = 0
333+
out << u8(0x00) << u8(0x00) << u8(0x00) << u8(0x00)
334+
// SessionId (8 bytes LE)
335+
for i in 0..8 { out << u8((h.session_id >> (u64(i) * 8)) & 0xFF) }
336+
// Signature (16 bytes)
337+
for b in h.signature { out << b }
338+
return out
339+
}
340+
341+
// decode_smb2_header parses the first 64 bytes of a received SMB2 buffer.
342+
// Returns an error if the buffer is too short or the magic bytes are wrong.
343+
pub fn decode_smb2_header(data []u8) !Smb2Header {
344+
if data.len < 64 {
345+
return error('SMB2 header requires 64 bytes, got ${data.len}')
346+
}
347+
// Verify magic
348+
if data[0] != 0xFE || data[1] != 0x53 || data[2] != 0x4D || data[3] != 0x42 {
349+
return error('invalid SMB2 magic bytes: expected FE534D42')
350+
}
351+
mut pid := [4]u8{}
352+
for i in 0..4 { pid[i] = data[i] }
353+
credit_charge := (u16(data[5]) << 8) | u16(data[4])
354+
status := u32(data[8]) | (u32(data[9]) << 8) | (u32(data[10]) << 16) | (u32(data[11]) << 24)
355+
cmd_code := u16(data[12]) | (u16(data[13]) << 8)
356+
credits_req := u16(data[14]) | (u16(data[15]) << 8)
357+
flags := u32(data[16]) | (u32(data[17]) << 8) | (u32(data[18]) << 16) | (u32(data[19]) << 24)
358+
msg_id := u64(data[24]) | (u64(data[25]) << 8) | (u64(data[26]) << 16) | (u64(data[27]) << 24) |
359+
(u64(data[28]) << 32) | (u64(data[29]) << 40) | (u64(data[30]) << 48) | (u64(data[31]) << 56)
360+
sess_id := u64(data[40]) | (u64(data[41]) << 8) | (u64(data[42]) << 16) | (u64(data[43]) << 24) |
361+
(u64(data[44]) << 32) | (u64(data[45]) << 40) | (u64(data[46]) << 48) | (u64(data[47]) << 56)
362+
mut sig := [16]u8{}
363+
for i in 0..16 { sig[i] = data[48 + i] }
364+
command := match cmd_code {
365+
u16(0x0000) { Smb2Command.negotiate }
366+
u16(0x0001) { Smb2Command.session_setup }
367+
u16(0x0003) { Smb2Command.tree_connect }
368+
u16(0x0005) { Smb2Command.create }
369+
u16(0x0006) { Smb2Command.close }
370+
u16(0x0008) { Smb2Command.read }
371+
u16(0x0009) { Smb2Command.write }
372+
else { return error('unknown SMB2 command code 0x${cmd_code:04X}') }
373+
}
374+
return Smb2Header{
375+
protocol_id: pid
376+
header_length: u16(64)
377+
credit_charge: credit_charge
378+
status: status
379+
command: command
380+
credits_requested: credits_req
381+
flags: flags
382+
message_id: msg_id
383+
session_id: sess_id
384+
signature: sig
385+
}
386+
}
387+
247388
// --- Tests ---
248389

249390
fn test_smb2_magic() {
@@ -284,3 +425,41 @@ fn test_parse_header_invalid_magic() {
284425
assert false
285426
}
286427

428+
fn test_negotiate_request_contains_dialect_311() {
429+
pkt := encode_negotiate_request()
430+
// Dialects start after the 64-byte header + 34 bytes of NEGOTIATE body prefix
431+
// (StructureSize(2) + DialectCount(2) + SecurityMode(2) + Reserved(2) +
432+
// Capabilities(4) + ClientGuid(16) + NegotiateContextOffset(4) + DialectCount × 2)
433+
// Check that 0x0311 appears somewhere in the last 6 bytes (3 dialects × 2 bytes)
434+
last6 := pkt[pkt.len - 6..]
435+
found_311 := (last6[4] == u8(smb2_dialect_311 & 0xFF) && last6[5] == u8(smb2_dialect_311 >> 8))
436+
assert found_311
437+
}
438+
439+
fn test_smb2_command_min_structure_size() {
440+
assert Smb2Command.negotiate.min_structure_size() == 36
441+
assert Smb2Command.session_setup.min_structure_size() == 25
442+
assert Smb2Command.create.min_structure_size() == 57
443+
}
444+
445+
fn test_encode_decode_smb2_header_roundtrip() {
446+
hdr := Smb2Header{
447+
protocol_id: [u8(0xFE), 0x53, 0x4D, 0x42]!
448+
header_length: u16(64)
449+
credit_charge: u16(1)
450+
status: u32(0)
451+
command: .negotiate
452+
credits_requested: u16(31)
453+
flags: u32(0)
454+
message_id: u64(1)
455+
session_id: u64(0)
456+
signature: [16]u8{}
457+
}
458+
wire := encode_smb2_header(hdr)
459+
assert wire.len == 64
460+
decoded := decode_smb2_header(wire) or { panic(err) }
461+
assert decoded.command == hdr.command
462+
assert decoded.message_id == hdr.message_id
463+
assert decoded.protocol_id[0] == u8(0xFE)
464+
}
465+

0 commit comments

Comments
 (0)