diff --git a/IPTables.Net.Tests/IptcInterfaceTest.cs b/IPTables.Net.Tests/IptcInterfaceTest.cs index 50a47b4..75996a0 100644 --- a/IPTables.Net.Tests/IptcInterfaceTest.cs +++ b/IPTables.Net.Tests/IptcInterfaceTest.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Net; +using System.Runtime.InteropServices; using IPTables.Net.Iptables.NativeLibrary; namespace IPTables.Net.Tests @@ -58,6 +59,46 @@ public void Dispose() [Collection(SystemIptablesCollectionDefinition.Name)] public class IptcInterfaceTest : IClassFixture { + [StructLayout(LayoutKind.Sequential)] + private struct NativeIptIp + { + public uint Source; + public uint Destination; + public uint SourceMask; + public uint DestinationMask; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public byte[] InputInterface; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public byte[] OutputInterface; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public byte[] InputInterfaceMask; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public byte[] OutputInterfaceMask; + + public ushort Protocol; + public byte Flags; + public byte InverseFlags; + } + + [StructLayout(LayoutKind.Sequential)] + private struct NativeIptEntry + { + public NativeIptIp Ip; + public uint Cache; + public ushort TargetOffset; + public ushort NextOffset; + public uint ComeFrom; + public ulong PacketCount; + public ulong ByteCount; + } + + private const int XtEntryMatchNameOffset = sizeof(ushort); + private const int XtEntryMatchRevisionOffset = sizeof(ushort) + 29; + private readonly IptcInterfaceFixture _fixture; public IptcInterfaceTest(IptcInterfaceFixture fixture) @@ -119,6 +160,44 @@ public void TestRuleInput() Assert.Equal(0, IptcInterface.RefCount); } + [Fact] + public void TestCommitReportsIncompatibleMatchRevision() + { + _fixture.SkipIfNeeded(); + + Assert.Equal(4, _fixture.IpVersion); + Assert.Equal(0, IptcInterface.RefCount); + + using (var iptc = new IptcInterface("filter", _fixture.IpVersion)) + { + const string command = "iptables -A test2 -p tcp -m tcp --dport 8081 -j ACCEPT"; + Assert.Equal(1, iptc.ExecuteCommand("iptables -A test2 -j test3")); + Assert.Equal(1, iptc.ExecuteCommand(command)); + + var rules = iptc.GetRules("test2"); + Assert.Equal(2, rules.Count); + + var match = IntPtr.Add(rules[1], Marshal.SizeOf()); + Assert.Equal("tcp", Marshal.PtrToStringAnsi(IntPtr.Add(match, XtEntryMatchNameOffset))); + Marshal.WriteByte(match, XtEntryMatchRevisionOffset, byte.MaxValue); + + Assert.False(iptc.Commit()); + Assert.NotEqual(0, iptc.GetLastError()); + + var error = iptc.GetErrorString(); + Assert.Contains("incompatible match \"tcp\" revision 255", error); + Assert.Contains("chain \"test2\", rule 2", error); + Assert.DoesNotContain("(0)", error); + } + + Assert.NotEqual(0, + IptablesSystemTestSupport.Execute(_fixture.GetBinary(), "-C test2 -j test3", false)); + Assert.NotEqual(0, + IptablesSystemTestSupport.Execute(_fixture.GetBinary(), + "-C test2 -p tcp -m tcp --dport 8081 -j ACCEPT", false)); + Assert.Equal(0, IptcInterface.RefCount); + } + [Fact] public void TestRuleIp() { diff --git a/IPTables.Net/Iptables/NativeLibrary/IptcInterface.cs b/IPTables.Net/Iptables/NativeLibrary/IptcInterface.cs index e9cd1b3..7a61d44 100644 --- a/IPTables.Net/Iptables/NativeLibrary/IptcInterface.cs +++ b/IPTables.Net/Iptables/NativeLibrary/IptcInterface.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; +using System.Text; using IPTables.Net.Exceptions; using Serilog; @@ -388,6 +389,12 @@ public static extern int ip6tc_set_counter( [DllImport(Helper, SetLastError = true)] public static extern int execute_command6(string command, IntPtr h); + [DllImport(Helper, SetLastError = true, CharSet = CharSet.Ansi)] + private static extern int commit_handle4(IntPtr h, StringBuilder diagnostic, UIntPtr diagnosticLength); + + [DllImport(Helper, SetLastError = true, CharSet = CharSet.Ansi)] + private static extern int commit_handle6(IntPtr h, StringBuilder diagnostic, UIntPtr diagnosticLength); + [DllImport(Helper, SetLastError = true)] public static extern int init_helper4(); @@ -516,6 +523,8 @@ public void Dispose() private List _debugEntries = new List(); private ILogger logger; private int _ipVersion; + private int? _lastCommitError; + private string _lastCommitDiagnostic; private void DebugEntry(string message) { @@ -542,6 +551,8 @@ public void OpenTable(string table) { if (_handle != IntPtr.Zero) throw new IpTablesNetException("A table is already open, commit or discard first"); + _lastCommitError = null; + _lastCommitDiagnostic = null; if (_ipVersion == 4) _handle = init_handle4(table); else @@ -596,7 +607,7 @@ public List GetChains() public int GetLastError() { - return Marshal.GetLastWin32Error(); + return _lastCommitError ?? Marshal.GetLastWin32Error(); } public string GetErrorString() @@ -607,7 +618,10 @@ public string GetErrorString() error = iptc_strerror(lastError); else error = ip6tc_strerror(lastError); - return string.Format("({0}) {1}", lastError, Marshal.PtrToStringAnsi(error)); + var message = string.Format("({0}) {1}", lastError, Marshal.PtrToStringAnsi(error)); + if (!string.IsNullOrWhiteSpace(_lastCommitDiagnostic)) + message += "; " + _lastCommitDiagnostic; + return message; } @@ -674,17 +688,25 @@ public bool Commit() _debugEntries.Clear(); } - bool status; + _lastCommitError = null; + _lastCommitDiagnostic = null; + var diagnostic = new StringBuilder(1024); + int result; if (_ipVersion == 4) - status = iptc_commit(_handle) == 1; + result = commit_handle4(_handle, diagnostic, new UIntPtr((uint)diagnostic.Capacity)); else - status = ip6tc_commit(_handle) == 1; + result = commit_handle6(_handle, diagnostic, new UIntPtr((uint)diagnostic.Capacity)); + + var commitError = Marshal.GetLastWin32Error(); + _handle = IntPtr.Zero; + + bool status = result == 1; if (!status) - Free(); - else - //Commit includes free - _handle = IntPtr.Zero; + { + _lastCommitError = commitError; + _lastCommitDiagnostic = diagnostic.ToString(); + } return status; } @@ -737,4 +759,4 @@ public bool FlushChain(string chainName) return ip6tc_flush_entries(chainName, _handle) == 1; } } -} \ No newline at end of file +} diff --git a/ipthelper/ipthelper.c b/ipthelper/ipthelper.c index 118a3db..31097d3 100644 --- a/ipthelper/ipthelper.c +++ b/ipthelper/ipthelper.c @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -53,6 +54,16 @@ #endif #include +#ifndef IPT_SO_GET_REVISION_MATCH +#define IPT_SO_GET_REVISION_MATCH (IPT_BASE_CTL + 2) +#define IPT_SO_GET_REVISION_TARGET (IPT_BASE_CTL + 3) +#endif + +#ifndef IP6T_SO_GET_REVISION_MATCH +#define IP6T_SO_GET_REVISION_MATCH 68 +#define IP6T_SO_GET_REVISION_TARGET 69 +#endif + char* errbuffer = NULL; jmp_buf buf = { }; @@ -922,6 +933,216 @@ EXPORT int execute_command6(const char* rule, void *h){ return ret; } +typedef struct { + int socket; + int ip_version; + int protocol; + int match_option; + int target_option; + void *handle; + const char *chain; + unsigned int rule_number; + char *diagnostic; + size_t diagnostic_length; +} revision_scan_t; + +static int check_extension_revision(revision_scan_t *scan, const char *kind, + const char *name, unsigned char revision, int option) +{ + struct xt_get_revision requested_revision; + socklen_t requested_revision_length = sizeof(requested_revision); + int query_errno; + + memset(&requested_revision, 0, sizeof(requested_revision)); + strncpy(requested_revision.name, name, sizeof(requested_revision.name) - 1); + requested_revision.revision = revision; + + if (getsockopt(scan->socket, scan->protocol, option, &requested_revision, + &requested_revision_length) >= 0) + return 0; + + query_errno = errno; + if (query_errno == EPROTONOSUPPORT || query_errno == EPROTOTYPE) { + snprintf(scan->diagnostic, scan->diagnostic_length, + "incompatible %s \"%s\" revision %u in chain \"%s\", rule %u", + kind, name, revision, scan->chain, scan->rule_number); + return 1; + } + + if (query_errno == ENOENT) { + snprintf(scan->diagnostic, scan->diagnostic_length, + "missing %s \"%s\" revision %u in chain \"%s\", rule %u", + kind, name, revision, scan->chain, scan->rule_number); + return 1; + } + + return 0; +} + +static int check_match_revision(const struct xt_entry_match *match, + revision_scan_t *scan) +{ + if (match->u.user.name[0] == '\0') + return 0; + + return check_extension_revision(scan, "match", match->u.user.name, + match->u.user.revision, scan->match_option); +} + +static int check_target_revision(const struct xt_entry_target *target, + revision_scan_t *scan) +{ + if (target->u.user.name[0] == '\0' || + strcmp(target->u.user.name, XT_ERROR_TARGET) == 0) + return 0; + if ((scan->ip_version == 4 && iptc_is_chain(target->u.user.name, scan->handle)) || + (scan->ip_version == 6 && ip6tc_is_chain(target->u.user.name, scan->handle))) + return 0; + + return check_extension_revision(scan, "target", target->u.user.name, + target->u.user.revision, scan->target_option); +} + +static void diagnose_revision4(void *handle, char *diagnostic, + size_t diagnostic_length) +{ + const char *chain; + revision_scan_t scan; + + if (diagnostic == NULL || diagnostic_length == 0) + return; + diagnostic[0] = '\0'; + + memset(&scan, 0, sizeof(scan)); + scan.socket = socket(AF_INET, SOCK_RAW, IPPROTO_RAW); + if (scan.socket < 0) + return; + scan.ip_version = 4; + scan.protocol = IPPROTO_IP; + scan.match_option = IPT_SO_GET_REVISION_MATCH; + scan.target_option = IPT_SO_GET_REVISION_TARGET; + scan.diagnostic = diagnostic; + scan.diagnostic_length = diagnostic_length; + scan.handle = handle; + + for (chain = iptc_first_chain(handle); chain != NULL; + chain = iptc_next_chain(handle)) { + const struct ipt_entry *entry; + + scan.chain = chain; + scan.rule_number = 1; + for (entry = iptc_first_rule(chain, handle); entry != NULL; + entry = iptc_next_rule(entry, handle), ++scan.rule_number) { + struct xt_entry_target *target; + + if (IPT_MATCH_ITERATE(entry, check_match_revision, &scan) != 0) + goto done; + + target = ipt_get_target((struct ipt_entry *)entry); + if (check_target_revision(target, &scan) != 0) + goto done; + } + } + +done: + close(scan.socket); +} + +static void diagnose_revision6(void *handle, char *diagnostic, + size_t diagnostic_length) +{ + const char *chain; + revision_scan_t scan; + + if (diagnostic == NULL || diagnostic_length == 0) + return; + diagnostic[0] = '\0'; + + memset(&scan, 0, sizeof(scan)); + scan.socket = socket(AF_INET6, SOCK_RAW, IPPROTO_RAW); + if (scan.socket < 0) + return; + scan.ip_version = 6; + scan.protocol = IPPROTO_IPV6; + scan.match_option = IP6T_SO_GET_REVISION_MATCH; + scan.target_option = IP6T_SO_GET_REVISION_TARGET; + scan.diagnostic = diagnostic; + scan.diagnostic_length = diagnostic_length; + scan.handle = handle; + + for (chain = ip6tc_first_chain(handle); chain != NULL; + chain = ip6tc_next_chain(handle)) { + const struct ip6t_entry *entry; + + scan.chain = chain; + scan.rule_number = 1; + for (entry = ip6tc_first_rule(chain, handle); entry != NULL; + entry = ip6tc_next_rule(entry, handle), ++scan.rule_number) { + struct xt_entry_target *target; + + if (IP6T_MATCH_ITERATE(entry, check_match_revision, &scan) != 0) + goto done; + + target = ip6t_get_target((struct ip6t_entry *)entry); + if (check_target_revision(target, &scan) != 0) + goto done; + } + } + +done: + close(scan.socket); +} + +EXPORT int commit_handle4(void *handle, char *diagnostic, + size_t diagnostic_length) +{ + int status; + int commit_errno; + + if (diagnostic != NULL && diagnostic_length != 0) + diagnostic[0] = '\0'; + if (handle == NULL) { + errno = EINVAL; + return 0; + } + + status = iptc_commit(handle); + commit_errno = errno; + if (!status) { + if (commit_errno == EPROTOTYPE || + commit_errno == EPROTONOSUPPORT || commit_errno == ENOENT) + diagnose_revision4(handle, diagnostic, diagnostic_length); + iptc_free(handle); + } + errno = commit_errno; + return status; +} + +EXPORT int commit_handle6(void *handle, char *diagnostic, + size_t diagnostic_length) +{ + int status; + int commit_errno; + + if (diagnostic != NULL && diagnostic_length != 0) + diagnostic[0] = '\0'; + if (handle == NULL) { + errno = EINVAL; + return 0; + } + + status = ip6tc_commit(handle); + commit_errno = errno; + if (!status) { + if (commit_errno == EPROTOTYPE || + commit_errno == EPROTONOSUPPORT || commit_errno == ENOENT) + diagnose_revision6(handle, diagnostic, diagnostic_length); + ip6tc_free(handle); + } + errno = commit_errno; + return status; +} + EXPORT int init_helper4(void){ int c; stdout_pipe.shm = -1; diff --git a/ipthelper/ipthelper.h b/ipthelper/ipthelper.h index f62935e..4cbe50f 100644 --- a/ipthelper/ipthelper.h +++ b/ipthelper/ipthelper.h @@ -1,4 +1,5 @@ #pragma once +#include #include #include @@ -24,6 +25,8 @@ extern "C" { extern EXPORT const char* output_rule6(const struct ip6t_entry *e, void *h, const char *chain, int counters); extern EXPORT int execute_command6(const char* rule, void *h); extern EXPORT int execute_command4(const char* rule, void *h); + extern EXPORT int commit_handle4(void *h, char *diagnostic, size_t diagnostic_length); + extern EXPORT int commit_handle6(void *h, char *diagnostic, size_t diagnostic_length); extern EXPORT int init_helper4(void); extern EXPORT int init_helper6(void); extern EXPORT void* init_handle4(const char* table);