Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
# Don't let one platform's failure cancel the others — we want to see each
# platform's real result independently (a flake on one shouldn't hide the rest).
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]

Expand Down Expand Up @@ -53,6 +56,10 @@ jobs:
- name: Build
run: dotnet build --no-restore --configuration Release

# Category!=LiveNetwork excludes the opt-in tests that make real outbound
# DNS/HTTP calls (tagged [Trait("Category","LiveNetwork")]); those depend on
# the runner's network and false-red CI (#495). Run them on demand locally:
# dotnet test --filter "Category=LiveNetwork"
- name: Test
run: dotnet test --no-build --configuration Release --verbosity normal
run: dotnet test --no-build --configuration Release --verbosity normal --filter "Category!=LiveNetwork"
timeout-minutes: 10
218 changes: 25 additions & 193 deletions Compilation/RuntimeEmitter.Dns.cs
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,7 @@ private void EmitDnsResolveRecord(TypeBuilder typeBuilder, EmittedRuntime runtim
// Wire protocol query types
const int qtMX = 15, qtTXT = 16, qtSRV = 33, qtCNAME = 5, qtNS = 2;
const int qtSOA = 6, qtPTR = 12, qtCAA = 257, qtNAPTR = 35;
const int qtA = 1, qtAAAA = 28;

// === MX section — DnsDoQuery returns pre-parsed List<object?> of dicts ===
il.MarkLabel(mxLabel);
Expand Down Expand Up @@ -665,14 +666,14 @@ private void EmitDnsResolveRecord(TypeBuilder typeBuilder, EmittedRuntime runtim
EmitDnsResolveViaWireProtocol(il, runtime, hostnameLocal, resultLocal, qtNAPTR, wrapWithConvertList: true);
il.Emit(OpCodes.Br, returnLabel);

// === A section (uses System.Net.Dns) ===
// === A section — wire protocol (List<object?> of IP strings) ===
il.MarkLabel(aLabel);
EmitDnsResolveAddresses(il, runtime, hostnameLocal, resultLocal, AddressFamily.InterNetwork);
EmitDnsResolveViaWireProtocol(il, runtime, hostnameLocal, resultLocal, qtA, wrapWithConvertList: false);
il.Emit(OpCodes.Br, returnLabel);

// === AAAA section (uses System.Net.Dns) ===
// === AAAA section — wire protocol (List<object?> of IP strings) ===
il.MarkLabel(aaaaLabel);
EmitDnsResolveAddresses(il, runtime, hostnameLocal, resultLocal, AddressFamily.InterNetworkV6);
EmitDnsResolveViaWireProtocol(il, runtime, hostnameLocal, resultLocal, qtAAAA, wrapWithConvertList: false);
il.Emit(OpCodes.Br, returnLabel);

// === Unknown rrtype ===
Expand Down Expand Up @@ -724,84 +725,6 @@ private static void EmitRrtypeCheck(ILGenerator il, LocalBuilder rrtypeLocal, st
il.Emit(OpCodes.Brtrue, target);
}

/// <summary>
/// Emits IL for A/AAAA resolution using System.Net.Dns.GetHostEntry.
/// Stores $Array result in resultLocal.
/// </summary>
private void EmitDnsResolveAddresses(ILGenerator il, EmittedRuntime runtime,
LocalBuilder hostnameLocal, LocalBuilder resultLocal, AddressFamily family)
{
// Dns.GetHostEntry(hostname)
il.Emit(OpCodes.Ldloc, hostnameLocal);
il.Emit(OpCodes.Call, typeof(Dns).GetMethod("GetHostEntry", [typeof(string)])!);
var hostEntryLocal = il.DeclareLocal(typeof(IPHostEntry));
il.Emit(OpCodes.Stloc, hostEntryLocal);

// Get AddressList
il.Emit(OpCodes.Ldloc, hostEntryLocal);
il.Emit(OpCodes.Callvirt, typeof(IPHostEntry).GetProperty("AddressList")!.GetGetMethod()!);
var addressListLocal = il.DeclareLocal(typeof(IPAddress[]));
il.Emit(OpCodes.Stloc, addressListLocal);

// Build list of matching addresses
il.Emit(OpCodes.Newobj, _types.ListOfObject.GetConstructor(Type.EmptyTypes)!);
var listLocal = il.DeclareLocal(_types.ListOfObject);
il.Emit(OpCodes.Stloc, listLocal);

var indexLocal = il.DeclareLocal(typeof(int));
il.Emit(OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Stloc, indexLocal);
var loopStart = il.DefineLabel();
var loopCond = il.DefineLabel();
il.Emit(OpCodes.Br, loopCond);

il.MarkLabel(loopStart);
// Check address family
il.Emit(OpCodes.Ldloc, addressListLocal);
il.Emit(OpCodes.Ldloc, indexLocal);
il.Emit(OpCodes.Ldelem_Ref);
il.Emit(OpCodes.Callvirt, typeof(IPAddress).GetProperty("AddressFamily")!.GetGetMethod()!);
il.Emit(OpCodes.Ldc_I4, (int)family);
var skipLabel = il.DefineLabel();
il.Emit(OpCodes.Bne_Un, skipLabel);

// Match - add to list
il.Emit(OpCodes.Ldloc, listLocal);
il.Emit(OpCodes.Ldloc, addressListLocal);
il.Emit(OpCodes.Ldloc, indexLocal);
il.Emit(OpCodes.Ldelem_Ref);
il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(typeof(IPAddress), "ToString"));
il.Emit(OpCodes.Callvirt, _types.ListOfObject.GetMethod("Add")!);

il.MarkLabel(skipLabel);
il.Emit(OpCodes.Ldloc, indexLocal);
il.Emit(OpCodes.Ldc_I4_1);
il.Emit(OpCodes.Add);
il.Emit(OpCodes.Stloc, indexLocal);

il.MarkLabel(loopCond);
il.Emit(OpCodes.Ldloc, indexLocal);
il.Emit(OpCodes.Ldloc, addressListLocal);
il.Emit(OpCodes.Ldlen);
il.Emit(OpCodes.Conv_I4);
il.Emit(OpCodes.Blt, loopStart);

// Check if empty
il.Emit(OpCodes.Ldloc, listLocal);
il.Emit(OpCodes.Callvirt, _types.GetPropertyGetter(_types.ListOfObject, "Count"));
var notEmptyLabel = il.DefineLabel();
il.Emit(OpCodes.Brtrue, notEmptyLabel);

il.Emit(OpCodes.Ldc_I4, (int)SocketError.HostNotFound);
il.Emit(OpCodes.Newobj, typeof(SocketException).GetConstructor([typeof(int)])!);
il.Emit(OpCodes.Throw);

il.MarkLabel(notEmptyLabel);
il.Emit(OpCodes.Ldloc, listLocal);
il.Emit(OpCodes.Newobj, runtime.TSArrayCtor);
il.Emit(OpCodes.Stloc, resultLocal);
}

/// <summary>
/// Emits DnsDoQuery: orchestrator that builds query, sends, and parses response.
/// Uses emitted wire protocol helpers — no DnsClient dependency.
Expand Down Expand Up @@ -3477,72 +3400,12 @@ private void EmitDnsResolveByFamily(TypeBuilder typeBuilder, EmittedRuntime runt

il.BeginExceptionBlock();

// Call DnsLookup(hostname, family_number) - returns { address, family } but we need array of strings
// Use System.Net.Dns.GetHostEntry directly for array result
// hostname.ToString()
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString"));

// Call Dns.GetHostEntry(hostname) -> IPHostEntry
il.Emit(OpCodes.Call, typeof(Dns).GetMethod("GetHostEntry", [typeof(string)])!);
var hostEntryLocal = il.DeclareLocal(typeof(IPHostEntry));
il.Emit(OpCodes.Stloc, hostEntryLocal);

// Get AddressList
il.Emit(OpCodes.Ldloc, hostEntryLocal);
il.Emit(OpCodes.Callvirt, typeof(IPHostEntry).GetProperty("AddressList")!.GetGetMethod()!);
var addressListLocal = il.DeclareLocal(typeof(IPAddress[]));
il.Emit(OpCodes.Stloc, addressListLocal);

// Build List<object> of matching addresses
il.Emit(OpCodes.Newobj, _types.ListOfObject.GetConstructor(Type.EmptyTypes)!);
var listLocal = il.DeclareLocal(_types.ListOfObject);
il.Emit(OpCodes.Stloc, listLocal);

// for (int i = 0; i < addressList.Length; i++)
var indexLocal = il.DeclareLocal(typeof(int));
il.Emit(OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Stloc, indexLocal);
var loopStart = il.DefineLabel();
var loopCond = il.DefineLabel();
il.Emit(OpCodes.Br, loopCond);

il.MarkLabel(loopStart);
// if (addressList[i].AddressFamily == target)
il.Emit(OpCodes.Ldloc, addressListLocal);
il.Emit(OpCodes.Ldloc, indexLocal);
il.Emit(OpCodes.Ldelem_Ref);
il.Emit(OpCodes.Callvirt, typeof(IPAddress).GetProperty("AddressFamily")!.GetGetMethod()!);
il.Emit(OpCodes.Ldc_I4, family == 4 ? (int)AddressFamily.InterNetwork : (int)AddressFamily.InterNetworkV6);
var skipLabel = il.DefineLabel();
il.Emit(OpCodes.Bne_Un, skipLabel);

// list.Add(address.ToString())
il.Emit(OpCodes.Ldloc, listLocal);
il.Emit(OpCodes.Ldloc, addressListLocal);
il.Emit(OpCodes.Ldloc, indexLocal);
il.Emit(OpCodes.Ldelem_Ref);
il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(typeof(IPAddress), "ToString"));
il.Emit(OpCodes.Callvirt, _types.ListOfObject.GetMethod("Add")!);

il.MarkLabel(skipLabel);
// i++
il.Emit(OpCodes.Ldloc, indexLocal);
il.Emit(OpCodes.Ldc_I4_1);
il.Emit(OpCodes.Add);
il.Emit(OpCodes.Stloc, indexLocal);

// i < addressList.Length
il.MarkLabel(loopCond);
il.Emit(OpCodes.Ldloc, indexLocal);
il.Emit(OpCodes.Ldloc, addressListLocal);
il.Emit(OpCodes.Ldlen);
il.Emit(OpCodes.Conv_I4);
il.Emit(OpCodes.Blt, loopStart);

// Create $Array from list
il.Emit(OpCodes.Ldloc, listLocal);
il.Emit(OpCodes.Newobj, runtime.TSArrayCtor);
// resultLocal = DnsResolveRecord(hostname, "A"|"AAAA") — A/AAAA via the emitted
// DNS wire protocol (honors SHARPTS_DNS_SERVER, Node/c-ares semantics — no
// hosts-file lookup), mirroring the interpreter. Returns a $Array of IP strings.
il.Emit(OpCodes.Ldarg_0); // hostname (object)
il.Emit(OpCodes.Ldstr, family == 4 ? "A" : "AAAA");
il.Emit(OpCodes.Call, runtime.DnsResolveRecord);
il.Emit(OpCodes.Stloc, resultLocal);

// callback.Invoke([null, result])
Expand Down Expand Up @@ -3720,52 +3583,21 @@ private void EmitDnsResolveWrapper(TypeBuilder typeBuilder, EmittedRuntime runti

il.BeginExceptionBlock();

il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(_types.Object, "ToString"));
il.Emit(OpCodes.Call, typeof(Dns).GetMethod("GetHostEntry", [typeof(string)])!);
var hostEntryLocal = il.DeclareLocal(typeof(IPHostEntry));
il.Emit(OpCodes.Stloc, hostEntryLocal);

// Build list from all addresses
il.Emit(OpCodes.Newobj, _types.ListOfObject.GetConstructor(Type.EmptyTypes)!);
var listLocal = il.DeclareLocal(_types.ListOfObject);
il.Emit(OpCodes.Stloc, listLocal);

il.Emit(OpCodes.Ldloc, hostEntryLocal);
il.Emit(OpCodes.Callvirt, typeof(IPHostEntry).GetProperty("AddressList")!.GetGetMethod()!);
var addrArrayLocal = il.DeclareLocal(typeof(IPAddress[]));
il.Emit(OpCodes.Stloc, addrArrayLocal);

var idxLocal = il.DeclareLocal(typeof(int));
il.Emit(OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Stloc, idxLocal);
var loopS = il.DefineLabel();
var loopC = il.DefineLabel();
il.Emit(OpCodes.Br, loopC);
il.MarkLabel(loopS);

il.Emit(OpCodes.Ldloc, listLocal);
il.Emit(OpCodes.Ldloc, addrArrayLocal);
il.Emit(OpCodes.Ldloc, idxLocal);
il.Emit(OpCodes.Ldelem_Ref);
il.Emit(OpCodes.Callvirt, _types.GetMethodNoParams(typeof(IPAddress), "ToString"));
il.Emit(OpCodes.Callvirt, _types.ListOfObject.GetMethod("Add")!);

il.Emit(OpCodes.Ldloc, idxLocal);
il.Emit(OpCodes.Ldc_I4_1);
il.Emit(OpCodes.Add);
il.Emit(OpCodes.Stloc, idxLocal);
il.MarkLabel(loopC);
il.Emit(OpCodes.Ldloc, idxLocal);
il.Emit(OpCodes.Ldloc, addrArrayLocal);
il.Emit(OpCodes.Ldlen);
il.Emit(OpCodes.Conv_I4);
il.Emit(OpCodes.Blt, loopS);

// Create $Array
// resultLocal = DnsResolveRecord(hostname, rrtype ?? "A") — A/AAAA and the
// default rrtype route through the emitted DNS wire protocol (honors
// SHARPTS_DNS_SERVER, Node/c-ares semantics), mirroring the interpreter.
// Returns a $Array of IP strings.
il.Emit(OpCodes.Ldarg_0); // hostname (object)
il.Emit(OpCodes.Ldloc, rrtypeLocal);
il.Emit(OpCodes.Isinst, _types.String); // rrtype string, or null
il.Emit(OpCodes.Dup);
var haveRrtypeLabel = il.DefineLabel();
il.Emit(OpCodes.Brtrue, haveRrtypeLabel);
il.Emit(OpCodes.Pop); // drop null
il.Emit(OpCodes.Ldstr, "A"); // default rrtype
il.MarkLabel(haveRrtypeLabel);
il.Emit(OpCodes.Call, runtime.DnsResolveRecord);
var resultLocal = il.DeclareLocal(_types.Object);
il.Emit(OpCodes.Ldloc, listLocal);
il.Emit(OpCodes.Newobj, runtime.TSArrayCtor);
il.Emit(OpCodes.Stloc, resultLocal);

// callback(null, result)
Expand Down
35 changes: 10 additions & 25 deletions Runtime/BuiltIns/Modules/DnsRecordResolver.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
using System.Net;
using System.Net.Sockets;

namespace SharpTS.Runtime.BuiltIns.Modules;

/// <summary>
Expand Down Expand Up @@ -73,27 +70,15 @@ public static object ResolveByType(string hostname, string rrtype)
public static List<object?> ResolveNaptr(string hostname) =>
(List<object?>)DnsWireProtocol.Query(hostname, DnsWireProtocol.TypeNAPTR);

public static List<object?> ResolveA(string hostname)
{
var hostEntry = Dns.GetHostEntry(hostname);
var addresses = hostEntry.AddressList
.Where(a => a.AddressFamily == AddressFamily.InterNetwork)
.ToArray();
if (addresses.Length == 0)
throw new SocketException((int)SocketError.HostNotFound);
return addresses.Select(a => (object?)a.ToString()).ToList();
}
// resolve4/resolve6 (A/AAAA) use the DNS wire protocol — like Node's c-ares
// resolver, and unlike dns.lookup — so they honor SHARPTS_DNS_SERVER and do NOT
// consult the hosts file. dns.lookup (DnsModuleInterpreter.LookupCore) stays on
// the OS resolver for hosts-file resolution.
public static List<object?> ResolveA(string hostname) =>
(List<object?>)DnsWireProtocol.Query(hostname, DnsWireProtocol.TypeA);

public static List<object?> ResolveAaaa(string hostname)
{
var hostEntry = Dns.GetHostEntry(hostname);
var addresses = hostEntry.AddressList
.Where(a => a.AddressFamily == AddressFamily.InterNetworkV6)
.ToArray();
if (addresses.Length == 0)
throw new SocketException((int)SocketError.HostNotFound);
return addresses.Select(a => (object?)a.ToString()).ToList();
}
public static List<object?> ResolveAaaa(string hostname) =>
(List<object?>)DnsWireProtocol.Query(hostname, DnsWireProtocol.TypeAAAA);

#region Overloads with custom DNS server

Expand All @@ -112,8 +97,8 @@ public static object Resolve(string hostname, string rrtype, string server) =>
"PTR" => DnsWireProtocol.Query(hostname, DnsWireProtocol.TypePTR, server),
"CAA" => DnsWireProtocol.Query(hostname, DnsWireProtocol.TypeCAA, server),
"NAPTR" => DnsWireProtocol.Query(hostname, DnsWireProtocol.TypeNAPTR, server),
"A" => ResolveA(hostname), // A/AAAA use system DNS (Dns.GetHostEntry)
"AAAA" => ResolveAaaa(hostname),
"A" => DnsWireProtocol.Query(hostname, DnsWireProtocol.TypeA, server),
"AAAA" => DnsWireProtocol.Query(hostname, DnsWireProtocol.TypeAAAA, server),
_ => throw new Exception($"Runtime Error: dns.resolve unknown rrtype: {rrtype}")
};

Expand Down
26 changes: 15 additions & 11 deletions Runtime/BuiltIns/Modules/Interpreter/DnsModuleInterpreter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -530,13 +530,13 @@ private static SharpTSObject CreateDnsError(string code, string hostname)

private static SharpTSArray ResolveAddresses(string hostname, AddressFamily? family)
{
var hostEntry = Dns.GetHostEntry(hostname);
var addresses = hostEntry.AddressList;
if (family != null)
addresses = addresses.Where(a => a.AddressFamily == family).ToArray();
if (addresses.Length == 0)
throw new SocketException((int)SocketError.HostNotFound);
return new SharpTSArray(addresses.Select(a => (object?)a.ToString()).ToList());
// resolve4/resolve6 go through the DNS wire protocol (honors SHARPTS_DNS_SERVER,
// Node/c-ares semantics — no hosts-file lookup). dns.lookup stays on
// Dns.GetHostEntry. DnsWireProtocol.ParseResponse throws on NXDOMAIN/ENODATA.
var queryType = family == AddressFamily.InterNetworkV6
? DnsWireProtocol.TypeAAAA
: DnsWireProtocol.TypeA;
return new SharpTSArray((List<object?>)DnsWireProtocol.Query(hostname, queryType));
}

/// <summary>
Expand Down Expand Up @@ -682,11 +682,13 @@ private static RuntimeValue Resolve4Async(Interp interpreter, RuntimeValue recei
interpreter.Unref();
}, isInterval: false);
}
catch (SocketException ex)
// Wire protocol throws Exception ("Runtime Error: dns.x ECODE host"), not
// SocketException; ExtractErrorCode parses both shapes.
catch (Exception ex)
{
interpreter.ScheduleTimer(0, 0, () =>
{
interpreter.InvokeGuestCallback(callback, [CreateDnsError(GetErrorCode(ex), hostname), null]);
interpreter.InvokeGuestCallback(callback, [CreateDnsError(ExtractErrorCode(ex), hostname), null]);
interpreter.Unref();
}, isInterval: false);
}
Expand Down Expand Up @@ -716,11 +718,13 @@ private static RuntimeValue Resolve6Async(Interp interpreter, RuntimeValue recei
interpreter.Unref();
}, isInterval: false);
}
catch (SocketException ex)
// Wire protocol throws Exception ("Runtime Error: dns.x ECODE host"), not
// SocketException; ExtractErrorCode parses both shapes.
catch (Exception ex)
{
interpreter.ScheduleTimer(0, 0, () =>
{
interpreter.InvokeGuestCallback(callback, [CreateDnsError(GetErrorCode(ex), hostname), null]);
interpreter.InvokeGuestCallback(callback, [CreateDnsError(ExtractErrorCode(ex), hostname), null]);
interpreter.Unref();
}, isInterval: false);
}
Expand Down
4 changes: 2 additions & 2 deletions STATUS-NODE.md
Original file line number Diff line number Diff line change
Expand Up @@ -716,8 +716,8 @@ This document tracks Node.js module and API implementation status in SharpTS.
| `ALL` | ✅ | Return all addresses hint |
| **Async Resolution** | | |
| `resolve` | ✅ | Async callback-based; supports rrtype parameter (A, AAAA, MX, TXT, SRV, CNAME, NS, SOA, PTR, CAA, NAPTR) |
| `resolve4` | ✅ | Async callback-based; resolves IPv4 addresses |
| `resolve6` | ✅ | Async callback-based; resolves IPv6 addresses |
| `resolve4` | ✅ | Async callback-based; resolves IPv4 via the DNS wire protocol (c-ares-style, honors `SHARPTS_DNS_SERVER`; does not read the hosts file — use `lookup` for that) |
| `resolve6` | ✅ | Async callback-based; resolves IPv6 via the DNS wire protocol (c-ares-style; does not read the hosts file) |
| `reverse` | ✅ | Async callback-based; reverse DNS lookup |
| `resolveMx` | ✅ | MX records → `[{ exchange, priority }]` |
| `resolveTxt` | ✅ | TXT records → `string[][]` (chunks per record) |
Expand Down
Loading
Loading